Merge pull request #4316 from SillyTavern/staging Staging

9a191c41e8b8fa7d203974c0b0debdfe1146a7a0

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
147 files changed, +2065 -5039Ignore whitespace
.github/readme-de_de.md+12 -301
@@ -1,6 +1,5 @@
1> [!IMPORTANT] 1> [!IMPORTANT]
2> Die hier veröffentlichten Informationen sind möglicherweise veraltet oder unvollständig. Für aktuelle Informationen nutzen Sie bitte die englische Version.2> Die hier veröffentlichten Informationen sind möglicherweise veraltet oder unvollständig. Für aktuelle Informationen nutzen Sie bitte die englische Version.
3> Letztes Update dieser README: 28.9.2024
43
5<a name="readme-top"></a>4<a name="readme-top"></a>
65
@@ -11,7 +10,7 @@
11[English](readme.md) | German | [中文](readme-zh_cn.md) | [繁體中文](readme-zh_tw.md) | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md) | [한국어](readme-ko_kr.md)10[English](readme.md) | German | [中文](readme-zh_cn.md) | [繁體中文](readme-zh_tw.md) | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md) | [한국어](readme-ko_kr.md)
1211
13[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)12[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
14[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)13[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/forks)
15[![GitHub Issues](https://img.shields.io/github/issues/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/issues)14[![GitHub Issues](https://img.shields.io/github/issues/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/issues)
16[![GitHub Pull Requests](https://img.shields.io/github/issues-pr/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/pulls)15[![GitHub Pull Requests](https://img.shields.io/github/issues-pr/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/pulls)
1716
@@ -27,7 +26,7 @@ Wir haben eine [Dokumentationswebsite](https://docs.sillytavern.app/), um die me
2726
28SillyTavern (oder ST abgekürtz) ist eine lokal installierte Benutzeroberfläche, die es dir ermöglicht, mit Textgenerations-LLMs, Bildgenerierungsmaschinen und TTS-Sprachmodellen zu interagieren.27SillyTavern (oder ST abgekürtz) ist eine lokal installierte Benutzeroberfläche, die es dir ermöglicht, mit Textgenerations-LLMs, Bildgenerierungsmaschinen und TTS-Sprachmodellen zu interagieren.
2928
30Angefangen im Februar 2023 als Fork von TavernAI 1.2.8 hat SillyTavern nun über 100 Mitwirkende und 2 Jahre unabhängiger Entwicklung hinter sich und dient weiterhin als führende Software für versierte KI-Hobbyisten.29Angefangen im Februar 2023 als Fork von TavernAI 1.2.8 hat SillyTavern nun über 200 Mitwirkende und 2 Jahre unabhängiger Entwicklung hinter sich und dient weiterhin als führende Software für versierte KI-Hobbyisten.
3130
32## Unsere Vision31## Unsere Vision
3332
@@ -35,29 +34,9 @@ Angefangen im Februar 2023 als Fork von TavernAI 1.2.8 hat SillyTavern nun über
352. Wir bieten weder Online- oder gehosteten Dienste an, noch verfolgen wir programmgesteuert Benutzerdaten.342. Wir bieten weder Online- oder gehosteten Dienste an, noch verfolgen wir programmgesteuert Benutzerdaten.
363. SillyTavern ist ein Herzensprojekt, das von einer engagierten Community von LLM-Enthusiasten unterstützt wird, und wird immer kostenlos und Open Source sein.353. SillyTavern ist ein Herzensprojekt, das von einer engagierten Community von LLM-Enthusiasten unterstützt wird, und wird immer kostenlos und Open Source sein.
3736
38## Branches37## Brauche ich einen leistungsstarken PC, um SillyTavern auszuführen?
3938
40SillyTavern wird mit einem Branchsystem entwickelt, um ein reibungsloses Erlebnis für alle Nutzer zu gewährleisten.39Die Hardwareanforderungen sind minimal: Es läuft auf allem, was NodeJS 18 oder höher ausführen kann. Wenn du LLM-Inferenz auf deinem lokalen Rechner durchführen möchtest, empfehlen wir eine NVIDIA-Grafikkarte der 3000er-Serie mit mindestens 6 GB VRAM, aber die tatsächlichen Anforderungen können je nach Modell und Backend, das du verwendest, variieren.
41
42* `release` -🌟 **Empfohlen für die meisten Nutzer.** Dies ist der stabilste und empfohlene Branch, der nur aktualisiert wird, wenn wichtige Versionen veröffentlicht werden. Er ist für die Mehrheit der Nutzer geeignet. Typischerweise einmal im Monat aktualisiert.
43* `staging` - ⚠️ **Nicht für den gelegentlichen Gebrauch empfohlen.** Dieser Branch enthält die neuesten Funktionen, kann jedoch jederzeit instabil sein. Nur für Power-User und Enthusiasten. Mehrmals täglich aktualisiert.
44
45Wenn du nicht vertraut mit der Verwendung der git CLI bist oder nicht verstehst, was ein Branch ist, mach dir keine Sorgen! Der Release-Branch ist immer die bevorzugte Option für dich.
46
47## Was brauche ich zusätzlich zu SillyTavern?
48
49Da SillyTavern nur eine Benutzeroberfläche ist, benötigst du Zugriff auf ein LLM-Backend, um Inferenz bereitzustellen. Du kannst AI Horde für sofortiges Chatten ohne weitere Einrichtung verwenden. Darüber hinaus unterstützen wir viele andere lokale und cloudbasierte LLM-Backends: OpenAI-kompatible API, KoboldAI, Tabby und viele mehr. Du kannst mehr über unsere unterstützten APIs in [der FAQ](https://docs.sillytavern.app/usage/api-connections/) lesen.
50
51### Brauche ich einen leistungsstarken PC, um SillyTavern auszuführen?
52
53Die Hardwareanforderungen sind minimal: Es läuft auf allem, was NodeJS 18 oder höher ausführen kann. Wenn du LLM-Inferenz auf deinem lokalen Rechner durchführen möchtest, empfehlen wir eine NVIDIA-Grafikkarte der 3000er-Serie mit mindestens 6 GB VRAM. Überprüfe die Dokumentation deines Backends für weitere Einzelheiten.
54
55### Vorgeschlagene Backends (keine Partnerschaft oder Werbebeziehung)
56
57* [AI Horde](https://aihorde.net/) - verwende Modelle, die von Freiwilligen gehostet werden. Erfordert keine weitere Einrichtung
58* [KoboldCpp](https://github.com/LostRuins/koboldcpp) - ein Favorit der Community, um GGUF-Modelle lokal auszuführen
59* [tabbyAPI](https://github.com/theroyallab/tabbyAPI) - eine beliebte, portable, speicherplatzoptimierte und lokal gehostete exl2 Inferenz-API
60* [OpenRouter](https://openrouter.ai) - eine einzige API für viele Cloud-Anbieter (OpenAI, Claude, Meta Llama usw.) sowie beliebte Community-Modelle.
6140
62## Fragen oder Vorschläge?41## Fragen oder Vorschläge?
6342
@@ -83,287 +62,18 @@ Oder nimm direkt Kontakt mit den Entwicklern auf:
83<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">62<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">
84<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">63<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">
8564
86## Charakterkarten65## Installation
87
88SillyTavern basiert auf dem Konzept der "Charakterkarten". Eine Charakterkarte ist eine Sammlung von Prompts, die das Verhalten des LLM festlegen und erforderlich sind, um persistente Gespräche in SillyTavern zu führen. Sie funktionieren ähnlich wie ChatGPT's GPTs oder Poe's Bots. Der Inhalt einer Charakterkarte kann alles sein: ein abstraktes Szenario, ein Assistent, der für eine bestimmte Aufgabe maßgeschneidert ist, eine berühmte Persönlichkeit oder ein fiktiver Charakter.
89
90Das Namensfeld ist der einzige erforderliche Eingabewert für die Charakterkarte. Um ein neutrales Gespräch mit dem LLM zu beginnen, erstelle eine neue Karte, die einfach "Assistent" genannt wird, und lasse die restlichen Felder leer. Für einen thematischeren Chat kannst du dem LLM verschiedene Hintergrundinformationen, Verhaltensweisen und Schreibmuster sowie ein Szenario geben, um das Gespräch zu beginnen.
91
92Um ein schnelles Gespräch zu führen, ohne eine Charakterkarte auszuwählen, oder um einfach die LLM-Verbindung zu testen, gib einfach dein Prompt in die Eingabezeile auf dem Willkommensbildschirm ein, nachdem du SillyTavern geöffnet hast. Bitte beachte, dass solche Chats vorübergehend sind und nicht gespeichert werden.
93
94Um eine allgemeine Vorstellung davon zu bekommen, wie man Charakterkarten definiert, sieh dir die mitgelieferte Charakterkarte (Seraphina) an oder lade ausgewählte von der Community erstellte Karten im Menü "Erweiterungen & Assets herunterladen" herunter.
95
96## Wichtigste Features
97
98* Erweiterte Text-Generierungs-Einstellungen mit vielen von der Community erstellten mitgelieferten Einstellungen (Presets)
99* Unterstützung für World Info: Erstelle reichhaltige Lore oder reduziere die Tokens in deiner Charakterkarte
100* Gruppenchats: Multi-Bot-Räume für Charaktere, die mit dir und/oder untereinander sprechen
101* Reichhaltige UI-Anpassungsoptionen: Themes zur Farbenwahl, Hintergrundbilder, benutzerdefiniertes CSS und mehr
102* Benutzer-Personas: Lass die KI ein wenig über dich wissen, um die Immersion zu erhöhen
103* Eingebaute RAG-Unterstützung: Füge Dokumente zu deinen Chats hinzu, auf die die KI verweisen kann
104* Umfangreiches "Chat-Befehle"-System und eigene [Scripting-Engine](https://docs.sillytavern.app/usage/st-script/)
105
106## Erweiterungen
107
108SillyTavern unterstützt Erweiterungen.
109
110* Emotionale Ausdrucksformen von Charakteren (Sprites)
111* Automatische Zusammenfassung des Chatverlaufs
112* Automatische UI- und Chat-Übersetzung
113* Bildgenerierung mit Stable Diffusion/FLUX/DALL-E
114* Text-to-Speech für KI-Antwortnachrichten (über ElevenLabs, Silero oder die TTS-Funktion des Betriebssystems)
115* Websuchfunktionen zum Hinzufügen zusätzlicher realer Kontexte zu deinen Eingabeaufforderungen
116* Viele weitere sind im Menü "Erweiterungen & Assets herunterladen" verfügbar.
117
118Tutorials zur Nutzung findest du in der [Dokumentation](https://docs.sillytavern.app/).
119
120# ⌛ Installation
121
122> \[!WARNING]
123>
124> * INSTALLIERE NICHT IN EINEM VON WINDOWS KONTROLLIERTEN ORDNER (Programme, System32 usw.).
125> * FÜHRE START.BAT NICHT MIT ADMIN-BERECHTIGUNGEN AUS.
126> * DIE INSTALLATION AUF WINDOWS 7 IST UNMÖGLICH, DA ES NODEJS 18.16 NICHT AUSFÜHREN KANN.
127
128## 🪟 Windows
129
130### Installation über Git
131
1321. Installiere [NodeJS](https://nodejs.org/en) (die neueste LTS-Version wird empfohlen).
1332. Installiere [Git für Windows](https://gitforwindows.org/).
1343. Öffne den Windows-Explorer (`Win+E`).
1354. Gehe zu oder erstelle einen Ordner, der nicht von Windows kontrolliert oder überwacht wird. (z.B.: C:\MySpecialFolder\)
1365. Öffne ein Eingabeaufforderungsfenster in diesem Ordner, indem du in die 'Adressleiste' oben klickst, `cmd` eingibst und Enter drückst.
1376. Sobald das schwarze Fenster (Eingabeaufforderung) erscheint, gib EINE der folgenden Optionen ein und drücke Enter:
138
139* für den Release-Branch: `git clone https://github.com/SillyTavern/SillyTavern -b release`
140* für den Staging-Branch: `git clone https://github.com/SillyTavern/SillyTavern -b staging`
141
1427. Sobald alles geklont ist, doppelklicke auf `Start.bat`, damit NodeJS seine Anforderungen installiert.
1438. Der Server wird dann gestartet, und SillyTavern wird in deinem Browser geöffnet.
144
145### Installation über GitHub Desktop
146
147(Dies ermöglicht die Nutzung von git **nur** in GitHub Desktop. Wenn du `git` auch in der Eingabeaufforderung verwenden möchtest, musst du auch [Git für Windows](https://gitforwindows.org/) installieren.)
148
1491. Installiere [NodeJS](https://nodejs.org/en) (die neueste LTS-Version wird empfohlen).
1502. Installiere [GitHub Desktop](https://central.github.com/deployments/desktop/desktop/latest/win32).
1513. Klicke nach der Installation von GitHub Desktop auf `Ein Repository aus dem Internet klonen....` (Hinweis: Du **musst kein** GitHub-Konto für diesen Schritt erstellen).
1524. Klicke im Menü auf den Tab URL, gib diese URL ein `https://github.com/SillyTavern/SillyTavern` und klicke auf Klonen. Du kannst den lokalen Pfad ändern, um festzulegen, wohin SillyTavern heruntergeladen werden soll.
1535. Um SillyTavern zu öffnen, durchsuche mit dem Windows-Explorer den Ordner, in den du das Repository geklont hast. Standardmäßig wird das Repository hier hin geklont: `C:\Users\[Dein Windows-Benutzername]\Documents\GitHub\SillyTavern`.
1546. Doppelklicke auf die Datei `start.bat`. (Hinweis: Der Teil `.bat` des Dateinamens könnte von deinem Betriebssystem verborgen sein. In diesem Fall sieht es aus wie eine Datei namens "`Start`". Dies ist die Datei, auf die du doppelklickst, um SillyTavern auszuführen.)
1557. Nach dem Doppelklicken sollte ein großes schwarzes Konsolenfenster erscheinen, und SillyTavern beginnt, das zu installieren, was es zum Betrieb benötigt.
1568. Nach dem Installationsprozess sollte das Konsolenfenster so aussehen, und ein SillyTavern-Tab sollte in deinem Browser geöffnet sein.
1579. Verbinde dich mit einer der [unterstützten APIs](https://docs.sillytavern.app/usage/api-connections/) und beginne zu chatten!
158
159## 🐧 Linux & 🍎 MacOS
160
161Für MacOS/Linux werden all diese Schritte in einem Terminal durchgeführt.
162
1631. Installiere git und nodeJS (die Methode zur Durchführung hängt von deinem Betriebssystem ab).
1642. Klone das Repository.
165
166* für den Release-Branch: `git clone https://github.com/SillyTavern/SillyTavern -b release`
167* für den Staging-Branch: `git clone https://github.com/SillyTavern/SillyTavern -b staging`
168
1693. `cd SillyTavern`, um in den Installationsordner zu navigieren.
1704. Führe das Skript `start.sh` mit einem dieser Befehle aus:
171
172* `./start.sh`
173* `bash start.sh`
174
175## ⚡ Installation über SillyTavern Launcher
176
177Der SillyTavern Launcher ist ein Installationsassistent, der dir bei der Einrichtung mit vielen Optionen helfen wird, einschließlich der Installation eines Backends für lokale Inferenz.
178
179### Für Windows-Nutzer
180
1811. Drücke auf deiner Tastatur **`WINDOWS + R`**, um das Ausführen-Dialogfeld zu öffnen. Führe dann den folgenden Befehl aus, um git zu installieren:
182
183```shell
184cmd /c winget install -e --id Git.Git
185```
186
1872. Drücke auf deiner Tastatur **`WINDOWS + E`**, um den Datei-Explorer zu öffnen, und navigiere dann zu dem Ordner, in dem du den Launcher installieren möchtest. Gib im gewünschten Ordner in die Adressleiste `cmd` ein und drücke Enter. Führe dann den folgenden Befehl aus:
188
189```shell
190git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher && start installer.bat
191```
192
193### Für Linux-Nutzer
194
1951. Öffne dein bevorzugtes Terminal und installiere git.
1962. Klone den SillyTavern-Launcher mit:
197
198```shell
199git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
200```
201
2023. Starte die installer.sh mit:
203
204```shell
205chmod +x install.sh && ./install.sh
206```
207
2084. Nach der Installation starte die launcher.sh mit:
209
210```shell
211chmod +x launcher.sh && ./launcher.sh
212```
213
214### Für Mac-Nutzer
215
2161. Öffne ein Terminal und installiere brew mit:
217
218```shell
219/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
220```
221
2222. Installiere git mit:
223
224```shell
225brew install git
226```
227
2283. Klone den SillyTavern-Launcher mit:
229
230```shell
231git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
232```
233
2344. Starte die installer.sh mit:
235
236```shell
237chmod +x install.sh && ./install.sh
238```
239
2405. Nach der Installation starte die launcher.sh mit:
241
242```shell
243chmod +x launcher.sh && ./launcher.sh
244```
245
246## 🐋 Installation über Docker
247
248Diese Anweisungen setzen voraus, dass du Docker installiert hast, auf deine Befehlszeile für die Installation von Containern zugreifen kannst und mit deren allgemeiner Funktionsweise vertraut bist.
249
250### Image selbst bauen
251
252Wir haben einen umfassenden Leitfaden zur Nutzung von SillyTavern in Docker [hier](http://docs.sillytavern.app/installation/docker/) der die Installationen auf Windows, macOS und Linux abdeckt! Lies ihn, wenn du das Image selbst bauen möchtest.
253
254### Verwendung der GitHub Container Registry (am einfachsten)
255
256Du benötigst zwei zwingende Verzeichniszuordnungen und eine Portzuordnung, um SillyTavern funktionsfähig zu machen. Ersetze in dem Befehl deine Auswahl an den folgenden Stellen:
257
258#### Container-Variablen
259
260##### Volumen-Zuordnungen
261
262* [config] - Das Verzeichnis, in dem die SillyTavern-Konfigurationsdateien auf deinem Host-Computer gespeichert werden
263* [data] - Das Verzeichnis, in dem die Benutzerdaten von SillyTavern (einschließlich Charaktere) auf deinem Host-Computer gespeichert werden
264* [plugins] - (optional) Das Verzeichnis, in dem die SillyTavern-Server-Plugins auf deinem Host-Computer gespeichert werden
265
266##### Port-Zuordnungen
267
268* [PublicPort] - Der Port, über den der Datenverkehr ausgegeben werden soll. Dies ist zwingend erforderlich, da du auf die Instanz von außerhalb des virtuellen Maschinencontainers zugreifst. EXPOSIERE DIES NICHT IM INTERNET, OHNE EINEN GETRENNTEN SERVICE FÜR DIE SICHERHEIT ZU IMPLEMENTIEREN.
269
270##### Zusätzliche Einstellungen
271
272* [DockerNet] - Das Docker-Netzwerk, mit dem der Container erstellt werden soll. Wenn du nicht weißt, was das ist, sieh dir die [offizielle Docker-Dokumentation](https://docs.docker.com/reference/cli/docker/network/) an.
273* [version] - Auf der rechten Seite dieser GitHub-Seite siehst du "Packages". Wähle das Paket "sillytavern" und du siehst die Imageversionen. Das Image-Tag "latest" hält dich auf dem Laufenden mit dem aktuellen Release. Du kannst auch "staging" und "release" Tags nutzen, die auf die nightly images der jeweiligen Branche verweisen, aber das könnte unangemessen sein, wenn du Erweiterungen verwendest, die möglicherweise kaputt sind und Zeit benötigen, um aktualisiert zu werden.
274
275#### Installationsbefehl
276
2771. Öffne deine Befehlszeile.
2782. Führe den folgenden Befehl aus:
279
280`docker create --name='sillytavern' --net='[DockerNet]' -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'`
281
282> Beachte, dass 8000 ein Standard-Listening-Port ist. Vergiss nicht, einen geeigneten Port zu verwenden, wenn du ihn in der Konfiguration änderst.
283
284## 📱 Installation über Termux auf Android OS
285
286> \[!HINWEIS]
287> **SillyTavern kann nativ auf Android-Geräten über Termux ausgeführt werden, aber wir bieten keine offizielle Unterstützung für diesen Anwendungsfall.**
288>
289> **Bitte beziehe dich auf diesen Leitfaden von ArroganceComplex#2659:**
290>
291> * <https://rentry.org/STAI-Termux>
292
293**Nicht unterstützte Plattform: android arm LEtime-web.** 32-Bit-Android benötigt eine externe Abhängigkeit, die nicht mit npm installiert werden kann. Verwende den folgenden Befehl, um sie zu installieren: `pkg install esbuild`. Führe dann die üblichen Installationsschritte aus.
294
295## API-Schlüsselverwaltung
296
297SillyTavern speichert deine API-Schlüssel in einer Datei `secrets.json` im Benutzerdatenverzeichnis (`/data/default-user/secrets.json` ist der Standardpfad).
298
299Standardmäßig sind API-Schlüssel nach dem Speichern und Aktualisieren der Seite nicht mehr über die Benutzeroberfläche sichtbar.
300
301Um die Ansicht deiner Schlüssel zu aktivieren:
302
3031. Setze den Wert von `allowKeysExposure` auf `true` in der Datei `config.yaml`.
3042. Starte den SillyTavern-Server neu.
3053. Klicke auf den Link 'Verborgene API-Schlüssel anzeigen' in der unteren rechten Ecke des API-Verbindungsfeldes.
306
307## Befehlszeilenargumente
308
309Sie können Befehlszeilenargumente an den Start des SillyTavern-Servers übergeben, um einige Einstellungen in `config.yaml` zu überschreiben.
310
311### Beispiele
312
313```shell
314node server.js --port 8000 --listen false
315# oder
316npm run start -- --port 8000 --listen false
317# oder (nur Windows)
318Start.bat --port 8000 --listen false
319```
320
321### Unterstützte Argumente
322
323| Option | Beschreibung | Typ |
324|----------------------------------|--------------------------------------------------------------------------------------------------|----------|
325| `--version` | Versionsnummer anzeigen | boolean |
326| `--enableIPv6` | Aktiviert IPv6. | boolean |
327| `--enableIPv4` | Aktiviert IPv4. | boolean |
328| `--port` | Legt den Port fest, unter dem SillyTavern ausgeführt wird. | number |
329| `--dnsPreferIPv6` | Bevorzugt IPv6 für DNS. Wenn nicht angegeben. | boolean |
330| `--browserLaunchEnabled` | Startet SillyTavern automatisch im Browser. | boolean |
331| `--browserLaunchHostname` | Der Autorun-Hostname, am besten auf „auto" belassen. | string |
332| `--browserLaunchPort` | Überschreibt den Port für Autorun. | string |
333| `--browserLaunchAvoidLocalhost` | Vermeidet die Verwendung von „localhost" für Autorun im Auto-Modus. | boolean |
334| `--listen` | SillyTavern lauscht auf allen Netzwerkschnittstellen. | boolean |
335| `--corsProxy` | Aktiviert CORS-Proxy. Wenn nicht angegeben. | boolean |
336| `--disableCsrf` | Deaktiviert CSRF-Schutz | boolean |
337| `--ssl` | Aktiviert SSL | boolean |
338| `--certPath` | Pfad zu Ihrer Zertifikatsdatei. | string |
339| `--keyPath` | Pfad zu Ihrer privaten Schlüsseldatei. | string |
340| `--whitelist` | Aktiviert den Whitelist-Modus | boolean |
341| `--dataRoot` | Stammverzeichnis für Datenspeicherung | string |
342| `--basicAuthMode` | Aktiviert die grundlegende Authentifizierung | boolean |
343| `--requestProxyEnabled` | Aktiviert die Verwendung eines Proxys für ausgehende Anfragen | boolean |
344| `--requestProxyUrl` | Proxy-URL anfordern (HTTP- oder SOCKS-Protokolle) | string |
345| `--requestProxyBypass` | Proxy-Bypass-Liste anfordern (durch Leerzeichen getrennte Liste von Hosts) | Array |
346
347## Remoteverbindungen
348
349Dies ist in den meisten Fällen für Personen gedacht, die SillyTavern auf ihren Mobiltelefonen verwenden möchten, während ihr PC den ST-Server im selben WLAN-Netzwerk betreibt. Es kann jedoch auch verwendet werden, um Remoteverbindungen von überall her zu ermöglichen.
350
351Lies die ausführliche Anleitung zum Einrichten von Remoteverbindungen in den [Docs](https://docs.sillytavern.app/usage/remoteconnections/).
352
353Möglicherweise möchtest du SillyTavern-Benutzerprofile auch mit (optionalem) Kennwortschutz konfigurieren: [Benutzer](https://docs.sillytavern.app/installation/st-1.12.0-migration-guide/#users).
35466
355## Leistungsprobleme?67Für detaillierte Installationsanweisungen besuche bitte unsere Dokumentation:
35668
3571. Deaktiviere den Unschärfeeffekt und aktiviere "Verringerte Bewegung" im Bedienfeld "Benutzereinstellungen" (UI-Design schaltet Kategorie um).69* **[Windows Installationsanleitung](https://docs.sillytavern.app/installation/windows/)**
3582. Wenn du Response Streaming verwendest, stelle die Streaming-FPS auf einen niedrigeren Wert ein (10-15 FPS werden empfohlen).70* **[MacOS/Linux Installationsanleitung](https://docs.sillytavern.app/installation/linuxmacos/)**
3593. Stelle sicher, dass der Browser die GPU-Beschleunigung zum Rendern verwenden kann.71* **[Android (Termux) Installationsanleitung](https://docs.sillytavern.app/installation/android-(termux)/)**
72* **[Docker Installationsanleitung](https://docs.sillytavern.app/installation/docker/)**
36073
361## Lizenz und Danksagungen74## Lizenz und Danksagungen
36275
363**Dieses Programm wird in der Hoffnung verbreitet, dass es nützlich ist, 76**Dieses Programm wird in der Hoffnung verbreitet, dass es nützlich ist, aber OHNE JEGLICHE GARANTIE; nicht einmal die stillschweigende Garantie der MARKTFÄHIGKEIT oder EIGNUNG FÜR EINEN BESTIMMTEN ZWECK. Siehe die GNU Affero General Public License für weitere Details.**
364aber OHNE JEGLICHE GARANTIE; nicht einmal die stillschweigende Garantie der
365MARKTFÄHIGKEIT oder EIGNUNG FÜR EINEN BESTIMMTEN ZWECK. Siehe die
366GNU Affero General Public License für weitere Details.**
36777
368* [TavernAI](https://github.com/TavernAI/TavernAI) 1.2.8 von Humi: MIT-Lizenz78* [TavernAI](https://github.com/TavernAI/TavernAI) 1.2.8 von Humi: MIT-Lizenz
369* Teile von CncAnons TavernAITurbo-Mod werden mit Genehmigung verwendet79* Teile von CncAnons TavernAITurbo-Mod werden mit Genehmigung verwendet
@@ -372,6 +82,7 @@ GNU Affero General Public License für weitere Details.**
372* Symboldesign von Font Awesome <https://fontawesome.com> (Symbole: CC BY 4.0, Schriftarten: SIL OFL 1.1, Code: MIT-Lizenz)82* Symboldesign von Font Awesome <https://fontawesome.com> (Symbole: CC BY 4.0, Schriftarten: SIL OFL 1.1, Code: MIT-Lizenz)
373* Standardinhalt von @OtisAlejandro (Seraphina-Charakter und Lorebook) und @kallmeflocc (10.000 Discord-Benutzer-Feierhintergrund)83* Standardinhalt von @OtisAlejandro (Seraphina-Charakter und Lorebook) und @kallmeflocc (10.000 Discord-Benutzer-Feierhintergrund)
374* Docker-Anleitung von [@mrguymiah](https://github.com/mrguymiah) und [@Bronya-Rand](https://github.com/Bronya-Rand)84* Docker-Anleitung von [@mrguymiah](https://github.com/mrguymiah) und [@Bronya-Rand](https://github.com/Bronya-Rand)
85* kokoro-js library by [@hexgrad](https://github.com/hexgrad) (Apache-2.0 License)
37586
376## Top Contributors87## Top Contributors
37788
.github/readme-ja_jp.md+48 -284
@@ -1,6 +1,8 @@
1> [!IMPORTANT] 1> [!IMPORTANT]
2> ここに掲載されている情報は、古かったり不完全であったりする可能性があります。最新の情報は英語版をご利用ください。2> ここに掲載されている情報は、古かったり不完全であったりする可能性があります。最新の情報は英語版をご利用ください。
33
4<a name="readme-top"></a>
5
4![][cover]6![][cover]
57
6<div align="center">8<div align="center">
@@ -8,7 +10,7 @@
8[English](readme.md) | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | [繁體中文](readme-zh_tw.md) | 日本語 | [Русский](readme-ru_ru.md) | [한국어](readme-ko_kr.md)10[English](readme.md) | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | [繁體中文](readme-zh_tw.md) | 日本語 | [Русский](readme-ru_ru.md) | [한국어](readme-ko_kr.md)
911
10[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)12[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
11[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)13[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/forks)
12[![GitHub Issues](https://img.shields.io/github/issues/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/issues)14[![GitHub Issues](https://img.shields.io/github/issues/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/issues)
13[![GitHub Pull Requests](https://img.shields.io/github/issues-pr/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/pulls)15[![GitHub Pull Requests](https://img.shields.io/github/issues-pr/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/pulls)
1416
@@ -16,315 +18,77 @@
1618
17---19---
1820
19モバイルデバイスにも適したレイアウト・マルチAPI(KoboldAI/CPP、Horde、NovelAI、Ooba、OpenAI、OpenRouter、Claude、Scale)、VN ライクな Waifu モード、Stable Diffusion、TTS、WorldInfo(伝承本)、カスタマイズ可能なUI、自動翻訳、大量のプロンプトオプション+サードパーティの拡張機能をインストールする機能21SillyTavernは、多くのLLM API(KoboldAI/CPP、Horde、NovelAI、Ooba、Tabby、OpenAI、OpenRouter、Claude、Mistralなど)に対応した統一インターフェース、モバイルフレンドリーなレイアウト、ビジュアルノベルモード、Automatic1111 & ComfyUI API画像生成連携、TTS、WorldInfo(伝承本)、カスタマイズ可能なUI、自動翻訳、必要以上に豊富なプロンプトオプション、そしてサードパーティ製拡張機能による無限の成長可能性を提供します。
20
21[TavernAI](https://github.com/TavernAI/TavernAI) v.1.2.8 のフォークに基づいています。
22
23## 重要事項
24
251. 私たちは[ドキュメントウェブサイト](https://docs.sillytavern.app/) を作成し、ほとんどの質問にお答えしています。
26
272. アップデートしたら使っていた拡張機能を見失いましたか?リリースバージョン 1.10.6 以降、これまで内蔵されていた拡張機能のほとんどがダウンロード可能なアドオンに変更されました。ダウンロードは、拡張機能パネル(トップバーのスタックドブロックアイコン)にある内蔵の "Download Extensions and Assets" メニューから行えます。
28
29### Cohee、RossAscends、SillyTavern コミュニティがお届けします
30
31### SillyTavern または TavernAI とは何ですか?
32
33SillyTavern は、あなたのコンピュータ(および Android スマホ)にインストールできるユーザーインターフェイスで、テキスト生成 AI と対話したり、あなたやコミュニティが作成したキャラクターとチャットやロールプレイをすることができます。
34
35SillyTavern は TavernAI 1.2.8 のフォークで、より活発な開発が行われており、多くの主要な機能が追加されています。現時点では、これらは完全に独立したプログラムと考えることができます。
36
37### ブランチ
38
39SillyTavern は、すべてのユーザーにスムーズな体験を保証するために、2 つのブランチシステムを使用して開発されています。
40
41* release -🌟 **ほとんどのユーザーにお勧め。** これは最も安定した推奨ブランチで、メジャーリリースがプッシュされた時のみ更新されます。大半のユーザーに適しています。
42* staging - ⚠️ **カジュアルな使用にはお勧めしない。** このブランチには最新の機能がありますが、いつ壊れるかわからないので注意してください。パワーユーザーとマニア向けです。
43
44git CLI の使い方に慣れていなかったり、ブランチが何なのかわからなかったりしても、心配はいりません!releaseブランチが良い選択肢となります。
45
46### Tavern 以外に何が必要ですか?
47
48Tavern は単なるユーザーインターフェイスなので、それだけでは何もできません。ロールプレイキャラクターとして機能する AI システムのバックエンドにアクセスする必要があります。様々なバックエンドがサポートされています。リストはこちらです: OpenAPI API (GPT)、KoboldAI (ローカルまたは Google Colab 上で動作)、その他。詳しくは [FAQ](https://docs.sillytavern.app/usage/faq/) をご覧ください。
49
50### Tavern を実行するには、ハイスペックなPCが必要ですか?
51
52Tavern は単なるフロントエンドのUIであり、必要なハードウェアはごくわずかです。パワフルである必要があるのは、AI システムのバックエンドです。
53
54## モバイルサポート
55
56> **注**
57
58> **このフォークは Termux を使って Android スマホで実行できます。ArroganceComplex#2659 のガイドを参照してください:**
59
60<https://rentry.org/STAI-Termux>
61
62## ご質問やご提案
63
64### コミュニティ Discord サーバーを開設しました
65
66サポートを受けたり、作成したキャラを共有したり、コミュニティの作ったキャラと遊びたいですか?:
6722
68### [参加](https://discord.gg/RZdyAEUPvj)23私たちは[ドキュメントウェブサイト](https://docs.sillytavern.app/)を用意しており、ほとんどの質問に答え、入門の手助けをします。
6924
70***25## SillyTavernとは?
7126
72開発者と直接連絡:27SillyTavern(略してST)は、テキスト生成LLM、画像生成エンジン、TTS音声モデルと対話するための、ローカルにインストールされるユーザーインターフェースです。
7328
74* Discord: cohee または rossascends292023年2月にTavernAI 1.2.8のフォークとして始まり、SillyTavernは現在200人以上の貢献者と2年間の独立した開発を経て、知識豊富なAI愛好家のための主要なソフトウェアとして機能し続けています。
75* Reddit: /u/RossAscends または /u/sillylossy
76* [GitHub issue を投稿](https://github.com/SillyTavern/SillyTavern/issues)
7730
78## このバージョンには以下のコードが含まれます31## 私たちのビジョン
7932
80* 大幅に修正された TavernAI 1.2.8 (コードの 50% 以上が書き換えまたは最適化されています)331. 私たちは、ユーザーにできるだけ多くの実用性とLLMプロンプトの制御権限を与えることを目指しています。急な学習曲線も楽しみの一部です!
81* スワイプ342. 私たちはオンラインサービスやホストされたサービスを提供せず、プログラム的にユーザーデータを追跡することもありません。
82* グループチャット: キャラクター同士が会話できるマルチボットルーム353. SillyTavernは、熱心なLLM愛好家のコミュニティによってもたらされた情熱的なプロジェクトであり、常に無料でオープンソースです。
83* チャットチェックポイント / ブランチ
84* 高度なKoboldAI / TextGen生成設定と、コミュニティが作成した多くのプリセット
85* ワールド情報サポート: 豊富な伝承を作成したり、キャラクターカードにトークンを保存したりできます
86* [OpenRouter](https://openrouter.ai) 各種 API(Claude、GPT-4/3.5 など)の接続
87* [Oobabooga's TextGen WebUI](https://github.com/oobabooga/text-generation-webui) API 接続
88* [AI Horde](https://aihorde.net/) 接続
89* プロンプト生成フォーマットの調整
9036
91## 拡張機能37## SillyTavernを実行するには強力なPCが必要ですか?
9238
93SillyTavern は拡張できるようになっており。39ハードウェア要件は最小限です。NodeJS 18以上を実行できるものであれば何でも動作します。ローカルマシンでLLM推論を行う場合は、少なくとも6GBのVRAMを搭載した3000シリーズのNVIDIAグラフィックスカードを推奨しますが、実際の要件は使用するモデルやバックエンドによって異なる場合があります。
9440
95* キャラクターの感情表現(スプライト)41## 質問や提案はありますか?
96* チャット履歴の自動サマリー
97* チャットに画像を送り、AI が内容を解釈する
98* Stable Diffusion 画像生成 (5 つのチャット関連プリセットと 'free mode')
99* AI 応答メッセージの音声合成(ElevenLabs、Silero、または OS のシステム TTS 経由)
10042
101含まれている拡張機能の完全なリストとその使い方のチュートリアルは [Docs](https://docs.sillytavern.app/) にあります。43### Discordサーバー
10244
103## RossAscends による UI/CSS/クオリティオブライフの調整45| [![][discord-shield-badge]][discord-link] | [私たちのDiscordコミュニティに参加してください!](https://discord.gg/sillytavern) サポートを受けたり、お気に入りのキャラクターやプロンプトを共有したりできます。 |
46| :---------------------------------------- | :----------------------------------------------------------------------------------------------------------------- |
10447
105* iOS 用に最適化されたモバイル UI で、ホーム画面へのショートカット保存とフルスクリーンモードでの起動をサポート。48または、開発者に直接連絡してください:
106* ホットキー
107 * Up = チャットの最後のメッセージを編集する
108 * Ctrl+Up = チャットで最後のユーザーメッセージを編集する
109 * Left = 左スワイプ
110 * Right = 右スワイプ (注: チャットバーに何か入力されている場合、スワイプホットキーが無効になります)
111 * Ctrl+Left = ローカルに保存された変数を見る(ブラウザのコンソールウィンドウにて)
112 * Enter (チャットバー選択時) = AI にメッセージを送る
113 * Ctrl+Enter = 最後の AIのレスポンスを再生成する
11449
115* ユーザー名の変更と文字の削除でページが更新されなくなりました。50* Discord: cohee, rossascends, wolfsblvt
51* Reddit: [/u/RossAscends](https://www.reddit.com/user/RossAscends/), [/u/sillylossy](https://www.reddit.com/user/sillylossy/), [u/Wolfsblvt](https://www.reddit.com/user/Wolfsblvt/)
52* [GitHub issueを投稿](https://github.com/SillyTavern/SillyTavern/issues)
11653
117* ページロード時に API に自動的に接続するかどうかを切り替えます。54### このプロジェクトが気に入りました!どうすれば貢献できますか?
118* ページの読み込み時に、最近見た文字を自動的に読み込むかどうかを切り替えます。
119* より良いトークンカウンター - 保存されていないキャラクターに対して機能し、永続的なトークンと一時的なトークンの両方を表示する。
12055
121* より良い過去のチャット561. プルリクエストを送ってください。貢献する方法については、[CONTRIBUTING.md](../CONTRIBUTING.md)をご覧ください。
122 * 新しいチャットのファイル名は、"(文字) - (作成日)" という読みやすい形式で保存されます572. 提供されたテンプレートを使用して、機能の提案や問題の報告を送ってください。
123 * チャットのプレビューが 40 文字から 300 文字に増加。583. 重複した問題を避けるために、まずこのreadmeファイル全体とドキュメントウェブサイトを確認してください。
124 * 文字リストの並べ替えに複数のオプション(名前順、作成日順、チャットサイズ順)があります。
12559
126* デフォルトでは、左右の設定パネルはクリックすると閉じます。60## スクリーンショット
127* ナビパネルのロックをクリックすると、パネルが開いたままになり、この設定はセッションをまたいで記憶されます。
128* ナビパネルの開閉状態もセッションをまたいで保存されます。
12961
130* カスタマイズ可能なチャット UI:62<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">
131 * 新しいメッセージが届いたときにサウンドを再生する63<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">
132 * 丸型、長方形のアバタースタイルの切り替え
133 * デスクトップのチャットウィンドウを広くする
134 * オプションの半透明ガラス風パネル
135 * 'メインテキスト'、'引用テキスト'、'斜体テキスト'のページカラーをカスタマイズ可能。
136 * カスタマイズ可能な UI 背景色とぼかし量
13764
138## インストール65## インストール
13966
140*注: このソフトウェアはローカルにインストールすることを目的としており、colab や他のクラウドノートブックサービス上では十分にテストされていません。*67詳細なインストール手順については、私たちのドキュメントをご覧ください:
141
142> **警告**
143
144> WINDOWS が管理しているフォルダ(Program Files、System32 など)にはインストールしないでください
145
146> START.BAT を管理者権限で実行しないでください
147
148### Windows
149
150Git 経由でのインストール(更新を容易にするため推奨)
151
152写真付きのわかりやすいガイドはこちらです:
153<https://docs.sillytavern.app/installation/windows/>
154
155 1. [NodeJS](https://nodejs.org/en) をインストール(最新の LTS 版を推奨)
156 2. [GitHub Desktop](https://central.github.com/deployments/desktop/desktop/latest/win32) をインストールする
157 3. Windows エクスプローラーを開く (`Win+E`)
158 4. Windows によって制御または監視されていないフォルダを参照または作成する。(例: C:\MySpecialFolder\)
159 5. 上部のアドレスバーをクリックし、`cmd` と入力して Enter キーを押し、そのフォルダーの中にコマンドプロンプトを開きます。
160 6. 黒いボックス(コマンドプロンプト)がポップアップしたら、そこに以下のいずれかを入力し、Enter を押します:
161
162* Release ブランチの場合: `git clone https://github.com/SillyTavern/SillyTavern -b release`
163* Staging ブランチの場合: `git clone https://github.com/SillyTavern/SillyTavern -b staging`
164
165 7. すべてをクローンしたら、`Start.bat` をダブルクリックして、NodeJS に要件をインストールさせる。
166 8. サーバーが起動し、SillyTavern がブラウザにポップアップ表示されます。
167
168ZIP ダウンロードによるインストール(推奨しない)
169
170 1. [NodeJS](https://nodejs.org/en) をインストールする(最新の LTS 版を推奨)
171 2. GitHub のリポジトリから zip をダウンロードする。(`ソースコード(zip)` は [Releases](https://github.com/SillyTavern/SillyTavern/releases/latest) から入手)
172 3. お好きなフォルダに解凍してください
173 4. `Start.bat` をダブルクリックまたはコマンドラインで実行する。
174 5. サーバーが準備できれば、ブラウザのタブを開きます。
175
176### Linux
177
178 1. `node -v` を実行して、Node.js v18 以上(最新の [LTS バージョン](https://nodejs.org/en/download/) を推奨)がインストールされていることを確認してください。
179または、[Node Version Manager](https://github.com/nvm-sh/nvm#installing-and-updating) スクリプトを使用して、迅速かつ簡単に Node のインストールを管理します。
180 2. `start.sh` スクリプトを実行する。
181 3. お楽しみください。
182
183## API キー管理
184
185SillyTavern は API キーをサーバーディレクトリの `secrets.json` ファイルに保存します。
186
187デフォルトでは、入力後にページをリロードしても、フロントエンドには表示されません。
188
189API ブロックのボタンをクリックして、キーを閲覧できるようにする:
190
1911. ファイル `config.yaml` で `allowKeysExposure` の値を `true` に設定する。
1922. SillyTavern サーバを再起動します。
193
194## リモート接続
195
196SillyTavern をスマホで使用しながら、同じ Wifi ネットワーク上で ST サーバーを PC で実行したい場合に使用します。
19768
198しかし、これはどこからでもリモート接続を許可するために使用することができます。69* **[Windowsインストールガイド](https://docs.sillytavern.app/installation/windows/)**
70* **[MacOS/Linuxインストールガイド](https://docs.sillytavern.app/installation/linuxmacos/)**
71* **[Android (Termux)インストールガイド](https://docs.sillytavern.app/installation/android-(termux)/)**
72* **[Dockerインストールガイド](https://docs.sillytavern.app/installation/docker/)**
19973
200**重要: SillyTavern はシングルユーザーのプログラムなので、ログインすれば誰でもすべてのキャラクターとチャットを見ることができ、UI 内で設定を変更することができます。**74## ライセンスとクレジット
201
202### 1. ホワイトリスト IP の管理
203
204* SillyTavern のベースインストールフォルダ内に `whitelist.txt` という新しいテキストファイルを作成します。
205* テキストエディタでこのファイルを開き、接続を許可したい IP のリストを追加します。
206
207*個々の IP とワイルドカード IP 範囲の両方が受け入れられる。例:*
208
209```txt
210192.168.0.1
211192.168.0.20
212```
213
214または
215
216```txt
217192.168.0.*
218```
219
220(上記のワイルドカード IP 範囲は、ローカルネットワーク上のどのデバイスでも)
221
222CIDR マスクも受け付ける(例:10.0.0.0/24)。
223
224* `whitelist.txt` ファイルを保存する。
225* TAI サーバーを再起動する。
226
227これでファイルに指定された IP を持つデバイスが接続できるようになります。
228
229*注: `config.yaml` にも `whitelist` 配列があり、同じように使うことができるが、`whitelist.txt` が存在する場合、この配列は無視される。*
230
231### 2. ST ホストマシンの IP の取得
232
233ホワイトリストの設定後、ST ホストデバイスの IP が必要になります。
234
235ST ホストデバイスが同じ無線 LAN ネットワーク上にある場合、ST ホストの内部無線 LAN IP を使用します:
236
237* Windows の場合: ウィンドウズボタン > 検索バーに `cmd.exe` と入力 > コンソールに `ipconfig` と入力して Enter > `IPv4` のリストを探す。
238
239同じネットワーク上にいない状態で、ホストしているSTに接続したい場合は、STホスト機器のパブリックIPが必要です。
240
241* ST ホストデバイスを使用中に、[このページ](https://whatismyipaddress.com/)にアクセスし、`IPv4` を探してください。これはリモートデバイスからの接続に使用するものです。
242
243### 3. リモートデバイスを ST ホストマシンに接続します。
244
245最終的に使用する IP が何であれ、その IP アドレスとポート番号をリモートデバイスのウェブブラウザに入力します。
246
247同じ無線 LAN ネットワーク上の ST ホストの典型的なアドレスは以下のようになります:
248
249`http://192.168.0.5:8000`
250
251http:// を使用し、https:// は使用しないでください
252
253### ST をすべての IP に開放する
254
255これはお勧めしませんが、`config.yaml` を開き、`whitelistMode` を `false` に変更してください。
256
257SillyTavern のベースインストールフォルダにある `whitelist.txt` が存在する場合は削除(または名前の変更)する必要があります。
258
259これは通常安全ではないので、これを行う際にはユーザー名とパスワードを設定する必要があります。
260
261ユーザー名とパスワードは `config.yaml` で設定します。
262
263ST サーバを再起動すると、ユーザ名とパスワードさえ知っていれば、IP に関係なくどのデバイスでも ST サーバに接続できるようになる。
264
265### まだ接続できませんか?
266
267* `config.yaml` で見つかったポートに対して、インバウンド/アウトバウンドのファイアウォールルールを作成します。これをルーターのポートフォワーディングと間違えないでください。そうしないと、誰かがあなたのチャットログを見つける可能性があり、それはマジで止めましょう。
268* 設定 > ネットワークとインターネット > イーサネットで、プライベートネットワークのプロファイルタイプを有効にします。そうしないと、前述のファイアウォールルールを使っても接続できません。
269
270## パフォーマンスに問題がありますか?
271
272ユーザー設定パネルでブラー効果なし(高速 UI)モードを有効にしてみてください。
273
274## このプロジェクトが気に入りました!どうすれば貢献できますか?
275
276### やるべきこと
277
2781. プルリクエストを送る
2792. 確立されたテンプレートを使って機能提案と課題レポートを送る
2803. 何か質問する前に、readme ファイルや組み込みのドキュメントを読んでください
281
282### やるべきではないこと
283
2841. 金銭の寄付を申し出る
2852. 何の脈絡もなくバグ報告を送る
2863. すでに何度も回答されている質問をする
287
288## 古い背景画像はどこにありますか?
289
290100% オリジナルコンテンツのみのポリシーに移行しているため、古い背景画像はこのリポジトリから削除されました。
291
292アーカイブはこちら:
293
294<https://files.catbox.moe/1xevnc.zip>
29575
296## スクリーンショット76**このプログラムは有用であることを期待して配布されていますが、いかなる保証もありません。商品性または特定目的への適合性の黙示の保証さえもありません。詳細はGNU Affero General Public Licenseをご覧ください。**
29777
298<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">78* [TavernAI](https://github.com/TavernAI/TavernAI) 1.2.8 by Humi: MITライセンス
299<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">79* CncAnonのTavernAITurbo modの一部を許可を得て使用
80* PepperTacoの作品に触発されたビジュアルノベルモード (<https://github.com/peppertaco/Tavern/>)
81* GoogleによるNoto Sansフォント (OFLライセンス)
82* Font Awesomeによるアイコンテーマ <https://fontawesome.com> (アイコン: CC BY 4.0, フォント: SIL OFL 1.1, コード: MITライセンス)
83* @OtisAlejandroによるデフォルトコンテンツ(Seraphinaキャラクターと伝承本)と@kallmefloccによる10K Discordユーザー記念背景
84* [@mrguymiah](https://github.com/mrguymiah)と[@Bronya-Rand](https://github.com/Bronya-Rand)によるDockerガイド
85* [@hexgrad](https://github.com/hexgrad)によるkokoro-jsライブラリ (Apache-2.0ライセンス)
30086
301## ライセンスとクレジット87## トップコントリビューター
30288
303**このプログラムは有用であることを願って配布されていますが、いかなる保証もありません;89[![Contributors](https://contrib.rocks/image?repo=SillyTavern/SillyTavern)](https://github.com/SillyTavern/SillyTavern/graphs/contributors)
304また、商品性または特定目的への適合性についての黙示の保証もありません。
305詳細は GNU Affero General Public License をご覧ください。**
306
307* Humi によるTAI Base: 不明ライセンス
308* Cohee の修正と派生コード: AGPL v3
309* RossAscends の追加: AGPL v3
310* CncAnon の TavernAITurbo 改造の一部: 不明ライセンス
311* kingbri のさまざまなコミットと提案 (<https://github.com/bdashore3>)
312* city_unit の拡張機能と様々な QoL 機能 (<https://github.com/city-unit>)
313* StefanDanielSchwarz のさまざまなコミットとバグ報告 (<https://github.com/StefanDanielSchwarz>)
314* PepperTaco の作品にインスパイアされた Waifu モード (<https:/fugithub.com/peppertaco/Tavern/>)
315* ピグマリオン大学の皆さん、素晴らしいテスターとしてクールな機能を提案してくれてありがとう!
316* TextGen のプリセットをコンパイルしてくれた obabooga に感謝
317* KAI Lite の KoboldAI プリセット: <https://lite.koboldai.net/>
318* Google による Noto Sans フォント(OFLライセンス)
319* Font Awesome によるアイコンテーマ <https://fontawesome.com> (アイコン: CC BY 4.0、フォント: SIL OFL 1.1、コード: MIT License)
320* ZeldaFan0225 による AI Horde クライアントライブラリ: <https://github.com/ZeldaFan0225/ai_horde>
321* AlpinDale による Linux 起動スクリプト
322* FAQ を提供してくれた paniphons に感謝
323* 10K ディスコード・ユーザー記念背景 by @kallmeflocc
324* デフォルトコンテンツ(キャラクターと伝承書)の提供: @OtisAlejandro、@RossAscends、@kallmeflocc
325* @doloroushyeonse による韓国語翻訳
326* k_euler_a による Horde のサポート <https://github.com/Teashrock>
327* [@XXpE3](https://github.com/XXpE3) による中国語翻訳、中国語 ISSUES の連絡先は @XXpE3
32890
329<!-- LINK GROUP -->91<!-- LINK GROUP -->
330[cover]: https://github.com/user-attachments/assets/01a6ae9a-16aa-45f2-8bff-32b5dc587e4492[cover]: https://github.com/user-attachments/assets/01a6ae9a-16aa-45f2-8bff-32b5dc587e44
93[discord-link]: https://discord.gg/sillytavern
94[discord-shield-badge]: https://img.shields.io/discord/1100685673633153084?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge
.github/readme-ko_kr.md+11 -311
@@ -10,7 +10,7 @@
10[English](readme.md) | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | [繁體中文](readme-zh_tw.md) | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md) | 한국어10[English](readme.md) | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | [繁體中文](readme-zh_tw.md) | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md) | 한국어
1111
12[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)12[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
13[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)13[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/forks)
14[![GitHub Issues](https://img.shields.io/github/issues/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/issues)14[![GitHub Issues](https://img.shields.io/github/issues/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/issues)
15[![GitHub Pull Requests](https://img.shields.io/github/issues-pr/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/pulls)15[![GitHub Pull Requests](https://img.shields.io/github/issues-pr/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/pulls)
1616
@@ -26,8 +26,7 @@ SillyTavern은 많은 LLM API(KoboldAI/CPP, Horde, NovelAI, Ooba, Tabby, OpenAI,
2626
27SillyTavern(짧게는 ST)은 텍스트 생성 LLM, 이미지 생성 엔진, TTS 음성 모델 등과 상호작할 수 있는 로컬 설치형 UI 입니다.27SillyTavern(짧게는 ST)은 텍스트 생성 LLM, 이미지 생성 엔진, TTS 음성 모델 등과 상호작할 수 있는 로컬 설치형 UI 입니다.
2828
292023년 2월, TavernAI 1.2.8의 포크로 시작한 SillyTavern은 현재 100명이 넘는 기여자를 보유하고 있으며, 2년간의 독자적인 개발을 거쳐 숙련된 AI 애호가들을 위한 선도적인 소프트웨어로 자리매김하고 있습니다.292023년 2월, TavernAI 1.2.8의 포크로 시작한 SillyTavern은 현재 200명이 넘는 기여자를 보유하고 있으며, 2년간의 독자적인 개발을 거쳐 숙련된 AI 애호가들을 위한 선도적인 소프트웨어로 자리매김하고 있습니다.
30
3130
32## 우리의 비전31## 우리의 비전
3332
@@ -35,30 +34,9 @@ SillyTavern(짧게는 ST)은 텍스트 생성 LLM, 이미지 생성 엔진, TTS
352. 저희는 어떠한 온라인 및 호스팅 서브시도 제공하지 않으며, 프로그래밍으로 사용자의 데이터를 추적하지 않습니다.342. 저희는 어떠한 온라인 및 호스팅 서브시도 제공하지 않으며, 프로그래밍으로 사용자의 데이터를 추적하지 않습니다.
363. SillyTavern은 헌신적인 LLM 커뮤니티가 여러분에게 제공하는 열정적인 프로젝트이며, 언제나 무료이며 오픈소스로 제공될 것입니다.353. SillyTavern은 헌신적인 LLM 커뮤니티가 여러분에게 제공하는 열정적인 프로젝트이며, 언제나 무료이며 오픈소스로 제공될 것입니다.
3736
38## 브랜치37## SillyTavern을 위해서 좋은 성능의 PC가 필요한가요?
39
40SillyTavern은 모든 사용자가 원활한 경험을 할 수 있도록 두 개의 브랜치를 활용하여 개발되고 있습니다.
41
42
43* `release` -🌟 **대부분의 사용자에게 추천됨.** 가장 안정적이고 권장되는 브랜치이며, 주요 릴리스가 배포될 때만 업데이트됩니다. 대부분의 사용자에게 적합합니다. 일반적으로 한달에 한번 업데이트됩니다.
44* `staging` - ⚠️ **일반적인 사용에 추천되지 않음.** 최신 기능을 가지고 있지만, 언제든지 문제가 발생할 수 있습니다. 고급 사용자 및 숙련자 전용입니다. 하루에 여러번 업데이트됩니다.
45
46만약 git CLI 사용에 익숙하지 않거나 브랜치가 무엇인지 모르겠다면 release 브랜치가 더 나은 선택입니다.
47
48## SillyTavern 외에 무엇이 필요한가요?
49
50SillyTavern은 인터페이스 역할만 하기 때문에, 실제로 채팅하려면 LLM 백엔드에 대한 액세스 권한이 필요합니다. 즉시 사용 가능한 채팅을 위해 AI Horde를 사용할 수 있습니다. 그 외에도 OpenAI 호환 API, KoboldAI, Tabby 등 많은 로컬 및 클라우드 기반 LLM 백엔드를 지원합니다. 지원되는 API에 대한 자세한 내용은 [FAQ](https://docs.sillytavern.app/usage/api-connections/)에서 확인할 수 있습니다.
5138
52### SillyTavern을 위해서 좋은 성능의 PC가 필요한가요?39하드웨어 요구 사항은 거의 없습니다: NodeJS 18 이상을 실행할 수 있는 모든 환경에서 작동합니다. 다만 로컬 LLM 모델을 사용할 경우, 최소 6GB VRAM 이상의 3000번대 NVIDIA 그래픽 카드를 권장하지만, 실제 요구 사항은 사용하는 모델과 백엔드에 따라 달라질 수 있습니다.
53
54하드웨어 요구 사항은 거의 없습니다: NodeJS 18 이상을 실행할 수 있는 모든 환경에서 작동합니다. 다만 로컬 LLM 모델을 사용할 경우, 최소 6GB VRAM 이상의 3000번대 NVIDIA 그래픽 카드를 권장합니다. 자세한 내용은 백엔드 문서를 참고하세요.
55
56### 추천되는 백엔드 (제휴 없음)
57
58* [AI Horde](https://aihorde.net/) - 자원 봉사자들이 호스팅하는 모델을 사용합니다. 추가 설정이 필요하지 않습니다.
59* [KoboldCpp](https://github.com/LostRuins/koboldcpp) - 로컬에서 GGUF 모델을 실행하기 위한 커뮤니티에서 선호하는 옵션입니다.
60* [tabbyAPI](https://github.com/theroyallab/tabbyAPI) - 인기 있는 경량 로컬 exl2 추론 API입니다.
61* [OpenRouter](https://openrouter.ai) - OpenAI, Claude, Meta Llama 등 다양한 클라우드 제공업체와 인기 있는 커뮤니티 모델을 위한 단일 API입니다.
6240
63## 질문이나 제안이 있으신가요?41## 질문이나 제안이 있으신가요?
6442
@@ -84,293 +62,14 @@ SillyTavern은 인터페이스 역할만 하기 때문에, 실제로 채팅하
84<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">62<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">
85<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">63<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">
8664
65## 설치
8766
88## 캐릭터 카드67자세한 설치 방법은 저희의 문서를 확인하세요:
89
90SillyTavern은 "캐릭터 카드"라는 개념을 중심으로 구축되었습니다. 캐릭터 카드는 LLM의 동작을 설정하는 프롬프트 모음이며, SillyTavern에서 지속적인 대화를 하려면 필수적입니다. 이는 ChatGPT의 GPT 또는 Poe의 봇과 유사하게 작동합니다. 캐릭터 카드의 내용은 추상적인 시나리오, 특정 작업에 맞춰진 도우미, 유명 인사 또는 가상 인물 등 무엇이든 될 수 있습니다.
91
92이름 필드는 유일한 필수 캐릭터 카드 입력 항목입니다. 언어 모델과 중립적인 대화를 시작하려면 "도우미"라고 간단히 이름 지은 새 카드를 만들고 나머지 상자는 비워 두세요. 더 주제가 있는 채팅을 원한다면 언어 모델에 다양한 배경 정보, 행동 및 작문 패턴, 그리고 채팅을 바로 시작할 시나리오를 제공할 수 있습니다.
93
94캐릭터 카드를 선택하지 않고 빠른 대화를 하거나 LLM 연결을 테스트하려면 SillyTavern을 연 후 시작 화면의 입력 창에 프롬프트 입력을 입력하기만 하면 됩니다. 이러한 채팅은 임시적이며 저장되지 않습니다.
95
96캐릭터 카드를 정의하는 방법에 대한 일반적인 아이디어를 얻으려면 기본 캐릭터(Seraphina)를 보거나 "확장 프로그램 및 에셋 다운로드" 메뉴에서 선택된 커뮤니티 제작 카드를 다운로드하세요.
97
98
99## 핵심 기능
100
101* 고급 텍스트 생성 설정과 다양한 커뮤니티 제작 프리셋
102* 월드 인포 지원: 풍부한 설정을 만들거나 캐릭터 카드에 토큰 저장
103* 그룹 채팅: 캐릭터가 사용자 혹은 다른 캐릭터와 대화할 수 있는 방
104* 다양한 UI 커스텀 옵션: 테마 색, 뱌경 이미지, 커스텀 CSS 등
105* 유저 페르소나: AI에게 사용자에 대한 정보를 주어 더욱 몰입감을 높임
106* 내장 RAG 지원: AI가 참조할 수 있도록 채팅에 문서를 추가
107* 광범위한 채팅 명령어 시스템 및 자체 [스크립트](https://docs.sillytavern.app/usage/st-script/)
108
109## 확장
110
111SillyTavern은 확장(익스텐션)을 지원합니다.
112
113* 캐릭터 감정 표현 (스프라이트)
114* 채팅 기록 자동 요약
115* 자동 UI 및 채팅 번역
116* Stable Diffusion/FLUX/DALL-E 이미지 생성
117* AI 응답 메시지 텍스트 음성 변환 (ElevenLabs, Silero 또는 OS 시스템 TTS 사용)
118* 프롬프트에 추가적인 현실 세계 맥락을 추가하기 위한 웹 검색 기능
119* "확장 프로그램 및 에셋 다운로드" 메뉴에서 더 많은 기능을 다운로드할 수 있습니다.
120
121
122사용 방법에 대한 튜토리얼은 [Docs](https://docs.sillytavern.app/)에서 확인할 수 있습니다.
123
124# ⌛ Installation
125
126> \[!WARNING]
127>
128> * **윈도우 제어 폴더에는 설치하지 마십시오 (Program Files, System32 등).**
129> * **권리자 권한으로 START.BAT을 실행하지 마십시오.**
130> * **Windows 7에서는 NodeJS 18.16을 실행할 수 없으므로 설치가 불가능합니다.**
131
132## 🪟 Windows
133
134### Git을 통해 설치하기
135
1361. [NodeJS](https://nodejs.org/ko) 설치 (최신 LTS 버전 권장)
1372. [Git for Windows](https://gitforwindows.org/) 설치
1383. 파일 탐색기 열기 (`Win+E`)
1394. Windows에서 제어하거나 모니터하지 않는 폴더를 찾거나 만드세요. (ex: C:\MySpecialFolder\)
1405. 상단의 주소 표시줄을 클릭하고 `cmd`를 입력한 후 Enter 키를 눌러 해당 폴더 내에서 명령 프롬프트를 여세요.
1416. 검은색 창(명령 프롬프트)이 나타나면 다음 중 하나를 입력하고 Enter 키를 누르세요.
142
143* Release 브랜치: `git clone https://github.com/SillyTavern/SillyTavern -b release`
144* Staging 브랜치: `git clone https://github.com/SillyTavern/SillyTavern -b staging`
145
1467. clone이 완료되면, `Start.bat`을 더블 클릭하여 NodeJS가 필요한 구성요소를 설치하도록 하세요.
1478. 그러면 서버가 시작하고, SillyTavern이 브라우저에 나타납니다.
148
149### GitHub Desktop을 통해 설치하기
150
151(이 방법은 **오직** GitHub Desktop에서만 git 사용이 가능합니다. 명령 프롬프트에서 git을 사용하려면 [Git for Windows](https://gitforwindows.org/)를 설치해야 합니다.)
152
153
154
155 1. [NodeJS](https://nodejs.org/ko) 설치 (최신 LTS 버전 권장)
156 2. [GitHub Desktop](https://central.github.com/deployments/desktop/desktop/latest/win32) 설치
157 3. GitHub Desktop을 설치했으면, `Clone a repository from the internet....`를 클릭하세요. (참고: 이 과정에서는 Github 계정이 **필요하지 않습니다**.)
158 4. 메뉴에서 URL 탭을 클릭하고, 다음 URL을 입력한 후 복제를 클릭합니다: `https://github.com/SillyTavern/SillyTavern` 리포지토리가 다운로드될 위치를 변경하려면 로컬 경로를 변경할 수 있습니다.
159 5. SillyTavern을 열려면 파일 탐색기를 사용하여 리포지토리를 복제한 폴더로 이동합니다. 기본적으로 리포지토리는 다음 위치에 복제됩니다: `C:\Users\[사용자 Windows 사용자 이름]\Documents\GitHub\SillyTavern`
160 6. `start.bat` 파일을 더블 클릭 하세요. (참고: `.bat` 확장자 명은 OS 설정에 따라 보이지 않을 수 있습니다, 그럴 때는 파일 이름이 "`Start`" 처럼 보일 수 있습니다. 이 파일을 더블 클릭해 SillyTavern을 실행하세요.)
161 7. 더블 클릭하면, 검고 큰 명령 프롬프트 창이 열리고 SillyTavern이 작동하는데 필요한 항목을 설치하기 시작합니다.
162 8. 설치 과정이 끝나고 모든 것이 잘 작동한다면, 브라우저에 SillyTavern 탭이 열려 있어야 하고, 명령 프롬프트 창에 다음과 같이 표시되어야 합니다:
163 9. Connect to any of the [supported APIs](https://docs.sillytavern.app/usage/api-connections/) and start chatting!
164
165## 🐧 Linux & 🍎 MacOS
166
167MacOS / Linux 에서는 이 모든 작업이 터미널에서 수행됩니다.
168
1691. git과 nodeJS 설치 (이 작업은 OS에 따라 달라집니다.)
1702. 리포지토리 clone하기
171
172* Release 브랜치: `git clone https://github.com/SillyTavern/SillyTavern -b release`
173* Staging 브랜치: `git clone https://github.com/SillyTavern/SillyTavern -b staging`
174
1753. `cd SillyTavern` 를 입력해 설치 폴더로 이동하기
1764. `start.sh` 스크립트를 아래의 명령어 중 하나로 실행하기:
177
178* `./start.sh`
179* `bash start.sh`
180
181## ⚡ SillyTavern Launcher를 통해 설치하기
182
183SillyTavern 런처는 로컬 LLM 사용을 위한 백엔드 설치를 포함하여 다양한 설정을 도와주는 설치 마법사입니다.
184
185
186### Windows 사용자
187
1881. 키보드에서 **`WINDOWS + R`** 키를 눌러 실행 창을 여세요. 그리고 아래의 명령어를 입력해 git을 설치하세요.
189
190```shell
191cmd /c winget install -e --id Git.Git
192```
193
1942. 키보드에서 **`WINDOWS + E`** 키를 눌러 파일 탐색기를 열고 런처를 설치할 폴더로 이동합니다. 원하는 폴더에 도착하면 주소 표시줄에 `cmd`를 입력하고 Enter 키를 누릅니다. 그 후 아래의 명령어를 입력합니다.
195
196
197```shell
198git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher && start installer.bat
199```
200
201### Linux 사용자
202
2031. 선호하는 터미널을 열고 git을 설치하세요.
2042. SillyTavern-Launcher를 clone 하세요:
205
206```shell
207git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
208```
209
2103. installer.sh를 실행하세요:
211
212```shell
213chmod +x install.sh && ./install.sh
214```
215
2164. 설치가 끝나면 launcher.sh를 실행하세요:
217
218```shell
219chmod +x launcher.sh && ./launcher.sh
220```
221
222### Mac 사용자
223
2241. 터미널을 열고 Brew를 설치하세요:
225
226```shell
227/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
228```
229
2302. git을 설치하세요:
231
232```shell
233brew install git
234```
235
2363. SillyTavern-Launcher를 clone 하세요:
237
238```shell
239git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
240```
241
2424. installer.sh를 실행하세요:
243
244```shell
245chmod +x install.sh && ./install.sh
246```
247
2485. 설치가 끝나면 launcher.sh를 실행하세요:
249
250```shell
251chmod +x launcher.sh && ./launcher.sh
252```
253
254## 🐋 Docker를 통해 설치하기
255
256이 방법은 Docker가 설치되어 있고, Docker 설치를 위해 커맨드 라인에 접근할 수 있으며, Docker의 일반적인 작동 방식에 익숙하다고 가정합니다.
257
258### 이미지 직접 빌드하기
259
260SillyTavern을 Docker에서 사용하는 방법에 대한 포괄적인 가이드는 [여기서](http://docs.sillytavern.app/installation/docker/) 확인할 수 있습니다. 이 가이드는 Windows, macOS 및 Linux에서의 설치를 다룹니다! 직접 이미지를 빌드하려면 이 가이드를 읽어보세요.
261
262### GitHub Container Registry 사용하기 (가장 쉬움)
263
264SillyTavern이 작동하려면 두 개의 필수 디렉터리 매핑과 하나의 포트 매핑이 필요합니다. 명령에서 다음 위치의 선택 항목을 바꿔주세요.
265
266
267#### Container Variables
268
269##### Volume Mappings
270
271* [config] - SillyTavern 구성 파일이 호스트 컴퓨터에 저장될 디렉터리
272* [data] - 캐릭터를 포함한 SillyTavern 사용자 데이터가 호스트 컴퓨터에 저장될 디렉터리
273* [plugins] - (선택 사항) SillyTavern 서버 플러그인이 호스트 컴퓨터에 저장될 디렉터리
274
275##### Port Mappings
276
277* [PublicPort] - 트래픽을 노출할 포트입니다. 가상 머신 컨테이너 외부에서 인스턴스에 접근하므로 필수 사항입니다. 보안을 위한 별도의 서비스를 구현하지 않고는 인터넷에 노출하지 마십시오.
278
279
280##### Additional Settings
281
282* [DockerNet] - 컨테이너가 연결되어 생성되어야 하는 Docker 네트워크입니다. 해당 내용을 모르는 경우 [공식 Docker 문서](https://docs.docker.com/reference/cli/docker/network/)를 참조하세요.
283* [version] - 이 GitHub 페이지의 오른쪽에서 "Packages"를 선택하면 "sillytavern" 패키지를 볼 수 있습니다. "latest" 이미지 태그는 현재 릴리스와 함께 최신 상태를 유지합니다. 각 브랜치의 야간 이미지를 가리키는 "staging" 및 "release" 태그를 사용할 수도 있지만, 업데이트에 시간이 걸릴 수 있고 중단될 수 있는 확장 프로그램을 사용하는 경우에는 적합하지 않을 수 있습니다.
284
285
286#### 설치 명령어
287
2881. 커맨드 라인 열기
2892. 아래의 명령어 실행
290
291`docker create --name='sillytavern' --net='[DockerNet]' -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'`
292
293> 8000은 기본 리스닝 포트입니다. 구성에서 포트를 변경한 경우 적절한 포트를 사용하는 것을 잊지 마세요.
294## 📱 Termux를 통해 Android OS에 설치하기
295
296> \[!NOTE]
297> **SillyTavern은 Termux를 사용하여 Android 기기에서 기본적으로 실행할 수 있지만, 이러한 사용 사례에 대한 공식적인 지원은 제공하지 않습니다.**
298>
299> **ArroganceComplex#2659의 가이드를 참조하세요:**
300>
301> * <https://rentry.org/STAI-Termux>
302
303**지원되지 않는 플랫폼: android arm LEtime-web.** 32비트 Android는 npm으로 설치할 수 없는 외부 종속성이 필요합니다. 다음 명령어를 사용하여 설치하세요: pkg install esbuild. 그런 다음 일반적인 설치 단계를 진행하세요.
304
305
306## API 키 관리
307
308SillyTavern은 API 키를 사용자 데이터 디렉터리의 `secrets.json` 파일에 저장합니다 (`/data/default-user/secrets.json`이 기본 경로입니다).
309
310
311
312기본적으로 API 키는 저장하고 페이지를 새로 고침한 후에는 인터페이스에서 보이지 않습니다.
313
314키 보기 기능을 활성화하려면 다음 단계를 따르세요:
315
3161. `config.yaml` 파일에서 `allowKeysExposure` 값을 `true로` 설정합니다.
3172. SillyTavern 서버를 다시 시작합니다.
3183. API 연결 패널 오른쪽 하단에 있는 '숨겨진 API 키 보기' 링크를 클릭합니다.
319
320## 커맨드 라인 인수
321
322`config.yaml`의 일부 설정을 덮어쓰기 위해 SillyTavern 서버 시작 시 커맨드 라인 인수를 전달할 수 있습니다.
323
324
325### 예시
326
327```shell
328node server.js --port 8000 --listen false
329# 혹은
330npm run start -- --port 8000 --listen false
331# 혹은 (Windows 전용)
332Start.bat --port 8000 --listen false
333```
334
335### 지원되는 인수
336
337| 옵션 | 설명 | 타입 |
338|-----------------------------------|--------------------------------------------------------------------------------------|----------|
339| `--version` | 버전 표시 | boolean |
340| `--enableIPv6` | IPv6 활성화 | boolean |
341| `--enableIPv4` | IPv4 활성화 | boolean |
342| `--port` | SillyTavern이 실행될 포트를 설정합니다. | number |
343| `--dnsPreferIPv6` | DNS에 IPv6를 우선으로 할당합니다. | boolean |
344| `--browserLaunchEnabled` | 브라우저에서 SillyTavern을 자동으로 실행합니다. | boolean |
345| `--browserLaunchHostname` | 자동 실행 호스트 이름, 'auto'가 최적의 설정일 것입니다. | string |
346| `--browserLaunchPort` | 자동 실행 포트 덮어쓰기 | string |
347| `--browserLaunchAvoidLocalhost` | 자동 모드에서 자동 실행 시 'localhost' 사용 방지 | boolean |
348| `--listen` | SillyTavern이 모든 네트워크 인터페이스에서 수신 대기합니다. | boolean |
349| `--corsProxy` | CORS 프록시 활성화. | boolean |
350| `--disableCsrf` | CSRF 보호 비활성화 | boolean |
351| `--ssl` | SSL 활성화 | boolean |
352| `--certPath` | 인증서 파일 경로 | string |
353| `--keyPath` | 프라이빗 키 파일 경로 | string |
354| `--whitelist` | 화이트리스트 모드 활성화 | boolean |
355| `--dataRoot` | 데이터 스토리지의 루트 디렉토리 | string |
356| `--basicAuthMode` | 기본 인증 활성화 | boolean |
357| `--requestProxyEnabled` | 외부 리퀘스트 프록시 활성화 | boolean |
358| `--requestProxyUrl` | 프록시 URL 리퀘스트 (HTTP 혹은 SOCKS 프로토콜) | string |
359| `--requestProxyBypass` | 프록시 바이패스 리스트 리퀘스트 (공백으로 구분된 호스트 목록) | array |
360
361## 원격 연결
362
363대부분의 경우 이는 PC에서 ST 서버를 실행하는 동안 모바일 장치에서 SillyTavern을 사용하려는 사람들을 위한 것입니다. 그러나 원격 연결을 다른 곳에서도 허용하도록 사용할 수 있습니다.
364
365원격 연결 설정 방법에 대한 자세한 가이드는 [Docs](https://docs.sillytavern.app/usage/remoteconnections/)에서 확인할 수 있습니다.
366
367또한 암호 보호 기능이 포함된 SillyTavern 사용자 프로필을 구성할 수 있습니다 (선택 사항): [Users](https://docs.sillytavern.app/installation/st-1.12.0-migration-guide/#users).
368
369## 성능 이슈가 발생하나요?
37068
3711. 사용자 설정 패널(UI 테마 전환 카테고리)에서 흐림 효과를 비활성화하고 동작 줄이기를 활성화합니다.69* **[Windows 설치 가이드](https://docs.sillytavern.app/installation/windows/)**
3722. 응답 스트리밍을 사용하는 경우 스트리밍 FPS를 더 낮은 값(10-15 FPS 권장)으로 설정합니다.70* **[MacOS/Linux 설치 가이드](https://docs.sillytavern.app/installation/linuxmacos/)**
3733. 브라우저에서 렌더링에 GPU 가속을 사용하도록 설정되어 있는지 확인합니다.71* **[Android (Termux) 설치 가이드](https://docs.sillytavern.app/installation/android-(termux)/)**
72* **[Docker 설치 가이드](https://docs.sillytavern.app/installation/docker/)**
37473
375## 라이센스 및 크레딧74## 라이센스 및 크레딧
37675
@@ -383,6 +82,7 @@ Start.bat --port 8000 --listen false
383* Font Awesome의 아이콘 테마 <https://fontawesome.com> (아이콘: CC BY 4.0, 폰트: SIL OFL 1.1, 코드: MIT 라이선스)82* Font Awesome의 아이콘 테마 <https://fontawesome.com> (아이콘: CC BY 4.0, 폰트: SIL OFL 1.1, 코드: MIT 라이선스)
384* 기본 콘텐츠는 @OtisAlejandro (Seraphina 캐릭터 및 로어북)와 @kallmeflocc (10K 디스코드 사용자 축전 배경화면)가 제공함83* 기본 콘텐츠는 @OtisAlejandro (Seraphina 캐릭터 및 로어북)와 @kallmeflocc (10K 디스코드 사용자 축전 배경화면)가 제공함
385* [@mrguymiah](https://github.com/mrguymiah)와 [@Bronya-Rand](https://github.com/Bronya-Rand)의 Docker 가이드84* [@mrguymiah](https://github.com/mrguymiah)와 [@Bronya-Rand](https://github.com/Bronya-Rand)의 Docker 가이드
85* [@hexgrad](https://github.com/hexgrad)의 kokoro-js 라이브러리 (Apache-2.0 라이선스)
38686
387## 상위 기여자87## 상위 기여자
38888
.github/readme-ru_ru.md+44 -400
@@ -10,7 +10,7 @@
10[English](readme.md) | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | [繁體中文](readme-zh_tw.md) | [日本語](readme-ja_jp.md) | Русский | [한국어](readme-ko_kr.md)10[English](readme.md) | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | [繁體中文](readme-zh_tw.md) | [日本語](readme-ja_jp.md) | Русский | [한국어](readme-ko_kr.md)
1111
12[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)12[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
13[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)13[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/forks)
14[![GitHub Issues](https://img.shields.io/github/issues/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/issues)14[![GitHub Issues](https://img.shields.io/github/issues/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/issues)
15[![GitHub Pull Requests](https://img.shields.io/github/issues-pr/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/pulls)15[![GitHub Pull Requests](https://img.shields.io/github/issues-pr/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/pulls)
1616
@@ -18,433 +18,77 @@
1818
19---19---
2020
21Мобайл-френдли интерфейс, поддержка множества API (KoboldAI/CPP, Horde, NovelAI, Ooba, OpenAI, OpenRouter, Claude, Scale), ВН-образный режим Вайфу, Stable Diffusion, TTS, поддержка миров (лорбуков), кастомизируемый UI, автоперевод, тончайшая настройка промптов + возможность устанавливать расширения.21SillyTavern предоставляет единый интерфейс для многих LLM API (KoboldAI/CPP, Horde, NovelAI, Ooba, Tabby, OpenAI, OpenRouter, Claude, Mistral и других), мобайл-френдли макет, режим визуальной новеллы, интеграцию с генерацией изображений через API Automatic1111 и ComfyUI, TTS, WorldInfo (лорбуки), кастомизируемый UI, автоперевод, тончайшую настройку промптов, и возможность устанавливать расширения.
2222
23Основано на форке [TavernAI](https://github.com/TavernAI/TavernAI) версии 1.2.823Чтобы помочь вам быстрее разобраться в SillyTavern, мы создали [сайт с документацией](https://docs.sillytavern.app/). Ответы на большинство вопросов можно найти там.
2424
25## Важные новости!25## Что такое SillyTavern?
2626
271. Чтобы помочь вам быстрее разобраться в SillyTavern, мы создали [сайт с документацией](https://docs.sillytavern.app/). Ответы на большинство вопросов можно найти там.27SillyTavern (или сокращенно ST) - это локально устанавливаемый пользовательский интерфейс, который позволяет вам взаимодействовать с LLM для генерации текста, движками для генерации изображений и моделями голоса TTS.
2828
292. Почему пропали расширения после апдейта? Начиная с версии 1.10.6, большинство встроенных расширений были конвертированы в формат загружаемых аддонов. Их можно установить обратно через меню "Download Extensions and Assets" на панели расширений (значок с тремя кубиками сверху).29Начавшись в феврале 2023 года как форк TavernAI 1.2.8, SillyTavern теперь насчитывает более 200 контрибьюторов и 2 года независимой разработки, и продолжает служить ведущим программным обеспечением для опытных энтузиастов ИИ.
3030
313. Не поддерживается следующая платформа: android arm LEtime-web. 32-битный Android требует внешнюю зависимость, которую нельзя установить посредством npm. Для её установки потребуется следующая команда: `pkg install esbuild`. После этого продолжайте установку по общей инструкции.31## Наше видение
3232
33### Разрабатывается Cohee, RossAscends и всем сообществом SillyTavern331. Мы стремимся предоставить пользователям как можно больше полезности и контроля над их промптами LLM. Крутая кривая обучения - это часть веселья!
342. Мы не предоставляем никаких онлайн или хостинговых услуг, а также программно не отслеживаем данные пользователей.
353. SillyTavern - это проект, созданный преданным сообществом энтузиастов LLM, и он всегда будет бесплатным и с открытым исходным кодом.
3436
35### Что такое SillyTavern и TavernAI?37## Нужен ли мне мощный компьютер для запуска SillyTavern?
3638
37SillyTavern — это интерфейс, который устанавливается на ПК (и на Android), который даёт возможность общаться с генеративным ИИ и чатиться/ролеплеить с вашими собственными персонажами или персонажами других пользователей.39Требования к оборудованию минимальны: он будет работать на всем, что может запустить NodeJS 18 или выше. Если вы собираетесь выполнять инференс LLM на своем локальном компьютере, мы рекомендуем видеокарту NVIDIA 3000-й серии с не менее чем 6 ГБ видеопамяти, но фактические требования могут варьироваться в зависимости от модели и используемого вами бэкенда.
3840
39SillyTavern — это форк версии TavernAI 1.2.8, который разрабатывается более активно и имеет множество новых функций. Сейчас уже можно сказать, что это две отдельные и абсолютно самостоятельные программы.41## Вопросы или предложения?
4042
41## Скриншоты43### Сервер в Discord
42
43<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">
44<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">
45
46### Ветки
47
48SillyTavern разрабатывается в двух ветках, чтобы всем категориям пользователей было удобно.
49
50* release -🌟 **Рекомендовано для большинства пользователей.** Самая стабильная ветка, рекомендуем именно её. Обновляется только в момент крупных релизов. Подходит для большей части пользователей.
51* staging - ⚠️ **Не рекомендуется для повседневного использования.** В этой ветке весь самый свежий и новый функционал, но будьте аккуратны, поскольку сломаться может в любом месте и в любое время. Только для продвинутых пользователей и энтузиастов.
52
53Если вы не умеете обращаться с git через командную строку, или не знаете, что такое ветка, то не переживайте! Наилучшим вариантом всегда остаётся ветка release.
54
55### Что ещё нужно, кроме SillyTavern?
56
57Сама по себе SillyTavern бесполезна, ведь это просто интерфейс. Вам потребуется доступ к бэкенду с ИИ, который и будет отыгрывать выбранного вами персонажа. Поддерживаются разные виды бэкендов: OpenAPI API (GPT), KoboldAI (локально или на Google Colab), и многое другое. Больше информации в [FAQ](https://docs.sillytavern.app/usage/faq/).
58
59### Требуется ли для SillyTavern мощный ПК?
60
61SillyTavern — это просто интерфейс, поэтому запустить его можно на любой картошке. Мощным должен быть бэкенд с ИИ.
62
63## Есть вопросы или предложения?
64
65### У нас появился сервер в Discord
6644
67| [![][discord-shield-badge]][discord-link] | [Вступайте в наше Discord-сообщество!](https://discord.gg/sillytavern) Задавайте вопросы, делитесь любимыми персонажами и промптами. |45| [![][discord-shield-badge]][discord-link] | [Вступайте в наше Discord-сообщество!](https://discord.gg/sillytavern) Получайте поддержку, делитесь любимыми персонажами и промптами. |
68| :---------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------- |46| :---------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------- |
6947
70Также можно написать разработчикам напрямую:48Или свяжитесь с разработчиками напрямую:
71
72* Discord: cohee или rossascends
73* Reddit: [/u/RossAscends](https://www.reddit.com/user/RossAscends/) или [/u/sillylossy](https://www.reddit.com/user/sillylossy/)
74* [Запостить issue на GitHub](https://github.com/SillyTavern/SillyTavern/issues)
75
76## Эта версия включает
77
78* Глубоко переработанную TavernAI 1.2.8 (переписано и оптимизировано более 50% кода)
79* Свайпы
80* Групповые чаты: комнаты для нескольких ботов, где персонажи могут говорить друг с другом и с вами
81* Чекпоинты и ветки для чатов
82* Продвинутые настройки для KoboldAI / TextGen со множеством созданных сообществом пресетов
83* Поддержка миров (функция "Информация о мире" / WorldInfo): создавайте свой богатый лор, или экономьте токены для карточек персонажей
84* Соединение через [OpenRouter](https://openrouter.ai) для разных API (Claude, GPT-4/3.5 и других)
85* Соединение с API [Oobabooga's TextGen WebUI](https://github.com/oobabooga/text-generation-webui)
86* Соединение с [AI Horde](https://aihorde.net/)
87* Настройку форматирования промптов
88
89## Расширения
90
91SillyTavern поддерживает расширения.
92
93* Эмоции для персонажей (спрайты)
94* Автоматический саммарайз (краткий пересказ) истории чата
95* Возможность отправить в чат картинку, которую ИИ сможет рассмотреть и понять
96* Генерация картинок в Stable Diffusion (5 пресетов для чата, плюс свободный режим)
97* Text-to-speech для сообщений ИИ (с помощью ElevenLabs, Silero, или родной TTS вашей ОС)
98
99Полный список расширений и инструкций к ним можно найти в [документации](https://docs.sillytavern.app/).
100
101## Улучшения от RossAscends для UI/CSS/общего удобства
102
103* Мобильный интерфейс адаптирован для iOS, добавлена возможность сохранить ярлык на главный экран и открыть приложение в полноэкранном режиме.
104* Горячие клавиши
105 * Up = Редактировать последнее сообщение в чате
106 * Ctrl+Up = Редактировать ВАШЕ последнее сообщение в чате
107 * Left = свайп влево
108 * Right = свайп вправо (ОБРАТИТЕ ВНИМАНИЕ: когда в окне ввода что-то напечатано, клавиши для свайпа не работают)
109 * Ctrl+Left = посмотреть локальные переменные (в консоли браузера)
110 * Enter (при нахождении внутри окна ввода) = отправить ваше сообщение ИИ
111 * Ctrl+Enter = Повторная генерация последнего ответа ИИ
112
113* Страница больше не перезагружается при смене имени пользователя или удалении персонажа
114
115* Отключаемая возможность автоматически соединяться с API при загрузке страницы.
116* Отключаемая возможность автоматически загружать последнего открытого персонажа при загрузке страницы.
117* Улучшенный счётчик токенов - работает с несохранёнными персонажами, отображает и перманентные, и временные токены
118
119* Улучшенный менеджер чатов
120 * Файлы с новыми чатами получают читабельные названия вида "(персонаж) - (когда создано)"
121 * Увеличен размер превью чата с 40 символов до 300.
122 * Несколько вариантов сортировки списка персонажей (по имени, дате создания, размеру чата).
123
124* Панели настроек слева и справа автоматически скрываются, если щёлкнуть за их пределы.
125* При нажатии на значок замка навигационная панель будет закреплена на экране, и эта настройка сохранится между сессиями
126* Сам статус панели (открыта или закрыта) также сохраняется между сессиями
127
128* Кастомизируемый интерфейс чата:
129 * Настройте звук при получении нового ответа
130 * Переключайтесь между круглыми и прямоугольными аватарками
131 * Увеличенное вширь окно чата для стационарных ПК
132 * Возможность включать полупрозрачные панели, стилизованные под стекло
133 * Настраиваемые цвета для обычного текста, курсива, цитат
134 * Настраиваемый цвет фона и интенсивность размытия
135
136# ⌛ Установка
137
138> **Внимание!**
139> * НЕ УСТАНАВЛИВАЙТЕ В ПАПКИ, КОТОРЫЕ КОНТРОЛИРУЕТ WINDOWS (Program Files, System32 и т.п.).
140> * НЕ ЗАПУСКАЙТЕ START.BAT С ПРАВАМИ АДМИНИСТРАТОРА
141> * УСТАНОВКА НА WINDOWS 7 НЕВОЗМОЖНА ИЗ-ЗА ОТСУТСТВИЯ NODEJS 18.16
142
143## 🪟 Windows
144
145## Установка через Git
146 1. Установите [NodeJS](https://nodejs.org/en) (рекомендуется последняя LTS-версия)
147 2. Установите [Git for Windows](https://gitforwindows.org/)
148 3. Откройте Проводник (`Win+E`)
149 4. Перейдите в папку, которую не контролирует Windows, или создайте её. (пример: C:\MySpecialFolder\)
150 5. Откройте командную строку. Для этого нажмите на адресную строку (сверху), введите `cmd` и нажмите Enter.
151 6. Когда появится чёрное окошко (командная строка), введите ОДНУ из перечисленных ниже команд:
152
153- для ветки release: `git clone https://github.com/SillyTavern/SillyTavern -b release`
154- для ветки staging: `git clone https://github.com/SillyTavern/SillyTavern -b staging`
155
156 7. Когда клонирование закончится, дважды щёлкните по `Start.bat`, чтобы установить зависимости для NodeJS.
157 8. После этого сервер запустится, и SillyTavern откроется в вашем браузере.
158
159## Установка с помощью SillyTavern Launcher
160 1. Установите [Git for Windows](https://gitforwindows.org/)
161 2. Откройте Проводник (`Win+E`) и создайте или выберите папку, в которую будет установлен лаунчер
162 3. Откройте командную строку. Для этого нажмите на адресную строку (сверху), введите `cmd` и нажмите Enter.
163 4. Когда появится чёрное окошко, введите следующую команду: `git clone https://github.com/SillyTavern/SillyTavern-Launcher.git`
164 5. Дважды щёлкните по `installer.bat` и выберите, что именно хотите установить
165 6. После завершения установки дважды щёлкните по `launcher.bat`
166
167## Установка с помощью GitHub Desktop
168(Тут речь про git **только** в рамках GitHub Desktop, если хотите использовать `git` в командной строке, вам также понадобится [Git for Windows](https://gitforwindows.org/))
169 1. Установите [NodeJS](https://nodejs.org/en) (latest LTS version is recommended)
170 2. Установите [GitHub Desktop](https://central.github.com/deployments/desktop/desktop/latest/win32)
171 3. После завершения установки GitHub Desktop, нажмите `Clone a repository from the internet....` (обратите внимание: для этого шага **НЕ требуется** аккаунт на GitHub)
172 4. В меню перейдите на вкладку URL, введите адрес `https://github.com/SillyTavern/SillyTavern`, и нажмите Clone. В поле Local path можно изменить директорию, в которую будет загружена SillyTavern.
173 6. Чтобы запустить SillyTavern, откройте Проводник и перейдите в выбранную на предыдущем шаге папку. По умолчанию репозиторий будет склонирован сюда: `C:\Users\[Имя пользователя]\Documents\GitHub\SillyTavern`
174 7. Дважды щёлкните по файлу `start.bat`. (обратите внимание: окончание `.bat` может быть скрыто настройками вашей ОС. Таким образом, имя файла будет выглядеть как "`Start`". Дважды щёлкните по нему, чтобы запустить SillyTavern)
175 8. После того, как вы дважды щёлкнули по файлу, должно открыться чёрное окошко, и SillyTavern начнёт устанавливать свои зависимости.
176 9. Если установка прошла успешно, то в командной строке будет вот такое, а в браузере откроется вкладка с SillyTavern:
177 10. Подключайтесь к любому из [поддерживаемых API](https://docs.sillytavern.app/usage/api-connections/) и начинайте переписку!
178
179## 🐧 Linux и 🍎 MacOS
180
181В MacOS и Linux всё это делается через Терминал.
182
1831. Установите git и nodeJS (как именно - зависит от вашей ОС)
1842. Клонируйте репозиторий
185
186- для ветки release: `git clone https://github.com/SillyTavern/SillyTavern -b release`
187- для ветки staging: `git clone https://github.com/SillyTavern/SillyTavern -b staging`
188
1893. Перейдите в папку установки с помощью `cd SillyTavern`.
1904. Запустите скрипт `start.sh` с помощью одной из команд:
191
192- `./start.sh`
193- `bash start.sh`
194
195## Установка с помощью SillyTavern Launcher
196
197### Для пользователей Linux
1981. Откройте любимый терминал и установите git
1992. Загрузите Sillytavern Launcher с помощью команды: `git clone https://github.com/SillyTavern/SillyTavern-Launcher.git`
2003. Перейдите в SillyTavern-Launcher: `cd SillyTavern-Launcher`
2014. Запустите лаунчер установки: `chmod +x install.sh && ./install.sh`, затем выберите, что конкретно хотите установить
2025. После завершения установки, запустите лаунчер следующей командой: `chmod +x launcher.sh && ./launcher.sh`
203
204### Для пользователей Mac
2051. Откройте терминал и установите brew: `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"`
2062. Затем установите git: `brew install git`
2073. Загрузите Sillytavern Launcher: `git clone https://github.com/SillyTavern/SillyTavern-Launcher.git`
2084. Перейдите в SillyTavern-Launcher: `cd SillyTavern-Launcher`
2095. Запустите лаунчер установки: `chmod +x install.sh && ./install.sh` and choose what you wanna install
2106. После завершения установки, запустите лаунчер следующей командой: `chmod +x launcher.sh && ./launcher.sh`
211
212## 🐋 Установка с помощью Docker
213
214Предполагается, что вы уже установили Docker, имеете доступ к командной строке для установки контейнеров и знакомы с их базовым управлением.
215
216### Сборка образа самостоятельно
217
218У нас есть подробное руководство по использованию SillyTavern в Docker [здесь](http://docs.sillytavern.app/installation/docker/), которое охватывает установку на Windows, macOS и Linux! Ознакомьтесь с ним, если хотите создать образ самостоятельно.
219
220### Использование реестра контейнеров GitHub (самый простой способ)
221
222Для работы SillyTavern вам понадобятся две обязательные настройки каталогов и одна настройка порта. В команде замените указанные значения на свои:
223
224#### Переменные контейнера
225
226##### Маппинг томов
227
228* [config] - директория, где на вашем хосте будут храниться файлы конфигурации SillyTavern.
229* [data] - директория, где на вашем хосте будут храниться пользовательские данные SillyTavern (включая персонажей).
230* [plugins] - (необязательно) директория, где на вашем хосте будут храниться плагины сервера SillyTavern.
231
232##### Маппинг портов
233
234* [PublicPort] - Порт, через который будет передаваться трафик. Это обязательно, так как вы будете обращаться к контейнеру извне его виртуальной машины. НЕ ОТКРЫВАЙТЕ этот порт в интернет без реализации дополнительного уровня безопасности.
235
236##### Дополнительные настройки
237
238* [DockerNet] - Docker сеть, к которой контейнер должен быть подключен. Если вы не знаете, что это, обратитесь к [официальной документации Docker](https://docs.docker.com/reference/cli/docker/network/).
239* [version] - на правой части этой страницы GitHub вы найдете раздел "Packages". Выберите пакет "sillytavern", чтобы увидеть версии образов. Тег "latest" позволит вам обновляться до текущего релиза. Также доступны теги "staging" и "release", которые соответствуют ночным сборкам соответствующих веток. Однако это может быть нецелесообразно, если вы используете расширения, которые могут ломаться и требуют времени для обновления.
240
241#### Команда установки
242
2431. Откройте командную строку
2442. Выполните следующую команду
245
246`docker create --name='sillytavern' --net='[DockerNet]' -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'`
247
248> Заметьте, что 8000 является портом по умолчанию. Не забудьте использовать соответствующий порт, если вы измените его в конфиге.
249
250## 📱 Мобильные устройства - Установка при помощи termux
251
252> **ОБРАТИТЕ ВНИМАНИЕ!**
253>
254> **На Android-телефонах SillyTavern можно запускать нативно посредством Termux. Обратитесь к гайду, написанному ArroganceComplex#2659:**
255>
256> * <https://rentry.org/STAI-Termux>
257
25849
259## Управление ключами от API50* Discord: cohee, rossascends, wolfsblvt
51* Reddit: [/u/RossAscends](https://www.reddit.com/user/RossAscends/), [/u/sillylossy](https://www.reddit.com/user/sillylossy/), [u/Wolfsblvt](https://www.reddit.com/user/Wolfsblvt/)
52* [Опубликовать issue на GitHub](https://github.com/SillyTavern/SillyTavern/issues)
26053
261SillyTavern сохраняет ключи от ваших API в файле `secrets.json` в папке на сервере.54### Мне нравится ваш проект! Как я могу внести свой вклад?
26255
263По умолчанию, ключи не будут отображаться на фронте после их ввода и перезагрузки страницы.561. Отправляйте пулл-реквесты. Узнайте, как внести свой вклад: [CONTRIBUTING.md](../CONTRIBUTING.md)
572. Отправляйте предложения по функциям и отчеты о проблемах, используя предоставленные шаблоны.
583. Прочтите весь этот файл readme и сайт документации, чтобы избежать отправки дублирующихся проблем.
26459
265Чтобы включить возможность отображения ключей путём нажатия кнопки в блоке API:60## Скриншоты
266
2671. Зайдите в файл `config.yaml` и установите `allowKeysExposure` в положение `true`.
2682. Перезапустите сервер SillyTavern.
269
270## Аргументы командной строки
271
272Вы можете передавать аргументы командной строки при запуске сервера SillyTavern, чтобы переопределять настройки из `config.yaml`.
273
274### Примеры
275
276```shell
277node server.js --port 8000 --listen false
278# или
279npm run start -- --port 8000 --listen false
280# или (только на Windows)
281Start.bat --port 8000 --listen false
282```
283
284### Поддерживаемые аргументы
285
286| Аргумент | Описание | Тип |
287|------------------------------|-----------------------------------------------------------------------------------------------------------|---------|
288| `--version` | Показывает номер версии. | boolean |
289| `--enableIPv6` | Включает IPv6. | boolean |
290| `--enableIPv4` | Включает IPv4. | boolean |
291| `--port` | Устанавливает порт, который будет использовать SillyTavern. | number |
292| `--dnsPreferIPv6` | Отдает предпочтение IPv6 для dns. | boolean |
293| `--browserLaunchEnabled` | Автоматический запуск SillyTavern в браузере. | boolean |
294| `--browserLaunchHostname` | Имя хоста автозапуска, лучше оставить на 'auto'. | string |
295| `--browserLaunchPort` | Переопределяет порт для автозапуска. | string |
296| `--browserLaunchAvoidLocalhost`| Избегает использования 'localhost' для автозапуска в режиме 'auto'. | boolean |
297| `--listen` | SillyTavern будет прослушивать все сетевые интерфейсы. | boolean |
298| `--corsProxy` | Включает CORS-прокси. | boolean |
299| `--disableCsrf` | Отключает защиту от CSRF. | boolean |
300| `--ssl` | Включает SSL. | boolean |
301| `--certPath` | Путь к файлу c сертификатом. | string |
302| `--keyPath` | Путь к файлу с закрытым ключом. | string |
303| `--whitelist` | Включает режим белого списка. | boolean |
304| `--dataRoot` | Корневой каталог для хранения данных. | string |
305| `--basicAuthMode` | Включает простую аутентификацию. | boolean |
306| `--requestProxyEnabled` | Разрешает использование прокси для исходящих запросов. | boolean |
307| `--requestProxyUrl` | URL-адрес прокси (протоколы HTTP или SOCKS). | string |
308| `--requestProxyBypass` | Bypass список прокси (список хостов, разделенных пробелами). | array |
309
310## Удалённое подключение
311
312В основном этим пользуются тогда, когда хотят использовать SillyTavern с телефона, запустив сервер SillyTavern на стационарном ПК в той же Wi-Fi-сети.
313
314Однако это позволит подключаться откуда угодно, а не только вам.
315
316**ВАЖНО: в SillyTavern не предусмотрена возможность использования программы несколькими людьми. Поэтому любой, кто подключится к вашему серверу, получит доступ ко всем вашим персонажам и чатам, а также сможет менять настройки через UI.**
317
318### 1. Заведение "белого списка" IP-адресов
319
320* Создайте в корневой папке SillyTavern файл с названием `whitelist.txt`.
321* Откройте файл в текстовом редакторе и внесите список IP-адресов, с которых хотите разрешить подключение.
322
323*Принимаются как обычные IP-адреса, так и целые диапазоны, размеченные с помощью астериска. Примеры:*
324
325```txt
326192.168.0.1
327192.168.0.20
328```
329
330или
331
332```txt
333192.168.0.*
334```
335
336(диапазон из примера сверху позволит подключаться всем устройствам в локальной сети)
337
338Также принимаются маски CIDR (вида 10.0.0.0/24).
339
340* Сохраните файл `whitelist.txt`.
341* Перезапустите сервер ST.
342
343После этого устройства из белого списка смогут подключаться к вашему серверу.
344
345*Обратите внимание: в файле `config.yaml` также имеется массив `whitelist`, который работает по тому же принципу. Однако если существует файл `whitelist.txt`, то этот массив игнорируется.*
346
347### 2. Получение IP хост-машины с ST
348
349После настройки белого списка адресов, следующим шагом будет получение IP-адреса хост-машины, на которой запущена SillyTavern.
350
351Если хост-машина находится в той же Wi-Fi-сети, то можно воспользоваться её внутренним Wi-Fi-IP-адресом:
352
353* На Windows: нажмите Пуск > введите `cmd.exe` в поиске > в консоли введите команду `ipconfig` и нажмите Enter > найдите пункт `IPv4-адрес`.
354
355Если вы (или кто-то другой) хотите подключаться к хост-машине из другой сети, то вам понадобится ваш публичный IP-адрес.
356
357* Откройте [эту страницу](https://whatismyipaddress.com/) с вашей хост-машины и найдите пункт `IPv4`. На этот адрес и будет подключаться удалённое устройство.
358
359### 3. Соединить удалённое устройство с хост-машиной ST
360
361Какой бы IP-адрес вы ни выбрали, вам нужно будет вводить его в адресной строке браузера вашего удалённого устройства.
362
363Обычный адрес хост-машины, находящейся в той же Wi-Fi-сети, выглядит примерно так:
364
365`http://192.168.0.5:8000`
366
367НЕ используйте https://
368Только http://
369
370### Открытие доступа до ST для всех IP-адресов
371
372Мы не рекомендуем так делать, но вы можете открыть файл `config.yaml` и изменить `whitelistMode` на `false`.
373
374Обязательно нужно удалить (или переименовать) файл `whitelist.txt`, если такой файл есть в корневой директории SillyTavern.
375
376Эта практика считается небезопасной, поэтому, если вы решите так сделать, мы попросим вас установить логин и пароль.
377
378Оба этих параметра настраиваются в `config.yaml` (username и password).
379
380Останется только перезапустить сервер ST, и после этого к вам сможет подключиться любой пользователь вне зависимости от IP-адреса его устройства. Главное, чтобы он знал логин и пароль.
381
382### Не получается соединиться?
383
384* Создайте входящее/исходящее правило в вашем фаерволле для порта, указанного в `config.yaml`. НЕ ПУТАЙТЕ этот процесс с пробросом портов на роутере. Если по ошибке перепутаете, то на ваш сервер сможет забраться посторонний человек и украсть ваши логи, этого следует избегать.
385* Переключите Сетевой профиль на значение "Частные". Для этого зайдите в Параметры > Сеть и Интернет > Ethernet. КРАЙНЕ важно для Windows 11, без этого не получится подключиться даже с правилом фаервола.
386
387## Проблемы с производительностью?
388
389Попробуйте включить опцию "Отключить эффект размытия" в меню "Пользовательские настройки".
390
391## Нравится ваш проект! Как помочь?
392
393### ЧТО ДЕЛАТЬ
394
3951. Присылайте пулл реквесты
3962. Присылайте идеи и баг-репорты, оформленные по установленным шаблонам
3973. Прежде чем задавать вопросы, прочтите readme и документацию
398
399### ЧЕГО НЕ ДЕЛАТЬ
400
4011. Предлагать донаты
4022. Присылать баг-репорты безо всякого контекста
4033. Задавать вопросы, на которые уже отвечали
404
405## Где найти старые фоны?
40661
407Мы двигаемся в сторону 100% уникальности всего используемого контента, поэтому старые фоны были убраны из репозитория.62<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">
63<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">
40864
409Они отправлены в архив, скачать их можно здесь:65## Установка
41066
411<https://files.catbox.moe/1xevnc.zip>67Для получения подробных инструкций по установке, пожалуйста, посетите нашу документацию:
41268
69* **[Руководство по установке для Windows](https://docs.sillytavern.app/installation/windows/)**
70* **[Руководство по установке для MacOS/Linux](https://docs.sillytavern.app/installation/linuxmacos/)**
71* **[Руководство по установке для Android (Termux)](https://docs.sillytavern.app/installation/android-(termux)/)**
72* **[Руководство по установке Docker](https://docs.sillytavern.app/installation/docker/)**
41373
74## Лицензия и благодарности
41475
76**Эта программа распространяется в надежде, что она будет полезна, но БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ; даже без подразумеваемой гарантии ТОВАРНОЙ ПРИГОДНОСТИ или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННОЙ ЦЕЛИ. Смотрите GNU Affero General Public License для получения более подробной информации.**
41577
416## Авторы и лицензии78* [TavernAI](https://github.com/TavernAI/TavernAI) 1.2.8 от Humi: лицензия MIT
79* Части мода CncAnon TavernAITurbo используются с разрешения
80* Режим визуальной новеллы вдохновлен работой PepperTaco (<https://github.com/peppertaco/Tavern/>)
81* Шрифт Noto Sans от Google (лицензия OFL)
82* Тема иконок от Font Awesome <https://fontawesome.com> (Иконки: CC BY 4.0, Шрифты: SIL OFL 1.1, Код: лицензия MIT)
83* Стандартный контент от @OtisAlejandro (персонаж Seraphina и лорбук) и @kallmeflocc (фон в честь 10 тысяч пользователей Discord)
84* Руководство по Docker от [@mrguymiah](https://github.com/mrguymiah) и [@Bronya-Rand](https://github.com/Bronya-Rand)
85* Библиотека kokoro-js от [@hexgrad](https://github.com/hexgrad) (лицензия Apache-2.0)
41786
418**Мы надеемся, что эта программа принесёт людям пользу,87## Ведущие контрибьюторы
419но мы не даём НИКАКИХ ГАРАНТИЙ; мы ни в коем случае не гарантируем того,
420что программа СООТВЕТСТВУЕТ КАКИМ-ЛИБО КРИТЕРИЯМ или ПРИГОДНА ДЛЯ КАКОЙ-ЛИБО ЦЕЛИ.
421Подробнее можно узнать в GNU Affero General Public License.**
42288
423* Базовая TAI от Humi: Лицензия неизвестна89[![Contributors](https://contrib.rocks/image?repo=SillyTavern/SillyTavern)](https://github.com/SillyTavern/SillyTavern/graphs/contributors)
424* Модификации от Cohee и производная кодовая база: AGPL v3
425* Дополнения RossAscends: AGPL v3
426* Кусочки TavernAITurbo мода от CncAnon: Лицензия неизвестна
427* Различные коммиты и предложения от kingbri (<https://github.com/bdashore3>)
428* Расширения и внедрение разного рода удобств - city_unit (<https://github.com/city-unit>)
429* Различные коммиты и баг-репорты от StefanDanielSchwarz (<https://github.com/StefanDanielSchwarz>)
430* Режим Вайфу вдохновлён работой PepperTaco (<https://github.com/peppertaco/Tavern/>)
431* Благодарность Pygmalion University за прекрасную работу по тестированию и за все предлагаемые крутые фичи!
432* Благодарность oobabooga за компиляцию пресетов для TextGen
433* Пресеты для KoboldAI из KAI Lite: <https://lite.koboldai.net/>
434* Шрифт Noto Sans от Google (OFL license)
435* Тема Font Awesome <https://fontawesome.com> (Иконки: CC BY 4.0, Шрифты: SIL OFL 1.1, Код: MIT License)
436* Клиентская библиотека для AI Horde от ZeldaFan0225: <https://github.com/ZeldaFan0225/ai_horde>
437* Пусковой скрипт для Linux от AlpinDale
438* Благодарность paniphons за оформление документа с FAQ
439* Фон в честь 10 тысяч пользователей в Discord от @kallmeflocc
440* Стандартный контент (персонажи и лорбуки) предоставлен пользователями @OtisAlejandro, @RossAscends и @kallmeflocc
441* Корейский перевод от @doloroushyeonse
442* Поддержка k_euler_a для Horde от <https://github.com/Teashrock>
443* Китайский перевод от [@XXpE3](https://github.com/XXpE3), 中文 ISSUES 可以联系 @XXpE3
44490
445<!-- LINK GROUP -->91<!-- LINK GROUP -->
446[back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square
447[cover]: https://github.com/user-attachments/assets/01a6ae9a-16aa-45f2-8bff-32b5dc587e4492[cover]: https://github.com/user-attachments/assets/01a6ae9a-16aa-45f2-8bff-32b5dc587e44
448[discord-link]: https://discord.gg/sillytavern93[discord-link]: https://discord.gg/sillytavern
449[discord-shield]: https://img.shields.io/discord/1100685673633153084?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square
450[discord-shield-badge]: https://img.shields.io/discord/1100685673633153084?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge94[discord-shield-badge]: https://img.shields.io/discord/1100685673633153084?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge
.github/readme-zh_cn.md+19 -321
@@ -34,29 +34,9 @@ SillyTavern 于 2023 年 2 月作为 TavernAI 1.2.8 的一个分支开始,如
342. 我们不提供任何在线或托管服务,也不会以编程方式跟踪任何用户数据。342. 我们不提供任何在线或托管服务,也不会以编程方式跟踪任何用户数据。
353. SillyTavern 是一个由专注的 LLM 爱好者社区为您带来的充满激情的项目,并且将永远是免费和开源的。353. SillyTavern 是一个由专注的 LLM 爱好者社区为您带来的充满激情的项目,并且将永远是免费和开源的。
3636
37## 分支37## 我需要一台性能强大的电脑来运行 SillyTavern 吗?
3838
39SillyTavern 采用双分支进行开发,以确保所有用户都能获得流畅的使用体验。39硬件要求很低:任何可以运行 NodeJS 18 或更高版本的设备都可以运行它。如果您打算在本地计算机上进行 LLM 推理,我们建议使用至少具有 6GB VRAM 的 3000 系列 NVIDIA 显卡,但实际要求可能会根据模型和您使用的后端而有所不同。
40
41- `release` -🌟 **推荐给大多数用户。** 这是最稳定、最推荐的分支,只有在重大版本推送时才会更新。适合大多数用户使用。通常每月更新一次。
42- `staging` - ⚠️ **不建议随意使用。** 该分支拥有最新功能,但要谨慎,因为它随时可能崩溃。仅适用于高级用户和爱好者。每天更新数次。
43
44如果你不熟悉使用 git 命令行,或者不了解什么是分支,别担心!`release` 分支始终是您的首选。
45
46## 除了 SillyTavern,我还需要什么?
47
48由于 SillyTavern 只是一个界面,您需要接入一个 LLM 后端来进行推理。您可以使用 AI Horde 进行开箱即用的聊天。除此之外,我们还支持许多其他本地和基于云的 LLM 后端:OpenAI 兼容 API、KoboldAI、Tabby 等等。您可以在[文档](https://docs.sillytavern.app/usage/api-connections/)中阅读更多关于我们支持的 API 的信息。
49
50### 我需要一台性能强大的电脑来运行 SillyTavern 吗?
51
52硬件要求很低:任何可以运行 NodeJS 18 或更高版本的设备都可以运行它。如果您打算在本地计算机上进行 LLM 推理,我们建议使用至少具有 6GB VRAM 的 3000 系列 NVIDIA 显卡。有关更多详细信息,请查看您后端的文档。
53
54### 建议的后端(非附属)
55
56- [AI Horde](https://aihorde.net/) - 使用志愿者托管的模型。无需进一步设置。
57- [KoboldCpp](https://github.com/LostRuins/koboldcpp) - 社区最喜欢的在本地运行 GGUF 模型的工具。
58- [tabbyAPI](https://github.com/theroyallab/tabbyAPI) - 一款流行的、轻量级的、本地托管的 exl2 推理 API。
59- [OpenRouter](https://openrouter.ai) - 一个适用于许多云提供商(OpenAI、Claude、Meta Llama 等)以及流行社区模型的单一 API。
6040
61## 有问题或建议?41## 有问题或建议?
6242
@@ -67,9 +47,9 @@ SillyTavern 采用双分支进行开发,以确保所有用户都能获得流
6747
68或者直接与开发人员联系:48或者直接与开发人员联系:
6949
70- Discord: cohee, rossascends, wolfsblvt50* Discord: cohee, rossascends, wolfsblvt
71- Reddit: [/u/RossAscends](https://www.reddit.com/user/RossAscends/), [/u/sillylossy](https://www.reddit.com/user/sillylossy/), [u/Wolfsblvt](https://www.reddit.com/user/Wolfsblvt/)51* Reddit: [/u/RossAscends](https://www.reddit.com/user/RossAscends/), [/u/sillylossy](https://www.reddit.com/user/sillylossy/), [u/Wolfsblvt](https://www.reddit.com/user/Wolfsblvt/)
72- [提交 GitHub 问题](https://github.com/SillyTavern/SillyTavern/issues)52* [提交 GitHub 问题](https://github.com/SillyTavern/SillyTavern/issues)
7353
74### 我喜欢你的项目!我该如何贡献自己的力量?54### 我喜欢你的项目!我该如何贡献自己的力量?
7555
@@ -82,315 +62,33 @@ SillyTavern 采用双分支进行开发,以确保所有用户都能获得流
82<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">62<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">
83<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">63<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">
8464
85## 角色卡65## 安装
86
87SillyTavern 围绕“角色卡”的概念构建。角色卡是设定 LLM 行为的 Prompt 集合,是在 SillyTavern 中进行持久对话所必需的。它们的功能类似于 ChatGPT 的 GPTs 或 Poe 的 bots。角色卡的内容可以是任何东西:一个抽象的场景、一个为特定任务量身定制的助手、一个著名人物或一个虚构角色。
88
89要在不选择角色卡的情况下进行快速对话或仅测试 LLM 连接,只需在打开 SillyTavern 后在欢迎屏幕的输入栏中键入您的 Prompt 输入。这将创建一个空的“助手”角色卡,您可以稍后自定义。
90
91要大致了解如何定义角色卡,请参阅默认角色(Seraphina)或从“下载扩展和资源”菜单中下载选定的社区制作卡片。
92
93## 主要功能
94
95- 高级文本生成设置,包含许多社区制作的预设
96- 世界书支持:创建丰富的传说或节省角色卡上的 Token
97- 群聊:多机器人房间,供角色与您或彼此交谈
98- 丰富的 UI 自定义选项:主题颜色、背景图片、自定义 CSS 等
99- 用户角色:让 AI 了解一些关于您的信息,以获得更强的沉浸感
100- 内置 RAG 支持:将文档添加到您的聊天中供 AI 参考
101- 广泛的聊天命令子系统和自己的[脚本引擎](https://docs.sillytavern.app/usage/st-script/)
102
103## 扩展
104
105SillyTavern 支持扩展。
106
107- 角色情绪表达
108- 聊天记录自动摘要
109- 自动 UI 和聊天翻译
110- Stable Diffusion/FLUX/DALL-E 图像生成
111- AI 回复消息的文本转语音(通过 ElevenLabs、Silero 或操作系统的 TTS)
112- 网络搜索功能,为您的 Prompt 添加额外的现实世界背景信息
113- 更多扩展可从“下载扩展和资源”菜单中下载。
114
115有关如何使用它们的使用教程,请参阅[文档](https://docs.sillytavern.app/)。
116
117## ⌛ 安装
118
119### 🪟 Windows
120
121> \[!WARNING]
122>
123> - 请勿安装到任何受 Windows 控制的文件夹(Program Files、System32 等)中。
124> - 请勿以管理员权限运行 Start.bat
125> - 无法在 Windows 7 上安装,因为它无法运行 NodeJS 18.16
126
127#### 通过 Git 安装(推荐)
128
1291. 安装 [NodeJS](https://nodejs.org/en)(建议使用最新的 LTS 版本)
1302. 安装 [Git for Windows](https://gitforwindows.org/)
1313. 打开 Windows 资源管理器 (`Win+E`)
1324. 浏览或创建一个不受 Windows 控制或监控的文件夹(例如:C:\MySpecialFolder\)
1335. 通过点击顶部的“地址栏”,输入 `cmd`,然后按 Enter,在该文件夹内打开命令提示符。
1346. 弹出黑框(命令提示符)后,键入以下其中一项并按 Enter:
135
136- Release 分支:`git clone https://github.com/SillyTavern/SillyTavern -b release`
137- Staging 分支: `git clone https://github.com/SillyTavern/SillyTavern -b staging`
138
1397. 等待所有内容克隆完成后,双击 `Start.bat` 以使 NodeJS 安装其依赖项。
1408. 然后服务器将启动,SillyTavern 将在您的浏览器中弹出。
141
142#### 通过 GitHub Desktop 安装
143
144(这**仅**允许在 GitHub Desktop 中使用 git,如果您也想在命令行上使用 `git`,则还需要安装 [Git for Windows](https://gitforwindows.org/))
145
1461. 安装 [NodeJS](https://nodejs.org/en)(建议使用最新的 LTS 版本)
1472. 安装 [GitHub Desktop](https://central.github.com/deployments/desktop/desktop/latest/win32)
1483. 安装 GitHub Desktop 后,点击 `Clone a repository from the internet....`(注意:此步骤**无需**创建 GitHub 帐户)
1494. 在菜单中,点击 URL 选项卡,输入此 URL `https://github.com/SillyTavern/SillyTavern`,然后点击 Clone。您可以更改本地路径以更改 SillyTavern 的下载位置。
1505. 要打开 SillyTavern,请使用 Windows 资源管理器浏览到克隆存储库的文件夹。默认情况下,存储库将克隆到此处:`C:\Users\[您的 Windows 用户名]\Documents\GitHub\SillyTavern`
1516. 双击 `start.bat` 文件。(注意:文件名的 `.bat` 部分可能被您的操作系统隐藏,在这种情况下,它将显示为一个名为“`Start`”的文件。双击此文件以运行 SillyTavern)
1527. 双击后,应打开一个大的黑色命令控制台窗口,SillyTavern 将开始安装其运行所需的组件。
1538. 安装过程完成后,如果一切正常,命令控制台窗口应如下所示,并且您的浏览器中应打开一个 SillyTavern 选项卡:
1549. 连接到任何[支持的 API](https://docs.sillytavern.app/usage/api-connections/) 并开始聊天!
155
156### 🐧 Linux & 🍎 MacOS
157
158对于 MacOS / Linux,所有这些都将在终端中完成。
159
1601. 安装 git 和 nodeJS(具体方法取决于您的操作系统)
1612. 克隆仓库
162
163- Release 分支:`git clone https://github.com/SillyTavern/SillyTavern -b release`
164- Staging 分支: `git clone https://github.com/SillyTavern/SillyTavern -b staging`
165
1663. `cd SillyTavern` 导航到安装文件夹。
1674. 使用以下命令之一运行 `start.sh` 脚本:
168
169- `./start.sh`
170- `bash start.sh`
171
172## 🐋 通过 Docker 安装
173
174这些说明假定您已安装 Docker,能够访问命令行以安装容器,并熟悉其常规操作。
175
176### 使用 GitHub Container Registry
177
178#### Docker Compose (最简单)
179
180从 [GitHub 仓库](https://github.com/SillyTavern/SillyTavern/blob/release/docker/docker-compose.yml) 获取 `docker-compose.yml` 文件,并在文件所在目录中运行以下命令。这将从 GitHub Container Registry 中拉取最新的 release 镜像并启动容器,自动创建必要的卷。
181
182```shell
183docker-compose up
184```
185
186根据您的需求自定义 `docker-compose.yml` 文件。默认端口为 8000。如果您想使用环境变量调整服务器配置,请在此处阅读文档:[链接](https://docs.sillytavern.app/administration/config-yaml/#environment-variables)。
187
188#### Docker CLI (高级)
189
190您将需要两个强制性的目录映射和一个端口映射才能使 SillyTavern 正常运行。在命令中,替换以下位置中的选项:
191
192#### 容器变量
193
194##### 卷映射
195
196- `CONFIG_PATH` - SillyTavern 配置文件将存储在主机上的目录
197- `DATA_PATH` - SillyTavern 用户数据(包括角色)将存储在主机上的目录
198- `PLUGINS_PATH` - (可选) SillyTavern 服务器插件将存储在主机上的目录
199- `EXTENSIONS_PATH` - (可选) 全局 UI 扩展将存储在主机上的目录
200
201##### 端口映射
202
203- `PUBLIC_PORT` - 暴露流量的端口。这是强制性的,因为您将从其虚拟机容器外部访问实例。**在未实现单独的安全服务的情况下,请勿将其暴露给互联网。**
20466
205##### 附加设置67有关详细的安装说明,请访问我们的文档:
20668
207- `SILLYTAVERN_VERSION` - 在此 GitHub 页面的右侧,您会看到“Packages”。选择“sillytavern”包,您将看到镜像版本。镜像标签“latest”将使您与当前 release 保持同步。您还可以使用指向相应分支的每日镜像的“staging”标签。69* **[Windows 安装指南](https://docs.sillytavern.app/installation/windows/)**
20870* **[MacOS/Linux 安装指南](https://docs.sillytavern.app/installation/linuxmacos/)**
209#### 运行容器71* **[Android (Termux) 安装指南](https://docs.sillytavern.app/installation/android-(termux)/)**
21072* **[Docker 安装指南](https://docs.sillytavern.app/installation/docker/)**
2111. 打开您的命令行
2122. 在您要存储配置和数据文件的文件夹中运行以下命令:
213
214```bash
215SILLYTAVERN_VERSION="latest"
216PUBLIC_PORT="8000"
217CONFIG_PATH="./config"
218DATA_PATH="./data"
219PLUGINS_PATH="./plugins"
220EXTENSIONS_PATH="./extensions"
221
222docker run \
223 --name="sillytavern" \
224 -p "$PUBLIC_PORT:8000/tcp" \
225 -v "$CONFIG_PATH:/home/node/app/config:rw" \
226 -v "$DATA_PATH:/home/node/app/data:rw" \
227 -v "$EXTENSIONS_PATH:/home/node/app/public/scripts/extensions/third-party:rw" \
228 -v "$PLUGINS_PATH:/home/node/app/plugins:rw" \
229 ghcr.io/sillytavern/sillytavern:"$SILLYTAVERN_VERSION"
230```
231
232> 默认情况下,容器将在前台运行。如果要在后台运行它,请将 `-d` 标志添加到 `docker run` 命令中。
233
234### 自己构建镜像
235
236我们有一个关于在 Docker 中使用 SillyTavern 的综合指南[在此处](http://docs.sillytavern.app/installation/docker/),涵盖了 Windows、macOS 和 Linux 上的安装!如果您希望自己构建镜像,请阅读它。
237
238## ⚡ 通过 SillyTavern Launcher 安装
239
240SillyTavern Launcher 是一个安装向导,可帮助您进行多种选项的设置,包括为本地推理安装后端。
241
242### 对于 Windows 用户
243
2441. 在键盘上:按 **`WINDOWS + R`** 打开“运行”对话框。然后,运行以下命令安装 git:
245
246```shell
247cmd /c winget install -e --id Git.Git
248```
249
2502. 在键盘上:按 **`WINDOWS + E`** 打开文件资源管理器,然后导航到要安装启动器的文件夹。进入所需文件夹后,在地址栏中键入 `cmd` 并按 Enter。然后,运行以下命令:
251
252```shell
253git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher && start installer.bat
254```
255
256### 对于 Linux 用户
257
2581. 打开您喜欢的终端并安装 git
2592. 使用以下命令克隆 SillyTavern-Launcher:
260
261```shell
262git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
263```
264
2653. 使用以下命令启动 installer.sh:
266
267```shell
268chmod +x install.sh && ./install.sh
269```
270
2714. 安装后使用以下命令启动 launcher.sh:
272
273```shell
274chmod +x launcher.sh && ./launcher.sh
275```
276
277### 对于 Mac 用户
278
2791. 打开终端并使用以下命令安装 brew:
280
281```shell
282/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
283```
284
2852. 使用以下命令安装 git:
286
287```shell
288brew install git
289```
290
2913. 使用以下命令克隆 SillyTavern-Launcher:
292
293```shell
294git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
295```
296
2974. 使用以下命令启动 installer.sh:
298
299```shell
300chmod +x install.sh && ./install.sh
301```
302
3035. 安装后使用以下命令启动 launcher.sh:
304
305```shell
306chmod +x launcher.sh && ./launcher.sh
307```
308
309## 📱 通过 Termux 在 Android OS 上安装
310
311> \[!NOTE]
312> **SillyTavern 可以在 Android 设备上使用 Termux 原生运行,但我们不为此用例提供官方支持。**
313>
314> **请参阅 ArroganceComplex#2659 编写的本指南:**
315>
316> - <https://rentry.org/STAI-Termux>
317
318**不支持的平台:android arm LEtime-web。** 32 位 Android 需要一个无法通过 npm 安装的外部依赖项。使用以下命令安装它:`pkg install esbuild`。然后运行常规安装步骤。
319
320## 命令行参数
321
322您可以将命令行参数传递给 SillyTavern 服务器启动脚本,以覆盖 `config.yaml` 中的某些设置。
323
324### 示例
325
326```shell
327node server.js --port 8000 --listen false
328# 或
329npm run start -- --port 8000 --listen false
330# 或 (仅限 Windows)
331Start.bat --port 8000 --listen false
332```
333
334### 支持的参数
335
336> \[!TIP]
337> 所有参数都不是必需的。如果您不提供它们,SillyTavern 将使用 `config.yaml` 中的设置。
338
339| 选项 | 描述 | 类型 |
340| -------------------------------- | ---------------------------------------------- | ------- |
341| `--version` | 显示版本号 | boolean |
342| `--configPath` | 覆盖 config.yaml 文件的路径 | string |
343| `--dataRoot` | 数据存储的根目录 | string |
344| `--port` | 设置 SillyTavern 将在其下运行的端口 | number |
345| `--listen` | SillyTavern 将侦听所有网络接口 | boolean |
346| `--whitelist` | 启用白名单模式 | boolean |
347| `--basicAuthMode` | 启用基本身份验证 | boolean |
348| `--enableIPv4` | 启用 IPv4 协议 | boolean |
349| `--enableIPv6` | 启用 IPv6 协议 | boolean |
350| `--listenAddressIPv4` | 要侦听的特定 IPv4 地址 | string |
351| `--listenAddressIPv6` | 要侦听的特定 IPv6 地址 | string |
352| `--dnsPreferIPv6` | DNS 首选 IPv6 | boolean |
353| `--ssl` | 启用 SSL | boolean |
354| `--certPath` | 您的证书文件路径 | string |
355| `--keyPath` | 您的私钥文件路径 | string |
356| `--browserLaunchEnabled` | 自动在浏览器中启动 SillyTavern | boolean |
357| `--browserLaunchHostname` | 自动运行主机名 | string |
358| `--browserLaunchPort` | 覆盖自动运行的端口 | string |
359| `--browserLaunchAvoidLocalhost` | 在自动模式下避免使用 'localhost' 进行自动运行 | boolean |
360| `--corsProxy` | 启用 CORS 代理 | boolean |
361| `--requestProxyEnabled` | 为传出请求启用代理 | boolean |
362| `--requestProxyUrl` | 请求代理 URL(HTTP 或 SOCKS 协议) | string |
363| `--requestProxyBypass` | 请求代理绕过列表(以空格分隔的主机列表) | array |
364| `--disableCsrf` | 禁用 CSRF 保护(不推荐) | boolean |
365
366## 远程连接
367
368这通常适用于那些想在手机上使用 SillyTavern,而他们的电脑在同一 Wi-Fi 网络上运行 ST 服务器的人。但是,它也可以用于允许从任何地方进行远程连接。
369
370请在[文档](https://docs.sillytavern.app/usage/remoteconnections/)中阅读有关如何设置远程连接的详细指南。
371
372您可能还需要配置 SillyTavern 用户配置文件(可选密码保护):[用户](https://docs.sillytavern.app/administration/multi-user/)。
37373
374## 许可证和致谢74## 许可证和致谢
37575
376**本程序的分发是希望它能有用,但不提供任何保证;甚至没有对适销性或特定用途适用性的默示保证。有关更多详细信息,请参阅 GNU Affero 通用公共许可证。**76**本程序的分发是希望它能有用,但不提供任何保证;甚至没有对适销性或特定用途适用性的默示保证。有关更多详细信息,请参阅 GNU Affero 通用公共许可证。**
37777
378- [TavernAI](https://github.com/TavernAI/TavernAI) 1.2.8 by Humi: MIT 许可证78* [TavernAI](https://github.com/TavernAI/TavernAI) 1.2.8 by Humi: MIT 许可证
379- CncAnon 的 TavernAITurbo mod 的部分内容经许可使用79* CncAnon 的 TavernAITurbo mod 的部分内容经许可使用
380- 视觉小说模式的灵感来自 PepperTaco 的工作 (<https://github.com/peppertaco/Tavern/>)80* 视觉小说模式的灵感来自 PepperTaco 的工作 (<https://github.com/peppertaco/Tavern/>)
381- Noto Sans 字体 by Google (OFL 许可证)81* Noto Sans 字体 by Google (OFL 许可证)
382- 图标主题 by Font Awesome <https://fontawesome.com> (图标: CC BY 4.0, 字体: SIL OFL 1.1, 代码: MIT 许可证)82* 图标主题 by Font Awesome <https://fontawesome.com> (图标: CC BY 4.0, 字体: SIL OFL 1.1, 代码: MIT 许可证)
383- 默认内容由 @OtisAlejandro (Seraphina 角色和世界书) 和 @kallmeflocc (10K Discord 用户庆祝背景) 提供83* 默认内容由 @OtisAlejandro (Seraphina 角色和世界书) 和 @kallmeflocc (10K Discord 用户庆祝背景) 提供
384- Docker 指南由 [@mrguymiah](https://github.com/mrguymiah) 和 [@Bronya-Rand](https://github.com/Bronya-Rand) 提供84* Docker 指南由 [@mrguymiah](https://github.com/mrguymiah) 和 [@Bronya-Rand](https://github.com/Bronya-Rand) 提供
385- kokoro-js 库由 [@hexgrad](https://github.com/hexgrad) 提供 (Apache-2.0 许可证)85* kokoro-js 库由 [@hexgrad](https://github.com/hexgrad) 提供 (Apache-2.0 许可证)
386- 中文翻译由 [@XXpE3](https://github.com/XXpE3) 完成,中文 ISSUES 可以联系 @XXpE3
38786
388## 主要贡献者87## 主要贡献者
38988
390[![Contributors](https://contrib.rocks/image?repo=SillyTavern/SillyTavern)](https://github.com/SillyTavern/SillyTavern/graphs/contributors)89[![Contributors](https://contrib.rocks/image?repo=SillyTavern/SillyTavern)](https://github.com/SillyTavern/SillyTavern/graphs/contributors)
39190
392<!-- LINK GROUP -->91<!-- LINK GROUP -->
393
394[cover]: https://github.com/user-attachments/assets/01a6ae9a-16aa-45f2-8bff-32b5dc587e4492[cover]: https://github.com/user-attachments/assets/01a6ae9a-16aa-45f2-8bff-32b5dc587e44
395[discord-link]: https://discord.gg/sillytavern93[discord-link]: https://discord.gg/sillytavern
396[discord-shield-badge]: https://img.shields.io/discord/1100685673633153084?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge94[discord-shield-badge]: https://img.shields.io/discord/1100685673633153084?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge
.github/readme-zh_tw.md+11 -298
@@ -10,7 +10,7 @@
10[English](readme.md) | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | 繁體中文 | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md) | [한국어](readme-ko_kr.md)10[English](readme.md) | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | 繁體中文 | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md) | [한국어](readme-ko_kr.md)
1111
12[![GitHub 星標](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)12[![GitHub 星標](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
13[![GitHub 分支](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)13[![GitHub 分支](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/forks)
14[![GitHub 問題](https://img.shields.io/github/issues/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/issues)14[![GitHub 問題](https://img.shields.io/github/issues/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/issues)
15[![GitHub 拉取請求](https://img.shields.io/github/issues-pr/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/pulls)15[![GitHub 拉取請求](https://img.shields.io/github/issues-pr/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/pulls)
1616
@@ -26,7 +26,7 @@ SillyTavern 提供一個統一的前端介面,整合多種大型語言模型
2626
27SillyTavern(簡稱 ST)是一款本地安裝的使用者介面,讓您能與大型語言模型(LLM)、影像生成引擎以及語音合成模型互動的前端。27SillyTavern(簡稱 ST)是一款本地安裝的使用者介面,讓您能與大型語言模型(LLM)、影像生成引擎以及語音合成模型互動的前端。
2828
29SillyTavern 起源於 2023 年 2 月,作為 TavernAI 1.2.8 的分支版本發展至今。目前已有超過 100 位貢獻者,並擁有超過兩年的獨立開發歷史。如今,它已成為 AI 愛好者中備受推崇的軟體之一。29SillyTavern 起源於 2023 年 2 月,作為 TavernAI 1.2.8 的分支版本發展至今。目前已有超過 200 位貢獻者,並擁有超過兩年的獨立開發歷史。如今,它已成為 AI 愛好者中備受推崇的軟體之一。
3030
31## 我們的願景31## 我們的願景
3232
@@ -34,29 +34,9 @@ SillyTavern 起源於 2023 年 2 月,作為 TavernAI 1.2.8 的分支版本發
342. 我們不提供任何線上或託管服務,也不會程式化追蹤任何使用者數據。342. 我們不提供任何線上或託管服務,也不會程式化追蹤任何使用者數據。
353. SillyTavern 是由一群熱衷於 LLM 的開發者社群所打造的專案,並將永遠保持免費與開源。353. SillyTavern 是由一群熱衷於 LLM 的開發者社群所打造的專案,並將永遠保持免費與開源。
3636
37## 分支介紹37## 我需要高效能電腦才能運行 SillyTavern 嗎?
3838
39SillyTavern 採用雙分支開發模式,確保為所有使用者提供流暢的體驗。39SillyTavern 的硬體需求相當低。任何能夠運行 NodeJS 18 或更高版本的設備都可以執行。若您打算在本地機器上進行 LLM 推理,我們建議使用擁有至少 6GB VRAM 的 3000 系列 NVIDIA 顯示卡,但實際需求可能因模型和您使用的後端而異。
40
41* `release`(穩定版):🌟 **推薦給大部分的使用者使用。** 此分支最為穩定,僅在主要版本發布時更新。適合大多數人,通常每月更新一次。
42* `staging`(開發版):⚠️ **不建議普通使用者使用。** 此分支包含最新功能,但可能隨時出現問題。適合進階使用者與愛好者,每日多次更新。
43
44如果您不熟悉 git CLI 或對分支概念不清楚,請放心,對您來說,`release`(穩定版)分支永遠是首選。
45
46## 使用 SillyTavern 需要什麼?
47
48由於 SillyTavern 僅是一個介面,您需要一個 LLM 後端來提供推理能力。您可以使用 AI Horde 以立即開始聊天。此外,我們支持許多其他本地和雲端 LLM 後端,例如 OpenAI 兼容 API、KoboldAI、Tabby 等。更多支持的 API 資訊,請參閱 [常見問題](https://docs.sillytavern.app/usage/api-connections/)。
49
50### 我需要高效能電腦才能運行 SillyTavern 嗎?
51
52SillyTavern 的硬體需求相當低。任何能夠運行 NodeJS 18 或更高版本的設備都可以執行。若您打算在本地機器上進行 LLM 推理,我們建議使用擁有至少 6GB VRAM 的 3000 系列 NVIDIA 顯示卡。更多詳細資訊,請參考您使用的後端文檔。
53
54### 推薦後端(僅為推薦,非官方合作和隸屬關係)
55
56* [AI Horde](https://aihorde.net/):使用志願者託管的模型,無需進一步設定
57* [KoboldCpp](https://github.com/LostRuins/koboldcpp):社群推崇的選擇,可在本地運行 GGUF 模型
58* [tabbyAPI](https://github.com/theroyallab/tabbyAPI):一個流行且輕量的本地託管 exl2 推理 API
59* [OpenRouter](https://openrouter.ai):提供多個雲端 LLM 提供商(如 OpenAI、Claude、Meta Llama 等)及熱門社群模型的單一 API
6040
61## 有任何問題或建議?41## 有任何問題或建議?
6242
@@ -82,287 +62,19 @@ SillyTavern 的硬體需求相當低。任何能夠運行 NodeJS 18 或更高版
82<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">62<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">
83<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">63<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">
8464
85## 角色卡65## 安裝指南
86
87SillyTavern 的核心概念是「角色卡」(Character Cards)。角色卡是一組設定 LLM 行為的提示詞,用於 SillyTavern 中進行持續性對話。功能類似於 ChatGPT 的 GPT 或 Poe 的聊天機器人。角色卡的內容可以是任何形式:抽象場景、針對特定任務設計的助手、知名人物,或者虛構角色。
88
89角色卡中唯一必填的項目是名稱欄位。若想與語言模型開始一般對話,您只需創建一個名稱為「Assistant」的新卡片,其餘欄位皆可保持空白。若希望進行更具主題性的對話,則可以提供語言模型背景資訊、行為模式、寫作風格以及特定情境來啟動聊天。
90
91如果僅想進行快速對話而不選擇角色卡片,或想測試 LLM 的連線,則可在開啟 SillyTavern 後,於歡迎頁面的輸入欄位中直接輸入您的提示內容。請注意,這類對話是暫時的,不會被永久保存。
92
93若想了解如何設定角色卡,可參考預設角色(如 Seraphina)或從「下載擴充功能 & 資源」(Download Extensions & Assets)選單中下載社群製作的角色卡。
94
95## 核心功能
96
97* 進階文本生成設定:內含許多社群製作的預設設定
98* 支援世界資訊(World Info):創建豐富的背景故事,或節省角色卡中的 Token(符元)使用
99* 群組聊天:多角色聊天室,可讓角色與您或彼此對話
100* 豐富的 UI 自定義選項:主題顏色、背景圖片、自定義 CSS 等
101* 使用者設定:讓 AI 更了解您並提升沉浸感
102* 內建 RAG 支持:可將文檔加入對話,供 AI 參考
103* 強大的聊天指令子系統:內含 [腳本引擎(Scripting Engine)](https://docs.sillytavern.app/usage/st-script/)
104
105## 擴充功能
106
107SillyTavern 支持多種擴充功能。
108
109* 角色情感表達:使用視覺圖片(立繪)呈現情緒表達
110* 聊天記錄自動摘要
111* 自動化介面與聊天翻譯
112* 穩定擴散(Stable Diffusion)、FLUX 和 DALL-E 的影像生成整合
113* 語音合成:AI 回應的訊息可使用 ElevenLabs、Silero 或系統 TTS 語音合成
114* 網頁搜尋功能:為提示詞添加真實世界的上下文資訊
115* 更多擴展:可從「下載擴充功能 & 資源」(Download Extensions & Assets)選單中下載
116
117想了解如何使用這些擴充功能,請參考:[官方說明文件](https://docs.sillytavern.app/)
118
119# ⌛ 安裝指南
120
121> \[!WARNING]
122>
123> * 請勿將程式安裝到 Windows 的系統控制資料夾(如 Program Files、System32 等)
124> * 請勿以管理員權限執行 Start.bat
125> * 無法在 Windows 7 系統上安裝,因為它無法執行 NodeJS 18.16
126
127## 🪟 Windows
128
129### 使用 Git 安裝
130
1311. 安裝 [NodeJS](https://nodejs.org/en)(建議使用最新的 LTS 版本)
1322. 安裝 [Git for Windows](https://gitforwindows.org/)
1333. 打開 Windows 檔案總管(`Win+E`)
1344. 創建/使用一個不受 Windows 系統控制或監控的資料夾(例如:C:\MySpecialFolder\)
1355. 在該資料夾內開啟命令提示字元(Command Prompt):點擊地址欄,輸入 `cmd` 並按下 Enter
1366. 當命令提示字元黑框彈出時,輸入以下其中一條指令後,按下 Enter:
137
138* 安裝 Release(穩定版)分支:`git clone https://github.com/SillyTavern/SillyTavern -b release`
139* 安裝 Staging(開發板)分支:`git clone https://github.com/SillyTavern/SillyTavern -b staging`
140
1417. 當程式碼下載完成後,雙擊 `Start.bat`,NodeJS 將自動安裝所需的依賴項
1428. 本地伺服器啟動後,SillyTavern 將自動在您的瀏覽器中打開
143
144### 使用 GitHub Desktop 安裝
145
146(此方式僅允許通過 GitHub Desktop 使用 git。如果您也希望在命令列中使用 `git`,則需額外安裝 [Git for Windows](https://gitforwindows.org/))
147
148 1. 安裝 [NodeJS](https://nodejs.org/en)(建議使用最新的 LTS 版本)
149 2. 安裝 [GitHub Desktop](https://central.github.com/deployments/desktop/desktop/latest/win32)
150 3. 安裝完成後,打開 GitHub Desktop,點擊 `Clone a repository from the internet....` (注意:此步驟 **無需創建 GitHub 帳號**。)
151 4. 在彈出選單中,點擊「URL」選項,輸入此網址:`https://github.com/SillyTavern/SillyTavern`,然後點擊「Clone」。您可以更改「Local path」來選擇 SillyTavern 的下載位置
152 6. 若想開啟 SillyTavern,需使用 Windows 檔案總管以進入您複製儲存庫的資料夾。預設位置為:`C:\Users\[您的 Windows 使用者名稱]\Documents\GitHub\SillyTavern`
153 7. 雙擊 `start.bat` 文件。(請注意:若您的作業系統隱藏了 `.bat` 副檔名,該文件可能顯示為「`Start`」。這就是您需要雙擊運行的文件。)
154 8. 雙擊後,將會彈出一個大型黑色的命令提示字元視窗,SillyTavern 會開始安裝其運行所需的文件與依賴
155 9. 安裝完成後,若一切正常,命令提示字元視窗應顯示運行中的訊息,且您的瀏覽器會自動打開 SillyTavern 頁籤
156 10. 連接到任何 SillyTavern [支援的 APIs](https://docs.sillytavern.app/usage/api-connections/) 並開始聊天吧!
157
158## 🐧 Linux & 🍎 MacOS
159
160對於 MacOS 和 Linux 系統,所有操作都將在終端機(Terminal)中完成。
161
1621. 安裝 git 和 NodeJS(具體方法因操作系統而異)
1632. 複製儲存庫(Clone the repo):
164
165* 安裝 Release(穩定版)分支:`git clone https://github.com/SillyTavern/SillyTavern -b release`
166* 安裝 Staging(開發板)分支:`git clone https://github.com/SillyTavern/SillyTavern -b staging`
167
1683. 使用命令 `cd SillyTavern` 以進入安裝資料夾
1694. 使用以下其中一條命令,以執行 `start.sh` 腳本:
170
171* `./start.sh`
172* `bash start.sh`
173
174## ⚡ 使用 SillyTavern Launcher 安裝
175
176SillyTavern Launcher 是一個安裝嚮導,協助您設定多種選項,包括安裝本地推理(inference)的後端。
177
178### 對於 Windows 使用者
179
1801. 在鍵盤上按下 **`WINDOWS + R`** 打開「執行」對話框,然後輸入以下指令以安裝 git:
181
182```shell
183cmd /c winget install -e --id Git.Git
184```
185
1862. 在鍵盤上按下 **`WINDOWS + E`** 打開檔案總管,導航至您想要安裝 Launcher 的資料夾。在目標資料夾的地址欄輸入 `cmd` 並按下 Enter。接著執行以下命令:
187
188```shell
189git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher && start installer.bat
190```
191
192### 對於 Linux 使用者
193
1941. 打開您喜歡的終端機(Terminal),安裝 git
1952. 使用以下指令以複製 Sillytavern-Launcher:
196
197```shell
198git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
199```
200
2013. 執行安裝腳本(installer.sh):
202
203```shell
204chmod +x install.sh && ./install.sh
205```
206
2074. 安裝完成後,執行啟動腳本(launcher.sh):
208
209```shell
210chmod +x launcher.sh && ./launcher.sh
211```
21266
213### 對於 Mac 使用者67有關詳細的安裝說明,請訪問我們的文檔:
21468
2151. 打開終端機(Terminal),並使用以下指令安裝 Homebrew:69* **[Windows 安裝指南](https://docs.sillytavern.app/installation/windows/)**
21670* **[MacOS/Linux 安裝指南](https://docs.sillytavern.app/installation/linuxmacos/)**
217```shell71* **[Android (Termux) 安裝指南](https://docs.sillytavern.app/installation/android-(termux)/)**
218/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"72* **[Docker 安裝指南](https://docs.sillytavern.app/installation/docker/)**
219```
220
2212. 使用 Homebrew 以安裝 git:
222
223```shell
224brew install git
225```
226
2273. 使用以下指令以複製 Sillytavern-Launcher:
228
229```shell
230git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
231```
232
2334. 執行安裝腳本(installer.sh):
234
235```shell
236chmod +x install.sh && ./install.sh
237```
238
2395. 安裝完成後,執行啟動腳本(launcher.sh):
240
241```shell
242chmod +x launcher.sh && ./launcher.sh
243```
244
245## 🐋 使用 Docker 安裝
246
247以下指南已假設您安裝 Docker,能夠訪問命令列進行容器安裝,並熟悉 Docker 的基本使用。
248
249### 自行構建映像
250
251我們提供了一份完整的 [SillyTavern Docker 使用指南](http://docs.sillytavern.app/installation/docker/)。該指南涵蓋了 Windows、macOS 和 Linux 的安裝過程。若您希望自行構建映像,建議先閱讀該文檔。
252
253### 使用 GitHub 容器註冊表(最簡易的方式)
254
255您需要設定兩個必要的目錄映射(directory mappings)和一個端口映射(port mapping)來使 SillyTavern 正常運行。在執行指令時,請將以下佔位符替換為您的實際配置:
256
257#### 容器變數
258
259##### 目錄映射(Volume Mappings)
260
261* [config]:用於存放 SillyTavern 設定文件的本地資料夾
262* [data]:用於存放 SillyTavern 使用者數據(包括角色)的本地資料夾
263* [plugins](可選):用於存放 SillyTavern 擴充功能的本地資料夾
264
265##### 端口映射(Port Mappings)
266
267* [PublicPort]:對外流量的訪問端口。這是必需的,因為您將從虛擬機容器外部訪問實例。除非實施了額外的安全服務,否則請勿將此端口暴露於網路
268
269##### 其他設定(Additional Settings)
270
271* [DockerNet]:容器應連接的 Docker 網路。如果您不熟悉此概念,請參閱 [Docker 官方說明文件](https://docs.docker.com/reference/cli/docker/network/)
272* [version]:在 GitHub 頁面的右側,您可以找到「Packages」。選擇「sillytavern」包,然後查看映像版本。「latest」標籤會使您保持與當前版本同步。您也可以選擇「staging」或「release」標籤,但這可能不適用於依賴擴充功能的使用者,因為擴充功能可能需要時間進行更新
273
274#### 安裝命令
275
2761. 打開命令列(Command Line)
2772. 執行以下指令:
278
279`docker create --name='sillytavern' --net='[DockerNet]' -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'`
280
281> 請注意:默認的監聽端口為 8000。如果您在設定文件中更改了此端口,請務必使用適當的端口號
282
283## 📱 於 Android 系統中使用 Termux 安裝
284
285> \[!NOTE]
286> **雖然您可以在 Android 設備上使用 Termux 直接運行 SillyTavern,但這不在我們的官方支持範圍內。**
287>
288> **請參閱 ArroganceComplex#2659 所提供的指南:**
289>
290> * <https://rentry.org/STAI-Termux>
291
292**不支援:Android ARM LEtime-web。** 32 位 Android 系統需要額外的依賴項,這無法通過 npm 安裝。請使用以下命令安裝:`pkg install esbuild`。完成後,請按照普通的安裝步驟進行操作
293
294## API 金鑰管理
295
296SillyTavern 將您的 API 金鑰(Keys)保存在使用者數據目錄中的 `secrets.json` 文件內(默認路徑為`/data/default-user/secrets.json`)
297
298默認情況下,API 金鑰在您保存並重新載入頁面後,將不會自介面中顯示
299
300如需啟用查看金鑰功能:
301
3021. 在 `config.yaml` 文件中,將 `allowKeysExposure` 的「值」設為 `true`
3032. 重新啟動 SillyTavern 伺服器
3043. 點擊 API 連線頁面右下角的「查看隱藏的 API 金鑰(View hidden API keys)」超連結
305
306## 命令列參數(Command-line Arguments)
307
308您可以在啟動 SillyTavern 伺服器時傳遞命令列參數,以覆蓋 `config.yaml` 文件中的某些設定。
309
310### 範例
311
312```shell
313node server.js --port 8000 --listen false
314# or
315npm run start -- --port 8000 --listen false
316# or(僅適用於 Windows)
317Start.bat --port 8000 --listen false
318```
319
320### Supported arguments
321
322| Option | Description | Type |
323|----------------------------------|---------------------------------------------|---------|
324| `--version` | 顯示版本序號 | boolean |
325| `--enableIPv6` | 啟用 IPv6 | boolean |
326| `--enableIPv4` | 啟用 IPv4 | boolean |
327| `--port` | 設定 SillyTavern 運行的端口。 | number |
328| `--dnsPreferIPv6` | 偏好使用 IPv6 解析 DNS。 | boolean |
329| `--browserLaunchEnabled` | 自動在瀏覽器中啟動 SillyTavern。 | boolean |
330| `--browserLaunchHostname` | 自動啟動時的主機名稱,通常建議保持為 'auto' | string |
331| `--browserLaunchPort` | 覆蓋自動啟動的端口設定 | string |
332| `--browserLaunchAvoidLocalhost` | 在自動模式下避免使用 'localhost' | boolean |
333| `--listen` | SillyTavern 是否可監聽所有網路接口。 | boolean |
334| `--corsProxy` | 啟用 CORS 代理。 | boolean |
335| `--disableCsrf` | 停用 CSRF 保護 | boolean |
336| `--ssl` | 啟用 SSL | boolean |
337| `--certPath` | 設定您證書文件的路徑 | string |
338| `--keyPath` | 設定您私人金鑰文件的路徑 | string |
339| `--whitelist` | 啟用白名單模式 | boolean |
340| `--dataRoot` | 設定數據儲存的根目錄 | string |
341| `--basicAuthMode` | 啟用基本身份驗證模式 | boolean |
342| `--requestProxyEnabled` | 啟用代理以處理外部請求 | boolean |
343| `--requestProxyUrl` | 設定請求代理的 URL(支持 HTTP 或 SOCKS 協議)| string |
344| `--requestProxyBypass` | 請求代理的例外主機清單(主機列表需以空格分隔)| array |
345
346## 遠端連線
347
348遠端連線功能最常用於希望在手機上使用 SillyTavern 的使用者。此時伺服器將由同一 Wi-Fi 網路上的 PC 運行。不過,您也可以設定來自其他網路的遠端連線。
349
350詳細設定指南請參閱 [官方說明文件](https://docs.sillytavern.app/usage/remoteconnections/)。
351
352您還可以選擇設定 SillyTavern 的使用者檔案,並開啟密碼保護(可選):[使用者設定指南](https://docs.sillytavern.app/installation/st-1.12.0-migration-guide/#users)。
353
354## 遇到任何效能問題?
355
3561. 在「使用者設定」選單(設定介面主題)中,禁用模糊效果(Blur Effect),並開啟「減少動畫效果」(Reduced Motion)
3572. 若使用響應串流傳輸,請將串流的 FPS 設定為較低的值(建議設定為 10-15 FPS)
3583. 確保瀏覽器已啟用 GPU 加速以進行渲染
35973
360## 授權與致謝74## 授權與致謝
36175
362**本程式(SillyTavern)的發布是基於其可能對使用者有所幫助的期許,但不提供任何形式的保證;包括但不限於對可銷售性(marketability)或特定用途適用性的隱含保證。如需更多詳情,請參閱 GNU Affero 通用公共許可證。**76**本程式(SillyTavern)的發布是基於其可能對使用者有所幫助的期許,但不提供任何形式的保證;包括但不限於對可銷售性(marketability)或特定用途適用性的隱含保證。如需更多詳情,請參閱 GNU Affero 通用公共許可證。**
36377
364This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
365
366* [TavernAI](https://github.com/TavernAI/TavernAI) 1.2.8 由 Humi 提供:MIT 許可78* [TavernAI](https://github.com/TavernAI/TavernAI) 1.2.8 由 Humi 提供:MIT 許可
367* 經授權使用部分來自 CncAnon 的 TavernAITurbo 模組79* 經授權使用部分來自 CncAnon 的 TavernAITurbo 模組
368* 視覺小說模式(Visual Novel Mode)的靈感,來源於 PepperTaco 的貢獻(<https://github.com/peppertaco/Tavern/>)80* 視覺小說模式(Visual Novel Mode)的靈感,來源於 PepperTaco 的貢獻(<https://github.com/peppertaco/Tavern/>)
@@ -370,6 +82,7 @@ This program is distributed in the hope that it will be useful, but WITHOUT ANY
370* 主題圖示由 Font Awesome <https://fontawesome.com> 提供(圖示:CC BY 4.0,字體:SIL OFL 1.1,程式碼:MIT 許可)82* 主題圖示由 Font Awesome <https://fontawesome.com> 提供(圖示:CC BY 4.0,字體:SIL OFL 1.1,程式碼:MIT 許可)
371* 預設資源來源於 @OtisAlejandro(包含角色 Seraphina 與知識書)與 @kallmeflocc(SillyTavern 官方 Discord 伺服器成員突破 10K 的慶祝背景)83* 預設資源來源於 @OtisAlejandro(包含角色 Seraphina 與知識書)與 @kallmeflocc(SillyTavern 官方 Discord 伺服器成員突破 10K 的慶祝背景)
372* Docker 安裝指南由 [@mrguymiah](https://github.com/mrguymiah) 和 [@Bronya-Rand](https://github.com/Bronya-Rand) 編寫84* Docker 安裝指南由 [@mrguymiah](https://github.com/mrguymiah) 和 [@Bronya-Rand](https://github.com/Bronya-Rand) 編寫
85* kokoro-js 函式庫由 [@hexgrad](https://github.com/hexgrad) 提供 (Apache-2.0 許可)
37386
374## 主要貢獻者87## 主要貢獻者
37588
.github/readme.md+8 -330
@@ -31,29 +31,9 @@ Beginning in February 2023 as a fork of TavernAI 1.2.8, SillyTavern now has over
312. We do not provide any online or hosted services, nor programmatically track any user data.312. We do not provide any online or hosted services, nor programmatically track any user data.
323. SillyTavern is a passion project brought to you by a dedicated community of LLM enthusiasts, and will always be free and open sourced.323. SillyTavern is a passion project brought to you by a dedicated community of LLM enthusiasts, and will always be free and open sourced.
3333
34## Branches34## Do I need a powerful PC to run SillyTavern?
3535
36SillyTavern is being developed using a two-branch system to ensure a smooth experience for all users.36The hardware requirements are minimal: it will run on anything that can run NodeJS 18 or higher. If you intend to do LLM inference on your local machine, we recommend a 3000-series NVIDIA graphics card with at least 6GB of VRAM, but actual requirements may vary depending on the model and backend you choose to use.
37
38* `release` -🌟 **Recommended for most users.** This is the most stable and recommended branch, updated only when major releases are pushed. It's suitable for the majority of users. Typically updated once a month.
39* `staging` - ⚠️ **Not recommended for casual use.** This branch has the latest features, but be cautious as it may break at any time. Only for power users and enthusiasts. Updates several times daily.
40
41If you're not familiar with using the git CLI or don't understand what a branch is, don't worry! The release branch is always the preferable option for you.
42
43## What do I need other than SillyTavern?
44
45Since SillyTavern is only an interface, you will need access to an LLM backend to provide inference. You can use AI Horde for instant out-of-the-box chatting. Aside from that, we support many other local and cloud-based LLM backends: OpenAI-compatible API, KoboldAI, Tabby, and many more. You can read more about our supported APIs in [the Docs](https://docs.sillytavern.app/usage/api-connections/).
46
47### Do I need a powerful PC to run SillyTavern?
48
49The hardware requirements are minimal: it will run on anything that can run NodeJS 18 or higher. If you intend to do LLM inference on your local machine, we recommend a 3000-series NVIDIA graphics card with at least 6GB of VRAM. Check your backend's documentation for more details.
50
51### Suggested Backends (not affiliated)
52
53* [AI Horde](https://aihorde.net/) - use models hosted by volunteers. Requires no further setup
54* [KoboldCpp](https://github.com/LostRuins/koboldcpp) - a community's favorite for running GGUF models locally
55* [tabbyAPI](https://github.com/theroyallab/tabbyAPI) - a popular, lightweight, locally-hosted exl2 inference API
56* [OpenRouter](https://openrouter.ai) - a single API for many cloud providers (OpenAI, Claude, Meta Llama, etc.) as well as popular community models.
5737
58## Questions or suggestions?38## Questions or suggestions?
5939
@@ -79,316 +59,14 @@ Or get in touch with the developers directly:
79<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">59<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">
80<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">60<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">
8161
82## Character Cards62## Installation
83
84SillyTavern is built around the concept of "character cards". A character card is a collection of prompts that set the behavior of the LLM and is required to have persistent conversations in SillyTavern. They function similarly to ChatGPT's GPTs or Poe's bots. The content of a character card can be anything: an abstract scenario, an assistant tailored for a specific task, a famous personality or a fictional character.
85
86To have a quick conversation without selecting a character card or to just test the LLM connection, simply type your prompt input into the input bar on the Welcome Screen after opening SillyTavern. This will create an empty "Assistant" character card that you can customize later.
87
88To get a general idea on how to define character cards, see the default character (Seraphina) or download selected community-made cards from the "Download Extensions & Assets" menu.
89
90## Key Features
91
92* Advanced text generation settings with many community-made presets
93* World Info support: create rich lore or save tokens on your character card
94* Group chats: multi-bot rooms for characters to talk to you and/or each other
95* Rich UI customization options: theme colors, background images, custom CSS, and more
96* User personas: let the AI know a bit about you for greater immersion
97* Built-in RAG support: add documents to your chats for the AI to reference
98* Extensive chat commands subsystem and own [scripting engine](https://docs.sillytavern.app/usage/st-script/)
99
100## Extensions
101
102SillyTavern has extensibility support.
103
104* Character emotional expressions (sprites)
105* Auto-Summary of the chat history
106* Automatic UI and chat translation
107* Stable Diffusion/FLUX/DALL-E image generation
108* Text-to-speech for AI response messages (via ElevenLabs, Silero, or the OS's System TTS)
109* Web Search capabilities for adding additional real world context to your prompts
110* Many more are available to download from the "Download Extensions & Assets" menu.
111
112Tutorials on how to use them can be found in the [Docs](https://docs.sillytavern.app/).
113
114## ⌛ Installation
115
116### 🪟 Windows
117
118> \[!WARNING]
119>
120> * DO NOT INSTALL INTO ANY WINDOWS CONTROLLED FOLDER (Program Files, System32, etc).
121> * DO NOT RUN START.BAT WITH ADMIN PERMISSIONS
122> * INSTALLATION ON WINDOWS 7 IS IMPOSSIBLE AS IT CAN NOT RUN NODEJS 18.16
123
124#### Installing via Git (recommended)
125
1261. Install [NodeJS](https://nodejs.org/en) (latest LTS version is recommended)
1272. Install [Git for Windows](https://gitforwindows.org/)
1283. Open Windows Explorer (`Win+E`)
1294. Browse to or Create a folder that is not controlled or monitored by Windows. (ex: C:\MySpecialFolder\)
1305. Open a Command Prompt inside that folder by clicking in the 'Address Bar' at the top, typing `cmd`, and pressing Enter.
1316. Once the black box (Command Prompt) pops up, type ONE of the following into it and press Enter:
132
133* for Release Branch: `git clone https://github.com/SillyTavern/SillyTavern -b release`
134* for Staging Branch: `git clone https://github.com/SillyTavern/SillyTavern -b staging`
135
1367. Once everything is cloned, double-click `Start.bat` to make NodeJS install its requirements.
1378. The server will then start, and SillyTavern will pop up in your browser.
138
139#### Installing via GitHub Desktop
140
141(This allows git usage **only** in GitHub Desktop, if you want to use `git` on the command line too, you also need to install [Git for Windows](https://gitforwindows.org/))
142
143 1. Install [NodeJS](https://nodejs.org/en) (latest LTS version is recommended)
144 2. Install [GitHub Desktop](https://central.github.com/deployments/desktop/desktop/latest/win32)
145 3. After installing GitHub Desktop, click on `Clone a repository from the internet....` (Note: You **do NOT need** to create a GitHub account for this step)
146 4. On the menu, click the URL tab, enter this URL `https://github.com/SillyTavern/SillyTavern`, and click Clone. You can change the Local path to change where SillyTavern is going to be downloaded.
147 6. To open SillyTavern, use Windows Explorer to browse into the folder where you cloned the repository. By default, the repository will be cloned here: `C:\Users\[Your Windows Username]\Documents\GitHub\SillyTavern`
148 7. Double-click on the `start.bat` file. (Note: the `.bat` part of the file name might be hidden by your OS, in that case, it will look like a file called "`Start`". This is what you double-click to run SillyTavern)
149 8. After double-clicking, a large black command console window should open and SillyTavern will begin to install what it needs to operate.
150 9. After the installation process, if everything is working, the command console window should look like this and a SillyTavern tab should be open in your browser:
151 10. Connect to any of the [supported APIs](https://docs.sillytavern.app/usage/api-connections/) and start chatting!
152
153### 🐧 Linux & 🍎 MacOS
154
155For MacOS / Linux all of these will be done in a Terminal.
156
1571. Install git and nodeJS (the method for doing this will vary depending on your OS)
1582. Clone the repo
159
160* for Release Branch: `git clone https://github.com/SillyTavern/SillyTavern -b release`
161* for Staging Branch: `git clone https://github.com/SillyTavern/SillyTavern -b staging`
162
1633. `cd SillyTavern` to navigate into the install folder.
1644. Run the `start.sh` script with one of these commands:
165
166* `./start.sh`
167* `bash start.sh`
168
169## 🐋 Installing via Docker
170
171These instructions assume you have installed Docker, are able to access your command line for the installation of containers, and familiar with their general operation.
172
173### Using the GitHub Container Registry
174
175#### Docker Compose (easiest)
176
177Grab the `docker-compose.yml` file from the [GitHub Repository](https://github.com/SillyTavern/SillyTavern/blob/release/docker/docker-compose.yml) and run the following command in the directory where the file is located. This will pull the latest release image from the GitHub Container Registry and start the container, automatically creating the necessary volumes.
178
179```shell
180docker-compose up
181```
182
183Customize the `docker-compose.yml` file to your needs. The default port is 8000. If you want to adjust the server configuration using environment variables, read the documentation [here](https://docs.sillytavern.app/administration/config-yaml/#environment-variables).
184
185#### Docker CLI (advanced)
186
187You will need two mandatory directory mappings and a port mapping to allow SillyTavern to function. In the command, replace your selections in the following places:
188
189#### Container Variables
190
191##### Volume Mappings
192
193* `CONFIG_PATH` - The directory where SillyTavern configuration files will be stored on your host machine
194* `DATA_PATH` - The directory where SillyTavern user data (including characters) will be stored on your host machine
195* `PLUGINS_PATH` - (optional) The directory where SillyTavern server plugins will be stored on your host machine
196* `EXTENSIONS_PATH` - (optional) The directory where global UI extensions will be stored on your host machine
197
198##### Port Mappings
199
200* `PUBLIC_PORT` - The port to expose the traffic on. This is mandatory, as you will be accessing the instance from outside of its virtual machine container. DO NOT expose this to the internet without implementing a separate service for security.
201
202##### Additional Settings
203
204* `SILLYTAVERN_VERSION` - On the right-hand side of this GitHub page, you'll see "Packages". Select the "sillytavern" package and you'll see the image versions. The image tag "latest" will keep you up-to-date with the current release. You can also utilize "staging" that points to the nightly image of the respective branch.
205
206#### Running the container
207
2081. Open your Command Line
2092. Run the following command in a folder where you want to store the configuration and data files:
210
211```bash
212SILLYTAVERN_VERSION="latest"
213PUBLIC_PORT="8000"
214CONFIG_PATH="./config"
215DATA_PATH="./data"
216PLUGINS_PATH="./plugins"
217EXTENSIONS_PATH="./extensions"
218
219docker run \
220 --name="sillytavern" \
221 -p "$PUBLIC_PORT:8000/tcp" \
222 -v "$CONFIG_PATH:/home/node/app/config:rw" \
223 -v "$DATA_PATH:/home/node/app/data:rw" \
224 -v "$EXTENSIONS_PATH:/home/node/app/public/scripts/extensions/third-party:rw" \
225 -v "$PLUGINS_PATH:/home/node/app/plugins:rw" \
226 ghcr.io/sillytavern/sillytavern:"$SILLYTAVERN_VERSION"
227```
228
229> By default the container will run in the foreground. If you want to run it in the background, add the `-d` flag to the `docker run` command.
230
231### Building the image yourself
232
233We have a comprehensive guide on using SillyTavern in Docker [here](http://docs.sillytavern.app/installation/docker/) which covers installations on Windows, macOS and Linux! Give it a read if you wish to build the image yourself.
234
235### Common issues with Docker
236
237#### SELinux Permission Issues with Mounted Volumes
238
239Linux distributions with SELinux enabled (such as RHEL, CentOS, Fedora, etc.) may prevent Docker containers from accessing mounted volumes due to security policies. This can result in permission denied errors when the container tries to read or write to the mounted directories.
240
241Two suffixes `:z` or `:Z` can be added to the volume mount. These suffixes tell Docker to relabel file objects on the shared volumes.
242
243* The `z` option is used when the volume content will be shared between containers.
244* The `Z` option is used when the volume content should only be used by the current container.
245
246Example:
247
248```yaml
249# docker-compose.yml
250volumes:
251 ## Shared volume
252 - ./config:/home/node/app/config:z
253 ## Private volume
254 - ./data:/home/node/app/data:Z
255```
256
257## ⚡ Installing via SillyTavern Launcher
258
259SillyTavern Launcher is an installation wizard that will help you get setup with many options, including installing a backend for local inference.
260
261### For Windows users
262
2631. On your keyboard: press **`WINDOWS + R`** to open Run dialog box. Then, run the following command to install git:
264
265```shell
266cmd /c winget install -e --id Git.Git
267```
268
2692. On your keyboard: press **`WINDOWS + E`** to open File Explorer, then navigate to the folder where you want to install the launcher. Once in the desired folder, type `cmd` into the address bar and press enter. Then, run the following command:
270
271```shell
272git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher && start installer.bat
273```
274
275### For Linux users
276
2771. Open your favorite terminal and install git
2782. Git clone the Sillytavern-Launcher with:
279
280```shell
281git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
282```
283
2843. Start the installer.sh with:
285
286```shell
287chmod +x install.sh && ./install.sh
288```
289
2904. After installation start the launcher.sh with:
291
292```shell
293chmod +x launcher.sh && ./launcher.sh
294```
295
296### For Mac users
297
2981. Open a terminal and install brew with:
299
300```shell
301/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
302```
303
3042. Install git with:
305
306```shell
307brew install git
308```
309
3103. Git clone the Sillytavern-Launcher with:
311
312```shell
313git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
314```
315
3164. Start the installer.sh with:
317
318```shell
319chmod +x install.sh && ./install.sh
320```
321
3225. After installation start the launcher.sh with:
323
324```shell
325chmod +x launcher.sh && ./launcher.sh
326```
327
328## 📱 Installing via Termux on Android OS
329
330> \[!NOTE]
331> **SillyTavern can be run natively on Android devices using Termux, but we do not provide official support for this use case.**
332>
333> **Please refer to this guide by ArroganceComplex#2659:**
334>
335> * <https://rentry.org/STAI-Termux>
336
337**Unsupported platform: android arm LEtime-web.** 32-bit Android requires an external dependency that can't be installed with npm. Use the following command to install it: `pkg install esbuild`. Then run the usual installation steps.
338
339## Command-line arguments
340
341You can pass command-line arguments to SillyTavern server startup to override some settings in `config.yaml`.
342
343### Examples
344
345```shell
346node server.js --port 8000 --listen false
347# or
348npm run start -- --port 8000 --listen false
349# or (Windows only)
350Start.bat --port 8000 --listen false
351```
352
353### Supported arguments
354
355> \[!TIP]
356> None of the arguments are required. If you don't provide them, SillyTavern will use the settings in `config.yaml`.
357
358| Option | Description | Type |
359|---------------------------------|----------------------------------------------------------------------|----------|
360| `--version` | Show version number | boolean |
361| `--configPath` | Override the path to the config.yaml file | string |
362| `--dataRoot` | Root directory for data storage | string |
363| `--port` | Sets the port under which SillyTavern will run | number |
364| `--listen` | SillyTavern will listen on all network interfaces | boolean |
365| `--whitelist` | Enables whitelist mode | boolean |
366| `--basicAuthMode` | Enables basic authentication | boolean |
367| `--enableIPv4` | Enables IPv4 protocol | boolean |
368| `--enableIPv6` | Enables IPv6 protocol | boolean |
369| `--listenAddressIPv4` | Specific IPv4 address to listen to | string |
370| `--listenAddressIPv6` | Specific IPv6 address to listen to | string |
371| `--dnsPreferIPv6` | Prefers IPv6 for DNS | boolean |
372| `--ssl` | Enables SSL | boolean |
373| `--certPath` | Path to your certificate file | string |
374| `--keyPath` | Path to your private key file | string |
375| `--browserLaunchEnabled` | Automatically launch SillyTavern in the browser | boolean |
376| `--browserLaunchHostname` | Browser launch hostname | string |
377| `--browserLaunchPort` | Overrides the port for browser launch | string |
378| `--browserLaunchAvoidLocalhost` | Avoids using 'localhost' for browser launch in auto mode | boolean |
379| `--corsProxy` | Enables CORS proxy | boolean |
380| `--requestProxyEnabled` | Enables a use of proxy for outgoing requests | boolean |
381| `--requestProxyUrl` | Request proxy URL (HTTP or SOCKS protocols) | string |
382| `--requestProxyBypass` | Request proxy bypass list (space separated list of hosts) | array |
383| `--disableCsrf` | Disables CSRF protection (NOT RECOMMENDED) | boolean |
384
385## Remote connections
386
387Most often this is for people who want to use SillyTavern on their mobile phones while their PC runs the ST server on the same Wi-Fi network. However, it can be used to allow remote connections from anywhere as well.
38863
389Read the detailed guide on how to set up remote connections in the [Docs](https://docs.sillytavern.app/usage/remoteconnections/).64For detailed installation instructions, please visit our documentation:
39065
391You may also want to configure SillyTavern user profiles with (optional) password protection: [Users](https://docs.sillytavern.app/administration/multi-user/).66* **[Windows Installation Guide](https://docs.sillytavern.app/installation/windows/)**
67* **[MacOS/Linux Installation Guide](https://docs.sillytavern.app/installation/linuxmacos/)**
68* **[Android (Termux) Installation Guide](https://docs.sillytavern.app/installation/android-(termux)/)**
69* **[Docker Installation Guide](https://docs.sillytavern.app/installation/docker/)**
39270
393## License and credits71## License and credits
39472
default/config.yaml+11 -2
@@ -100,7 +100,7 @@ disableCsrfProtection: false
100securityOverride: false100securityOverride: false
101# -- LOGGING CONFIGURATION --101# -- LOGGING CONFIGURATION --
102logging:102logging:
103 # Enable access logging to access.log file103 # Enable access logging to access.log file and console output
104 # Records new connections with timestamp, IP address and user agent104 # Records new connections with timestamp, IP address and user agent
105 enableAccessLog: true105 enableAccessLog: true
106 # Minimum log level to display in the terminal (DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3)106 # Minimum log level to display in the terminal (DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3)
@@ -139,7 +139,7 @@ thumbnails:
139 # JPG thumbnail quality (0-100)139 # JPG thumbnail quality (0-100)
140 quality: 95140 quality: 95
141 # Maximum thumbnail dimensions per type [width, height]141 # Maximum thumbnail dimensions per type [width, height]
142 dimensions: { 'bg': [160, 90], 'avatar': [96, 144] }142 dimensions: { 'bg': [160, 90], 'avatar': [96, 144], 'persona': [96, 144] }
143143
144# PERFORMANCE-RELATED CONFIGURATION144# PERFORMANCE-RELATED CONFIGURATION
145performance:145performance:
@@ -151,6 +151,15 @@ performance:
151 # Enables disk caching for character cards. Improves performances with large card libraries.151 # Enables disk caching for character cards. Improves performances with large card libraries.
152 useDiskCache: true152 useDiskCache: true
153153
154# CACHE BUSTER CONFIGURATION
155# IMPORTANT: Requires localhost or a domain with HTTPS, otherwise will not work!
156cacheBuster:
157 # Clear browser cache on first load or after uploading image files
158 enabled: false
159 # Only clear cache for the specified user agent regex pattern
160 # Example: 'firefox|safari' (case-insensitive)
161 userAgentPattern: ''
162
154# Allow secret keys exposure via API163# Allow secret keys exposure via API
155allowKeysExposure: false164allowKeysExposure: false
156# Skip new default content checks165# Skip new default content checks
default/content/index.json+20 -0
@@ -688,6 +688,10 @@
688 "type": "instruct"688 "type": "instruct"
689 },689 },
690 {690 {
691 "filename": "presets/instruct/KoboldAI.json",
692 "type": "instruct"
693 },
694 {
691 "filename": "presets/context/Llama-3-Instruct-Names.json",695 "filename": "presets/context/Llama-3-Instruct-Names.json",
692 "type": "context"696 "type": "context"
693 },697 },
@@ -818,5 +822,21 @@
818 {822 {
819 "filename": "presets/context/Dots1.json",823 "filename": "presets/context/Dots1.json",
820 "type": "context"824 "type": "context"
825 },
826 {
827 "filename": "presets/context/Mistral V7-Tekken.json",
828 "type": "context"
829 },
830 {
831 "filename": "presets/instruct/Mistral V7-Tekken.json",
832 "type": "instruct"
833 },
834 {
835 "filename": "presets/instruct/Moonshot AI.json",
836 "type": "instruct"
837 },
838 {
839 "filename": "presets/context/Moonshot AI.json",
840 "type": "context"
821 }841 }
822]842]
default/content/presets/context/Mistral V7-Tekken.json+10 -0
@@ -0,0 +1,10 @@
1{
2 "story_string": "[SYSTEM_PROMPT]{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim}}[/SYSTEM_PROMPT]",
3 "example_separator": "",
4 "chat_start": "",
5 "use_stop_strings": false,
6 "always_force_name2": true,
7 "trim_sentences": false,
8 "single_line": false,
9 "name": "Mistral V7-Tekken"
10}
default/content/presets/context/Moonshot AI.json+11 -0
@@ -0,0 +1,11 @@
1{
2 "story_string": "<|im_system|>system<|im_middle|>{{#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}}{{trim}}<|im_end|>",
3 "example_separator": "",
4 "chat_start": "",
5 "use_stop_strings": false,
6 "names_as_stop_strings": true,
7 "always_force_name2": true,
8 "trim_sentences": false,
9 "single_line": false,
10 "name": "Moonshot AI"
11}
\ No newline at end of file11 \ No newline at end of file
default/content/presets/instruct/KoboldAI.json+22 -0
@@ -0,0 +1,22 @@
1{
2 "input_sequence": "{{[INPUT]}}",
3 "output_sequence": "{{[OUPUT]}}",
4 "last_output_sequence": "",
5 "system_sequence": "{{[SYSTEM]}}",
6 "stop_sequence": "",
7 "wrap": true,
8 "macro": true,
9 "names_behavior": "force",
10 "activation_regex": "",
11 "system_sequence_prefix": "",
12 "system_sequence_suffix": "",
13 "first_output_sequence": "",
14 "skip_examples": false,
15 "output_suffix": "{{[OUTPUT_END]}}",
16 "input_suffix": "{{[INPUT_END]}}",
17 "system_suffix": "{{[SYSTEM_END]}}",
18 "user_alignment_message": "",
19 "system_same_as_user": false,
20 "last_system_sequence": "",
21 "name": "KoboldAI"
22}
default/content/presets/instruct/Mistral V7-Tekken.json+22 -0
@@ -0,0 +1,22 @@
1{
2 "input_sequence": "[INST]",
3 "output_sequence": " ",
4 "last_output_sequence": "",
5 "system_sequence": "[SYSTEM_PROMPT]",
6 "stop_sequence": "</s>",
7 "wrap": false,
8 "macro": true,
9 "names_behavior": "force",
10 "activation_regex": "",
11 "system_sequence_prefix": "",
12 "system_sequence_suffix": "",
13 "first_output_sequence": "",
14 "skip_examples": false,
15 "output_suffix": "</s>",
16 "input_suffix": "[/INST]",
17 "system_suffix": "[/SYSTEM_PROMPT]",
18 "user_alignment_message": "",
19 "system_same_as_user": false,
20 "last_system_sequence": "",
21 "name": "Mistral V7-Tekken"
22}
default/content/presets/instruct/Moonshot AI.json+24 -0
@@ -0,0 +1,24 @@
1{
2 "input_sequence": "<|im_user|>user<|im_middle|>",
3 "output_sequence": "<|im_assistant|>assistant<|im_middle|>",
4 "last_output_sequence": "",
5 "system_sequence": "<|im_system|>system<|im_middle|>",
6 "stop_sequence": "<|im_end|>",
7 "wrap": false,
8 "macro": true,
9 "activation_regex": "",
10 "system_sequence_prefix": "",
11 "system_sequence_suffix": "",
12 "first_output_sequence": "",
13 "skip_examples": false,
14 "output_suffix": "<|im_end|>",
15 "input_suffix": "<|im_end|>",
16 "system_suffix": "<|im_end|>",
17 "user_alignment_message": "",
18 "system_same_as_user": false,
19 "last_system_sequence": "",
20 "first_input_sequence": "",
21 "last_input_sequence": "",
22 "names_behavior": "force",
23 "name": "Moonshot AI"
24}
default/content/presets/openai/Default.json+1 -4
@@ -2,12 +2,11 @@
2 "chat_completion_source": "openai",2 "chat_completion_source": "openai",
3 "openai_model": "gpt-4-turbo",3 "openai_model": "gpt-4-turbo",
4 "claude_model": "claude-3-5-sonnet-20240620",4 "claude_model": "claude-3-5-sonnet-20240620",
5 "windowai_model": "",
6 "openrouter_model": "OR_Website",5 "openrouter_model": "OR_Website",
7 "openrouter_use_fallback": false,6 "openrouter_use_fallback": false,
8 "openrouter_group_models": false,7 "openrouter_group_models": false,
9 "openrouter_sort_models": "alphabetically",8 "openrouter_sort_models": "alphabetically",
10 "ai21_model": "jamba-1.5-large",9 "ai21_model": "jamba-large",
11 "mistralai_model": "mistral-large-latest",10 "mistralai_model": "mistral-large-latest",
12 "custom_model": "",11 "custom_model": "",
13 "custom_url": "",12 "custom_url": "",
@@ -226,12 +225,10 @@
226 ]225 ]
227 }226 }
228 ],227 ],
229 "api_url_scale": "",
230 "show_external_models": false,228 "show_external_models": false,
231 "assistant_prefill": "",229 "assistant_prefill": "",
232 "assistant_impersonation": "",230 "assistant_impersonation": "",
233 "claude_use_sysprompt": false,231 "claude_use_sysprompt": false,
234 "use_alt_scale": false,
235 "squash_system_messages": false,232 "squash_system_messages": false,
236 "image_inlining": false,233 "image_inlining": false,
237 "bypass_status_check": false,234 "bypass_status_check": false,
default/content/settings.json+1 -2
@@ -623,8 +623,7 @@
623 "wi_format": "{0}",623 "wi_format": "{0}",
624 "openai_model": "gpt-4-turbo",624 "openai_model": "gpt-4-turbo",
625 "claude_model": "claude-3-5-sonnet-20240620",625 "claude_model": "claude-3-5-sonnet-20240620",
626 "ai21_model": "jamba-1.5-large",626 "ai21_model": "jamba-large",
627 "windowai_model": "",
628 "openrouter_model": "OR_Website",627 "openrouter_model": "OR_Website",
629 "reverse_proxy": "",628 "reverse_proxy": "",
630 "chat_completion_source": "openai",629 "chat_completion_source": "openai",
index.d.ts+5 -0
@@ -66,4 +66,9 @@ declare global {
66 * Parsed command line arguments.66 * Parsed command line arguments.
67 */67 */
68 var COMMAND_LINE_ARGS: CommandLineArguments;68 var COMMAND_LINE_ARGS: CommandLineArguments;
69
70 /**
71 * Forces a global mode if set to `true` before parsing the CLI arguments.
72 */
73 var FORCE_GLOBAL_MODE: boolean;
69}74}
package-lock.json+53 -25
@@ -1,12 +1,12 @@
1{1{
2 "name": "sillytavern",2 "name": "sillytavern",
3 "version": "1.13.1",3 "version": "1.13.2",
4 "lockfileVersion": 3,4 "lockfileVersion": 3,
5 "requires": true,5 "requires": true,
6 "packages": {6 "packages": {
7 "": {7 "": {
8 "name": "sillytavern",8 "name": "sillytavern",
9 "version": "1.13.1",9 "version": "1.13.2",
10 "hasInstallScript": true,10 "hasInstallScript": true,
11 "license": "AGPL-3.0",11 "license": "AGPL-3.0",
12 "dependencies": {12 "dependencies": {
@@ -40,7 +40,7 @@
40 "@popperjs/core": "^2.11.8",40 "@popperjs/core": "^2.11.8",
41 "@zeldafan0225/ai_horde": "^5.2.0",41 "@zeldafan0225/ai_horde": "^5.2.0",
42 "archiver": "^7.0.1",42 "archiver": "^7.0.1",
43 "bing-translate-api": "^4.0.2",43 "bing-translate-api": "^4.1.0",
44 "body-parser": "^1.20.2",44 "body-parser": "^1.20.2",
45 "bowser": "^2.11.0",45 "bowser": "^2.11.0",
46 "bytes": "^3.1.2",46 "bytes": "^3.1.2",
@@ -55,8 +55,9 @@
55 "diff-match-patch": "^1.0.5",55 "diff-match-patch": "^1.0.5",
56 "dompurify": "^3.2.6",56 "dompurify": "^3.2.6",
57 "droll": "^0.2.1",57 "droll": "^0.2.1",
58 "env-paths": "^3.0.0",
58 "express": "^4.21.0",59 "express": "^4.21.0",
59 "form-data": "^4.0.3",60 "form-data": "^4.0.4",
60 "fuse.js": "^7.1.0",61 "fuse.js": "^7.1.0",
61 "google-translate-api-browser": "^3.0.1",62 "google-translate-api-browser": "^3.0.1",
62 "google-translate-api-x": "^10.7.2",63 "google-translate-api-x": "^10.7.2",
@@ -77,7 +78,7 @@
77 "multer": "^2.0.2",78 "multer": "^2.0.2",
78 "node-fetch": "^3.3.2",79 "node-fetch": "^3.3.2",
79 "node-persist": "^4.0.4",80 "node-persist": "^4.0.4",
80 "open": "^10.1.2",81 "open": "^10.2.0",
81 "png-chunk-text": "^1.0.0",82 "png-chunk-text": "^1.0.0",
82 "png-chunks-extract": "^1.0.0",83 "png-chunks-extract": "^1.0.0",
83 "proxy-agent": "^6.5.0",84 "proxy-agent": "^6.5.0",
@@ -95,13 +96,13 @@
95 "wavefile": "^11.0.0",96 "wavefile": "^11.0.0",
96 "webpack": "^5.98.0",97 "webpack": "^5.98.0",
97 "write-file-atomic": "^5.0.1",98 "write-file-atomic": "^5.0.1",
98 "ws": "^8.18.2",99 "ws": "^8.18.3",
99 "yaml": "^2.8.0",100 "yaml": "^2.8.0",
100 "yargs": "^17.7.1",101 "yargs": "^17.7.1",
101 "yauzl": "^3.2.0"102 "yauzl": "^3.2.0"
102 },103 },
103 "bin": {104 "bin": {
104 "sillytavern": "server.js"105 "sillytavern": "src/server-global.js"
105 },106 },
106 "devDependencies": {107 "devDependencies": {
107 "@types/archiver": "^6.0.3",108 "@types/archiver": "^6.0.3",
@@ -117,9 +118,9 @@
117 "@types/jquery-cropper": "^1.0.4",118 "@types/jquery-cropper": "^1.0.4",
118 "@types/jquery.transit": "^0.9.33",119 "@types/jquery.transit": "^0.9.33",
119 "@types/jqueryui": "^1.12.24",120 "@types/jqueryui": "^1.12.24",
120 "@types/lodash": "^4.17.17",121 "@types/lodash": "^4.17.20",
121 "@types/mime-types": "^3.0.1",122 "@types/mime-types": "^3.0.1",
122 "@types/multer": "^1.4.13",123 "@types/multer": "^2.0.0",
123 "@types/node": "^18.19.84",124 "@types/node": "^18.19.84",
124 "@types/node-persist": "^3.1.8",125 "@types/node-persist": "^3.1.8",
125 "@types/png-chunk-text": "^1.0.3",126 "@types/png-chunk-text": "^1.0.3",
@@ -1972,9 +1973,9 @@
1972 }1973 }
1973 },1974 },
1974 "node_modules/@types/lodash": {1975 "node_modules/@types/lodash": {
1975 "version": "4.17.17",1976 "version": "4.17.20",
1976 "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.17.tgz",1977 "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
1977 "integrity": "sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==",1978 "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
1978 "dev": true,1979 "dev": true,
1979 "license": "MIT"1980 "license": "MIT"
1980 },1981 },
@@ -1999,9 +2000,9 @@
1999 "license": "MIT"2000 "license": "MIT"
2000 },2001 },
2001 "node_modules/@types/multer": {2002 "node_modules/@types/multer": {
2002 "version": "1.4.13",2003 "version": "2.0.0",
2003 "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz",2004 "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz",
2004 "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==",2005 "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==",
2005 "dev": true,2006 "dev": true,
2006 "license": "MIT",2007 "license": "MIT",
2007 "dependencies": {2008 "dependencies": {
@@ -2832,9 +2833,9 @@
2832 }2833 }
2833 },2834 },
2834 "node_modules/bing-translate-api": {2835 "node_modules/bing-translate-api": {
2835 "version": "4.0.2",2836 "version": "4.1.0",
2836 "resolved": "https://registry.npmjs.org/bing-translate-api/-/bing-translate-api-4.0.2.tgz",2837 "resolved": "https://registry.npmjs.org/bing-translate-api/-/bing-translate-api-4.1.0.tgz",
2837 "integrity": "sha512-JJ8XUehnxzOhHU91oy86xEtp8OOMjVEjCZJX042fKxoO19NNvxJ5omeCcxQNFoPbDqVpBJwqiGVquL0oPdQm1Q==",2838 "integrity": "sha512-oP2663Yd5MXX4kbB/3LdS9YgPiE+ls9+2iFZH2ZXigWhWyHT3R4m6aCup4TNJd3/U4gqHHnQoxTaIW7uOf4+vA==",
2838 "license": "MIT",2839 "license": "MIT",
2839 "dependencies": {2840 "dependencies": {
2840 "got": "^11.8.6"2841 "got": "^11.8.6"
@@ -4087,6 +4088,18 @@
4087 "url": "https://github.com/fb55/entities?sponsor=1"4088 "url": "https://github.com/fb55/entities?sponsor=1"
4088 }4089 }
4089 },4090 },
4091 "node_modules/env-paths": {
4092 "version": "3.0.0",
4093 "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
4094 "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
4095 "license": "MIT",
4096 "engines": {
4097 "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
4098 },
4099 "funding": {
4100 "url": "https://github.com/sponsors/sindresorhus"
4101 }
4102 },
4090 "node_modules/es-define-property": {4103 "node_modules/es-define-property": {
4091 "version": "1.0.1",4104 "version": "1.0.1",
4092 "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",4105 "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -6420,15 +6433,15 @@
6420 }6433 }
6421 },6434 },
6422 "node_modules/open": {6435 "node_modules/open": {
6423 "version": "10.1.2",6436 "version": "10.2.0",
6424 "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz",6437 "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz",
6425 "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==",6438 "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
6426 "license": "MIT",6439 "license": "MIT",
6427 "dependencies": {6440 "dependencies": {
6428 "default-browser": "^5.2.1",6441 "default-browser": "^5.2.1",
6429 "define-lazy-prop": "^3.0.0",6442 "define-lazy-prop": "^3.0.0",
6430 "is-inside-container": "^1.0.0",6443 "is-inside-container": "^1.0.0",
6431 "is-wsl": "^3.1.0"6444 "wsl-utils": "^0.1.0"
6432 },6445 },
6433 "engines": {6446 "engines": {
6434 "node": ">=18"6447 "node": ">=18"
@@ -8503,9 +8516,9 @@
8503 }8516 }
8504 },8517 },
8505 "node_modules/ws": {8518 "node_modules/ws": {
8506 "version": "8.18.2",8519 "version": "8.18.3",
8507 "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",8520 "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
8508 "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",8521 "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
8509 "license": "MIT",8522 "license": "MIT",
8510 "engines": {8523 "engines": {
8511 "node": ">=10.0.0"8524 "node": ">=10.0.0"
@@ -8523,6 +8536,21 @@
8523 }8536 }
8524 }8537 }
8525 },8538 },
8539 "node_modules/wsl-utils": {
8540 "version": "0.1.0",
8541 "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
8542 "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==",
8543 "license": "MIT",
8544 "dependencies": {
8545 "is-wsl": "^3.1.0"
8546 },
8547 "engines": {
8548 "node": ">=18"
8549 },
8550 "funding": {
8551 "url": "https://github.com/sponsors/sindresorhus"
8552 }
8553 },
8526 "node_modules/xhr": {8554 "node_modules/xhr": {
8527 "version": "2.6.0",8555 "version": "2.6.0",
8528 "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz",8556 "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz",
package.json+10 -8
@@ -30,7 +30,7 @@
30 "@popperjs/core": "^2.11.8",30 "@popperjs/core": "^2.11.8",
31 "@zeldafan0225/ai_horde": "^5.2.0",31 "@zeldafan0225/ai_horde": "^5.2.0",
32 "archiver": "^7.0.1",32 "archiver": "^7.0.1",
33 "bing-translate-api": "^4.0.2",33 "bing-translate-api": "^4.1.0",
34 "body-parser": "^1.20.2",34 "body-parser": "^1.20.2",
35 "bowser": "^2.11.0",35 "bowser": "^2.11.0",
36 "bytes": "^3.1.2",36 "bytes": "^3.1.2",
@@ -45,8 +45,9 @@
45 "diff-match-patch": "^1.0.5",45 "diff-match-patch": "^1.0.5",
46 "dompurify": "^3.2.6",46 "dompurify": "^3.2.6",
47 "droll": "^0.2.1",47 "droll": "^0.2.1",
48 "env-paths": "^3.0.0",
48 "express": "^4.21.0",49 "express": "^4.21.0",
49 "form-data": "^4.0.3",50 "form-data": "^4.0.4",
50 "fuse.js": "^7.1.0",51 "fuse.js": "^7.1.0",
51 "google-translate-api-browser": "^3.0.1",52 "google-translate-api-browser": "^3.0.1",
52 "google-translate-api-x": "^10.7.2",53 "google-translate-api-x": "^10.7.2",
@@ -67,7 +68,7 @@
67 "multer": "^2.0.2",68 "multer": "^2.0.2",
68 "node-fetch": "^3.3.2",69 "node-fetch": "^3.3.2",
69 "node-persist": "^4.0.4",70 "node-persist": "^4.0.4",
70 "open": "^10.1.2",71 "open": "^10.2.0",
71 "png-chunk-text": "^1.0.0",72 "png-chunk-text": "^1.0.0",
72 "png-chunks-extract": "^1.0.0",73 "png-chunks-extract": "^1.0.0",
73 "proxy-agent": "^6.5.0",74 "proxy-agent": "^6.5.0",
@@ -85,7 +86,7 @@
85 "wavefile": "^11.0.0",86 "wavefile": "^11.0.0",
86 "webpack": "^5.98.0",87 "webpack": "^5.98.0",
87 "write-file-atomic": "^5.0.1",88 "write-file-atomic": "^5.0.1",
88 "ws": "^8.18.2",89 "ws": "^8.18.3",
89 "yaml": "^2.8.0",90 "yaml": "^2.8.0",
90 "yargs": "^17.7.1",91 "yargs": "^17.7.1",
91 "yauzl": "^3.2.0"92 "yauzl": "^3.2.0"
@@ -111,10 +112,11 @@
111 "type": "git",112 "type": "git",
112 "url": "https://github.com/SillyTavern/SillyTavern.git"113 "url": "https://github.com/SillyTavern/SillyTavern.git"
113 },114 },
114 "version": "1.13.1",115 "version": "1.13.2",
115 "scripts": {116 "scripts": {
116 "start": "node server.js",117 "start": "node server.js",
117 "debug": "node --inspect server.js",118 "debug": "node --inspect server.js",
119 "start:global": "node server.js --global",
118 "start:electron": "cd ./src/electron && npm run start",120 "start:electron": "cd ./src/electron && npm run start",
119 "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",121 "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",
120 "start:bun": "bun server.js",122 "start:bun": "bun server.js",
@@ -126,7 +128,7 @@
126 "plugins:install": "node plugins install"128 "plugins:install": "node plugins install"
127 },129 },
128 "bin": {130 "bin": {
129 "sillytavern": "./server.js"131 "sillytavern": "./src/server-global.js"
130 },132 },
131 "rules": {133 "rules": {
132 "no-path-concat": "off",134 "no-path-concat": "off",
@@ -147,9 +149,9 @@
147 "@types/jquery-cropper": "^1.0.4",149 "@types/jquery-cropper": "^1.0.4",
148 "@types/jquery.transit": "^0.9.33",150 "@types/jquery.transit": "^0.9.33",
149 "@types/jqueryui": "^1.12.24",151 "@types/jqueryui": "^1.12.24",
150 "@types/lodash": "^4.17.17",152 "@types/lodash": "^4.17.20",
151 "@types/mime-types": "^3.0.1",153 "@types/mime-types": "^3.0.1",
152 "@types/multer": "^1.4.13",154 "@types/multer": "^2.0.0",
153 "@types/node": "^18.19.84",155 "@types/node": "^18.19.84",
154 "@types/node-persist": "^3.1.8",156 "@types/node-persist": "^3.1.8",
155 "@types/png-chunk-text": "^1.0.3",157 "@types/png-chunk-text": "^1.0.3",
public/css/promptmanager.css+4 -0
@@ -112,6 +112,10 @@
112 flex-direction: column;112 flex-direction: column;
113}113}
114114
115.completion_prompt_manager_popup_entry_form .select2-container {
116 margin: 5px 0;
117}
118
115#completion_prompt_manager_popup .completion_prompt_manager_popup_entry_form_control:has(#completion_prompt_manager_popup_entry_form_prompt) {119#completion_prompt_manager_popup .completion_prompt_manager_popup_entry_form_control:has(#completion_prompt_manager_popup_entry_form_prompt) {
116 flex: 1;120 flex: 1;
117 display: flex;121 display: flex;
public/css/select2-overrides.css+12 -59
@@ -19,14 +19,15 @@
19.select2-container .select2-selection .select2-selection__clear {19.select2-container .select2-selection .select2-selection__clear {
20 color: var(--SmartThemeBodyColor);20 color: var(--SmartThemeBodyColor);
21 font-size: 20px;21 font-size: 20px;
22 padding: 0;
23 position: absolute;22 position: absolute;
24 right: 5px;23 right: 5px;
25 top: 0;24 margin-top: 0; /* compensate for the increased font size */
25 margin-right: 0;
26}26}
2727
28.select2-container .select2-search__field {28.select2-container .select2-search--inline textarea.select2-search__field {
29 opacity: 0.8;29 opacity: 0.8;
30 margin-top: 7px; /* get placeholder text to the center */
30}31}
3132
32.select2-selection--single .select2-selection__placeholder {33.select2-selection--single .select2-selection__placeholder {
@@ -39,8 +40,8 @@
3940
40.select2-container .select2-selection--single .select2-selection__rendered {41.select2-container .select2-selection--single .select2-selection__rendered {
41 color: var(--SmartThemeBodyColor);42 color: var(--SmartThemeBodyColor);
42 line-height: revert;43 line-height: unset;
43 padding-left: unset;44 padding: 3px 5px;
44}45}
4546
46.select2-container .select2-results>.select2-results__options {47.select2-container .select2-results>.select2-results__options {
@@ -63,9 +64,10 @@
63 background-color: var(--black30a);64 background-color: var(--black30a);
64 color: var(--SmartThemeBodyColor);65 color: var(--SmartThemeBodyColor);
65 border: 1px solid var(--SmartThemeBorderColor);66 border: 1px solid var(--SmartThemeBorderColor);
66 border-radius: 7px;
67 font-family: var(--mainFontFamily);67 font-family: var(--mainFontFamily);
68 padding: 3px 5px;68 padding: 3px 5px;
69 display: unset; /* override the ST textarea display style */
70 border-radius: 0; /* border radius was interfering with cursor */
69}71}
7072
71/* Customize the selected option */73/* Customize the selected option */
@@ -93,7 +95,10 @@
93 border: 1px solid var(--SmartThemeBorderColor);95 border: 1px solid var(--SmartThemeBorderColor);
94 border-radius: 7px;96 border-radius: 7px;
95 font-family: var(--mainFontFamily);97 font-family: var(--mainFontFamily);
96 padding: 3px 5px;98}
99
100.select2-container .select2-selection--multiple.select2-selection--clearable {
101 padding-right: 20px;
97}102}
98103
99.select2-container.select2-container--focus .select2-selection--multiple,104.select2-container.select2-container--focus .select2-selection--multiple,
@@ -230,55 +235,3 @@ span.select2.select2-container .select2-selection__choice__remove:hover {
230.select2_choice_clickable_buttonstyle+span.select2-container .select2-selection__choice__display:hover {235.select2_choice_clickable_buttonstyle+span.select2-container .select2-selection__choice__display:hover {
231 background-color: var(--white30a);236 background-color: var(--white30a);
232}237}
233
234/* Custom class to support same line multi inputs of select2 controls */
235.select2_multi_sameline+span.select2-container .select2-selection--multiple {
236 display: flex;
237 flex-wrap: wrap;
238}
239
240.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-search--inline {
241 /* Allow search placeholder to take up all space if needed */
242 flex-grow: 1;
243}
244
245.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-selection__rendered {
246 /* Fix weird styling choice or huge margin around selected options */
247 margin-block-start: 2px;
248 margin-block-end: 2px;
249 display: flex;
250 align-items: center;
251 flex-wrap: wrap;
252 row-gap: 5px;
253}
254
255.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-selection__choice {
256 margin-top: 0px;
257}
258
259.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-search__field {
260 /* Min height to reserve spacing */
261 min-height: calc(var(--mainFontSize) + 13px);
262 /* Min width to be clickable */
263 min-width: 4em;
264 align-content: center;
265 /* Fix search textarea alignment issue with UL elements */
266 margin-top: 0px;
267 height: unset;
268 /* Prevent height from jumping around when input is focused */
269 line-height: 1;
270}
271
272.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-selection__rendered {
273 /* Min height to reserve spacing */
274 min-height: calc(var(--mainFontSize) + 13px);
275}
276
277/* Make search bar invisible unless the select2 is active, to save space */
278.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-search--inline {
279 height: 1px;
280}
281
282.select2_multi_sameline+span.select2-container.select2-container--focus .select2-selection--multiple .select2-search--inline {
283 height: unset;
284}
public/css/st-tailwind.css+0 -8
@@ -470,14 +470,6 @@
470 margin-left: 20px;470 margin-left: 20px;
471}471}
472472
473/*used to fix smallness of certain FontAwesome glyph which break button squareness*/
474/*currently used on: CharList Import*/
475
476.faSmallFontSquareFix {
477 font-size: calc(var(--mainFontSize) *1.25);
478 width: calc(var(--mainFontSize) *1.95);
479}
480
481.textarea_compact {473.textarea_compact {
482 font-size: calc(var(--mainFontSize) * 0.95);474 font-size: calc(var(--mainFontSize) * 0.95);
483 line-height: 1.2;475 line-height: 1.2;
public/css/tags.css+3 -1
@@ -157,7 +157,9 @@
157 flex-direction: row;157 flex-direction: row;
158 flex-wrap: wrap;158 flex-wrap: wrap;
159 align-items: flex-start;159 align-items: flex-start;
160 margin: 5px;160 margin-top: 5px;
161 max-height: 40dvh;
162 overflow-y: auto;
161}163}
162164
163.rm_tag_filter .tag {165.rm_tag_filter .tag {
public/css/toggle-dependent.css+2 -1
@@ -93,7 +93,8 @@ body.charListGrid #rm_print_characters_block .group_select .ch_name,
93body.charListGrid #rm_print_characters_block .group_select .group_select_counter,93body.charListGrid #rm_print_characters_block .group_select .group_select_counter,
94#user_avatar_block.gridView .avatar-container .ch_name,94#user_avatar_block.gridView .avatar-container .ch_name,
95#user_avatar_block.gridView .avatar-container .bogus_folder_counter,95#user_avatar_block.gridView .avatar-container .bogus_folder_counter,
96#user_avatar_block.gridView .avatar-container .group_select_counter {96#user_avatar_block.gridView .avatar-container .group_select_counter,
97#user_avatar_block.gridView .avatar-container .ch_additional_info {
97 width: 100%;98 width: 100%;
98 max-width: 100px;99 max-width: 100px;
99 text-align: center;100 text-align: center;
public/css/world-info.css+14 -1
@@ -107,7 +107,6 @@
107 height: auto;107 height: auto;
108 margin-top: 0;108 margin-top: 0;
109 margin-bottom: 0;109 margin-bottom: 0;
110 min-height: calc(var(--mainFontSize) + 14px);
111}110}
112111
113.delete_entry_button {112.delete_entry_button {
@@ -286,3 +285,17 @@ select.keyselect+span.select2-container .select2-selection--multiple {
286.world_entry:has(input[name="delay_until_recursion"]:not(:checked)) .world_entry_form_control:has(input[name="delayUntilRecursionLevel"]) {285.world_entry:has(input[name="delay_until_recursion"]:not(:checked)) .world_entry_form_control:has(input[name="delayUntilRecursionLevel"]) {
287 display: none;286 display: none;
288}287}
288
289.world_entry label[for="__invisible"] {
290 visibility: hidden;
291 pointer-events: none;
292 opacity: 0;
293 width: 0;
294 overflow: hidden;
295}
296
297#WIMultiSelector .select2-container .select2-selection--multiple {
298 max-height: 25vh;
299 overflow-y: auto;
300 overflow-x: hidden;
301}
public/global.d.ts+6 -0
@@ -73,4 +73,10 @@ declare global {
73 * @param args - The arguments for the conversion function.73 * @param args - The arguments for the conversion function.
74 */74 */
75 function convertVideoToAnimatedWebp(args: ConvertVideoArgs): Promise<Uint8Array>;75 function convertVideoToAnimatedWebp(args: ConvertVideoArgs): Promise<Uint8Array>;
76
77 interface ColorPickerEvent extends JQuery.ChangeEvent<HTMLElement> {
78 detail: {
79 rgba: string;
80 };
81 }
76}82}
public/img/nanogpt.svg+1 -43
@@ -1,43 +1 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>1<svg xmlns="http://www.w3.org/2000/svg" width="734.222" height="756.943" viewBox="0 0 734.222 756.943"><path d="M357.038 52.288l-73.383 52.313-55.764 142.923-54.617 143.196c1.171.279 425.288-132.081 426.226-133.018C600.225 256.976 432.641.031 431.422 0c-.55-.014-34.023 23.515-74.384 52.288m181.711 32.471l-10.329 8.35c-.665.67 99.325 156.949 102.358 159.98 1.875 1.874 79.763 35.16 81.138 34.675 1.14-.403-160.629-210.253-162.275-210.505-.67-.103-5.572 3.272-10.892 7.5M57.422 239.973L.14 314.826c-1.188 1.714 3.842 4.638 68.856 40.032l71.106 37.861c1.12-.373 72.985-183.777 72.538-185.118-.294-.884-96.296-40.583-98.218-40.615-.55-.01-26.2 32.835-57 72.987m568.524 51.223l-91.43 199.175-88.086 193.416c.271.271.657.305.859.076s65.024-80.248 144.049-177.819l142.882-178.1c-.886-.775-103.789-42.998-104.792-42.998-.347 0-1.914 2.813-3.482 6.25m-243.024 63.521l-211.816 65.765c-.466.388 206.21 336.458 206.917 336.461.728.004 217.289-466.962 216.767-467.41-.202-.174-95.543 29.159-211.868 65.184m-343 16.73c0 .732 221.624 257.149 222.982 257.988.56.346 1.018.38 1.018.076 0-.611-122.164-199.524-125.049-203.612-1.152-1.632-18.448-11.562-48.125-27.63l-50.826-26.822"/></svg>
2<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
4<svg
5 width="190.91216mm"
6 height="201.47243mm"
7 viewBox="0 0 190.91216 201.47243"
8 version="1.1"
9 id="svg1"
10 xmlns="http://www.w3.org/2000/svg"
11 xmlns:svg="http://www.w3.org/2000/svg">
12 <defs
13 id="defs1" />
14 <g
15 id="layer1"
16 transform="translate(-2.2086436)">
17 <g
18 id="g8"
19 transform="matrix(0.99583856,0,0,1.0472862,2.2086437,-7.9847956)">
20 <path
21 style="font-weight:bold;font-size:89.3125px;font-family:'Courier New';-inkscape-font-specification:'Courier New Bold';letter-spacing:-8.37308px;stroke-width:1.73283"
22 d="m 161.17775,117.85719 v 23.36554 q 0,20.76935 -10.30578,39.77331 -10.21695,19.00395 -29.31819,19.00395 -11.01653,0 -18.65701,-5.60771 -7.551611,-5.71157 -14.392485,-20.8732 -6.752041,-15.26549 -6.752041,-32.29635 v -23.36554 q 0,-20.665499 10.216893,-39.669461 10.216893,-19.107806 29.318113,-19.107806 10.92769,0 18.47935,5.607725 7.64049,5.607727 14.48139,20.873203 6.92976,15.161629 6.92976,32.296339 z m -18.30165,0 q 0,-17.3424 -8.26241,-30.32325 -4.70867,-7.165428 -13.2376,-7.165428 -8.26239,0 -13.50413,7.892355 -7.906998,11.942383 -7.906998,29.596323 v 23.36554 q 0,17.44626 8.351218,30.32326 4.61983,7.16543 13.2376,7.16543 8.17356,0 13.4153,-7.89237 7.90702,-11.94237 7.90702,-29.59632 z"
23 id="path8" />
24 <path
25 style="font-weight:bold;font-size:89.3125px;font-family:'Courier New';-inkscape-font-specification:'Courier New Bold';letter-spacing:-8.37308px;stroke-width:1.67461"
26 d="M 54.800839,58.349248 V 166.67311 H 73.27526 q 6.604382,0 9.371083,2.79982 2.855949,2.70327 2.855949,7.14436 0,4.34453 -2.855949,7.14435 -2.766701,2.70327 -9.371083,2.70327 H 18.030494 q -6.604382,0 -9.4603308,-2.70327 -2.7667007,-2.79982 -2.7667007,-7.2409 0,-4.34454 2.7667007,-7.04781 2.8559488,-2.79982 9.4603308,-2.79982 H 36.504915 V 84.126852 l -16.332459,4.634176 q -3.926929,1.158544 -5.890395,1.158544 -3.480687,0 -6.0688915,-2.992905 -2.4989554,-2.992906 -2.4989554,-7.337446 0,-3.958358 1.8742165,-6.178902 1.8742167,-2.317087 7.8538604,-4.054903 z"
27 transform="matrix(0.99545738,0,0,1.0756271,-5.6882567,-3.6821097)"
28 id="path7" />
29 </g>
30 <g
31 id="g1"
32 transform="matrix(0.84984007,0,0,0.87855774,28.999003,5.2337341e-7)">
33 <path
34 style="font-weight:bold;font-size:89.3125px;font-family:'Courier New';-inkscape-font-specification:'Courier New Bold';letter-spacing:-8.37308px;stroke-width:1.73283"
35 d="m 184.13459,37.01017 q -3.34269,3.095908 -6.12102,4.409323 -2.77834,1.266508 -5.86056,1.266508 -7.1629,0 -12.54593,-6.191815 -5.33961,-6.191816 -5.33961,-15.385722 0,-7.176877 3.68997,-13.0403372 Q 162.99318,4.3096483e-6 172.15301,4.3096483e-6 q 6.55514,0 11.46064,4.2685995903517 4.90551,4.2216915 7.20632,12.2429061 2.30081,7.974308 2.30081,16.65223 0,14.729014 -10.549,23.641474 -8.16137,6.848524 -17.88555,6.848524 -5.38303,0 -8.33501,-2.345385 -1.99693,-1.547954 -1.99693,-4.080969 0,-2.063938 1.25893,-3.424261 1.30235,-1.407231 3.16905,-1.407231 1.04187,0 2.34422,0.703615 1.69304,0.938154 3.55974,0.938154 6.72879,0 12.4157,-4.925307 5.68691,-4.972215 7.03266,-12.102184 z m -1.25893,-15.151183 q -1.43258,-6.660892 -4.25433,-9.428446 -2.77834,-2.814461 -6.38149,-2.814461 -3.77681,0 -6.42491,3.142816 -2.6481,3.095907 -2.6481,8.255753 0,5.394384 2.6481,8.72483 2.69151,3.330446 6.12102,3.330446 6.03421,0 10.93971,-11.210938 z"
36 id="path6" />
37 <path
38 style="font-weight:bold;font-size:89.3125px;font-family:'Courier New';-inkscape-font-specification:'Courier New Bold';letter-spacing:-8.37308px;stroke-width:1.51638"
39 d="m 142.10195,39.730816 h -22.30654 q -3.49058,0 -4.15545,-0.234538 -0.99731,-0.375262 -1.69543,-1.641769 -0.69812,-1.313415 -0.69812,-2.955185 0,-1.594861 0.69812,-2.908276 0.69812,-1.313416 1.72867,-1.641769 0.66488,-0.234539 4.12221,-0.234539 h 22.30654 q 3.49059,0 4.15545,0.234539 0.99732,0.375261 1.69543,1.688676 0.69812,1.266508 0.69812,2.861369 0,1.64177 -0.69812,2.955185 -0.69811,1.266507 -1.72867,1.641769 -0.66487,0.234538 -4.12221,0.234538 z"
40 id="text2" />
41 </g>
42 </g>
43</svg>
public/index.html+134 -125
@@ -300,7 +300,7 @@
300 </label>300 </label>
301 </div>301 </div>
302 </div>302 </div>
303 <small data-tg-type="openrouter">303 <small id="prompt_cost_block" data-tg-type="openrouter">
304 <span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="or_prompt_cost">&ndash;</span>304 <span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="or_prompt_cost">&ndash;</span>
305 </small>305 </small>
306 </div>306 </div>
@@ -691,7 +691,7 @@
691 </span>691 </span>
692 </div>692 </div>
693 </div>693 </div>
694 <div class="range-block" data-source="openai,claude,windowai,aimlapi,openrouter,ai21,scale,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek,xai,pollinations">694 <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek,xai,pollinations">
695 <div class="range-block-title" data-i18n="Temperature">695 <div class="range-block-title" data-i18n="Temperature">
696 Temperature696 Temperature
697 </div>697 </div>
@@ -743,7 +743,7 @@
743 </div>743 </div>
744 </div>744 </div>
745 </div>745 </div>
746 <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,scale,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek,xai,pollinations">746 <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek,xai,pollinations">
747 <div class="range-block-title" data-i18n="Top P">747 <div class="range-block-title" data-i18n="Top P">
748 Top P748 Top P
749 </div>749 </div>
@@ -1314,7 +1314,7 @@
1314 <input class="neo-range-slider" type="range" id="epsilon_cutoff_textgenerationwebui" name="volume" min="0" max="9" step="0.01">1314 <input class="neo-range-slider" type="range" id="epsilon_cutoff_textgenerationwebui" name="volume" min="0" max="9" step="0.01">
1315 <input class="neo-range-input" type="number" min="0" max="9" step="0.01" data-for="epsilon_cutoff_textgenerationwebui" id="epsilon_cutoff_counter_textgenerationwebui">1315 <input class="neo-range-input" type="number" min="0" max="9" step="0.01" data-for="epsilon_cutoff_textgenerationwebui" id="epsilon_cutoff_counter_textgenerationwebui">
1316 </div>1316 </div>
1317 <div data-tg-type="aphrodite,koboldcpp,llamacpp" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">1317 <div data-tg-type="ooba,aphrodite,koboldcpp,llamacpp" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
1318 <small>1318 <small>
1319 <span data-i18n="Top nsigma">Top nsigma</span>1319 <span data-i18n="Top nsigma">Top nsigma</span>
1320 <div class="fa-solid fa-circle-info opacity50p" title="A sampling method that filters logits based on their statistical properties. It keeps tokens within n standard deviations of the maximum logit value, providing a simpler alternative to top-p/top-k sampling while maintaining sampling stability across different temperatures."></div>1320 <div class="fa-solid fa-circle-info opacity50p" title="A sampling method that filters logits based on their statistical properties. It keeps tokens within n standard deviations of the maximum logit value, providing a simpler alternative to top-p/top-k sampling while maintaining sampling stability across different temperatures."></div>
@@ -1499,7 +1499,7 @@
1499 </div>1499 </div>
1500 </div>1500 </div>
1501 </div>1501 </div>
1502 <div data-tg-type="ooba,infermaticai,koboldcpp,llamacpp,ollama,tabby" id="mirostat_block_ooba" class="wide100p">1502 <div data-tg-type="ooba,infermaticai,koboldcpp,llamacpp,tabby" id="mirostat_block_ooba" class="wide100p">
1503 <h4 class="wide100p textAlignCenter">1503 <h4 class="wide100p textAlignCenter">
1504 <label data-i18n="Mirostat (mode=1 is only for llama.cpp)">Mirostat</label>1504 <label data-i18n="Mirostat (mode=1 is only for llama.cpp)">Mirostat</label>
1505 <div class=" fa-solid fa-circle-info opacity50p " data-i18n="[title]Mirostat_desc" title="Mirostat is a thermostat for output perplexity.&#13;Mirostat matches the output perplexity to that of the input, thus avoiding the repetition trap&#13;(where, as the autoregressive inference produces text, the perplexity of the output tends toward zero)&#13;and the confusion trap (where the perplexity diverges).&#13;For details, see the paper Mirostat: A Neural Text Decoding Algorithm that Directly Controls Perplexity by Basu et al. (2020).&#13;Mode chooses the Mirostat version. 0=disable, 1=Mirostat 1.0 (llama.cpp only), 2=Mirostat 2.0."></div>1505 <div class=" fa-solid fa-circle-info opacity50p " data-i18n="[title]Mirostat_desc" title="Mirostat is a thermostat for output perplexity.&#13;Mirostat matches the output perplexity to that of the input, thus avoiding the repetition trap&#13;(where, as the autoregressive inference produces text, the perplexity of the output tends toward zero)&#13;and the confusion trap (where the perplexity diverges).&#13;For details, see the paper Mirostat: A Neural Text Decoding Algorithm that Directly Controls Perplexity by Basu et al. (2020).&#13;Mode chooses the Mirostat version. 0=disable, 1=Mirostat 1.0 (llama.cpp only), 2=Mirostat 2.0."></div>
@@ -1811,6 +1811,7 @@
1811 <div data-name="temperature" draggable="true"><span>Temperature</span><small></small></div>1811 <div data-name="temperature" draggable="true"><span>Temperature</span><small></small></div>
1812 <div data-name="dynamic_temperature" draggable="true"><span>Dynamic Temperature</span><small></small></div>1812 <div data-name="dynamic_temperature" draggable="true"><span>Dynamic Temperature</span><small></small></div>
1813 <div data-name="quadratic_sampling" draggable="true"><span>Quadratic / Smooth Sampling</span><small></small></div>1813 <div data-name="quadratic_sampling" draggable="true"><span>Quadratic / Smooth Sampling</span><small></small></div>
1814 <div data-name="top_n_sigma" draggable="true"><span>Top Nsigma</span><small></small></div>
1814 <div data-name="top_k" draggable="true"><span>Top K</span><small></small></div>1815 <div data-name="top_k" draggable="true"><span>Top K</span><small></small></div>
1815 <div data-name="top_p" draggable="true"><span>Top P</span><small></small></div>1816 <div data-name="top_p" draggable="true"><span>Top P</span><small></small></div>
1816 <div data-name="typical_p" draggable="true"><span>Typical P</span><small></small></div>1817 <div data-name="typical_p" draggable="true"><span>Typical P</span><small></small></div>
@@ -1969,7 +1970,7 @@
1969 </span>1970 </span>
1970 </div>1971 </div>
1971 </div>1972 </div>
1972 <div class="range-block" data-source="makersuite,vertexai,aimlapi,openrouter,claude,xai">1973 <div class="range-block" data-source="makersuite,vertexai,aimlapi,openrouter,claude,xai,nanogpt">
1973 <label for="openai_enable_web_search" class="checkbox_label flexWrap widthFreeExpand">1974 <label for="openai_enable_web_search" class="checkbox_label flexWrap widthFreeExpand">
1974 <input id="openai_enable_web_search" type="checkbox" />1975 <input id="openai_enable_web_search" type="checkbox" />
1975 <span data-i18n="Enable web search">Enable web search</span>1976 <span data-i18n="Enable web search">Enable web search</span>
@@ -2078,7 +2079,7 @@
2078 </span>2079 </span>
2079 </div>2080 </div>
2080 </div>2081 </div>
2081 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations">2082 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations,perplexity">
2082 <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#10;Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response." data-i18n="[title]Constrains effort on reasoning for reasoning models.">2083 <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#10;Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response." data-i18n="[title]Constrains effort on reasoning for reasoning models.">
2083 <label for="openai_reasoning_effort">2084 <label for="openai_reasoning_effort">
2084 <span data-i18n="Reasoning Effort">Reasoning Effort</span>2085 <span data-i18n="Reasoning Effort">Reasoning Effort</span>
@@ -2092,7 +2093,7 @@
2092 <option data-i18n="openai_reasoning_effort_high" value="high">High</option>2093 <option data-i18n="openai_reasoning_effort_high" value="high">High</option>
2093 <option data-i18n="openai_reasoning_effort_maximum" value="max">Maximum</option>2094 <option data-i18n="openai_reasoning_effort_maximum" value="max">Maximum</option>
2094 </select>2095 </select>
2095 <div class="toggle-description justifyLeft marginBot5" data-source="openai,custom,xai,aimlapi,openrouter" data-i18n="OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.">2096 <div class="toggle-description justifyLeft marginBot5" data-source="openai,custom,xai,aimlapi,openrouter,perplexity" data-i18n="OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.">
2096 OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.2097 OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.
2097 </div>2098 </div>
2098 <div class="toggle-description justifyLeft marginBot5" data-source="claude" data-i18n="Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.">2099 <div class="toggle-description justifyLeft marginBot5" data-source="claude" data-i18n="Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.">
@@ -2131,7 +2132,7 @@
2131 </div>2132 </div>
2132 </div>2133 </div>
2133 </div>2134 </div>
2134 <div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,scale,custom">2135 <div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,custom">
2135 <div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias">2136 <div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias">
2136 Logit Bias2137 Logit Bias
2137 </div>2138 </div>
@@ -2801,8 +2802,6 @@
2801 <option value="openrouter">OpenRouter</option>2802 <option value="openrouter">OpenRouter</option>
2802 <option value="perplexity">Perplexity</option>2803 <option value="perplexity">Perplexity</option>
2803 <option value="pollinations">Pollinations</option>2804 <option value="pollinations">Pollinations</option>
2804 <option value="scale">Scale</option>
2805 <option value="windowai">Window AI</option>
2806 <option value="xai">xAI (Grok)</option>2805 <option value="xai">xAI (Grok)</option>
2807 </optgroup>2806 </optgroup>
2808 </select>2807 </select>
@@ -3022,34 +3021,11 @@
3022 <option value="claude-3-5-haiku-latest">claude-3-5-haiku-latest</option>3021 <option value="claude-3-5-haiku-latest">claude-3-5-haiku-latest</option>
3023 <option value="claude-3-5-haiku-20241022">claude-3-5-haiku-20241022</option>3022 <option value="claude-3-5-haiku-20241022">claude-3-5-haiku-20241022</option>
3024 <option value="claude-3-opus-20240229">claude-3-opus-20240229</option>3023 <option value="claude-3-opus-20240229">claude-3-opus-20240229</option>
3025 <option value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
3026 <option value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>3024 <option value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
3027 <option value="claude-2.1">claude-2.1</option>
3028 <option value="claude-2.0">claude-2.0</option>
3029 </optgroup>3025 </optgroup>
3030 </select>3026 </select>
3031 </div>3027 </div>
3032 </form>3028 </form>
3033 <form id="windowai_form" data-source="windowai" action="javascript:void(null);" method="post" enctype="multipart/form-data">
3034 <div>
3035 <h4 data-i18n="Window AI Model">Window AI Model</h4>
3036 <select id="model_windowai_select">
3037 <option data-i18n="Use extension settings" value="">Use extension settings</option>
3038 <option value="openai/gpt-3.5-turbo">openai/gpt-3.5-turbo</option>
3039 <option value="openai/gpt-3.5-turbo-16k">openai/gpt-3.5-turbo-16k</option>
3040 <option value="openai/gpt-4">openai/gpt-4</option>
3041 <option value="openai/gpt-4-32k">openai/gpt-4-32k</option>
3042 <option value="anthropic/claude-instant-v1">anthropic/claude-instant-v1</option>
3043 <option value="anthropic/claude-instant-v1-100k">anthropic/claude-instant-v1-100k</option>
3044 <option value="anthropic/claude-v1">anthropic/claude-v1</option>
3045 <option value="anthropic/claude-v1-100k">anthropic/claude-v1-100k</option>
3046 <option value="google/palm-2-chat-bison">google/palm-2-chat-bison</option>
3047 <option value="google/palm-2-codechat-bison">google/palm-2-codechat-bison</option>
3048 <option value="togethercomputer/GPT-NeoXT-Chat-Base-20B">togethercomputer/GPT-NeoXT-Chat-Base-20B</option>
3049 <option value="cohere/command-nightly">cohere/command-nightly</option>
3050 </select>
3051 </div>
3052 </form>
3053 <form id="openrouter_form" data-source="openrouter" action="javascript:void(null);" method="post" enctype="multipart/form-data">3029 <form id="openrouter_form" data-source="openrouter" action="javascript:void(null);" method="post" enctype="multipart/form-data">
3054 <h4 data-i18n="OpenRouter API Key">OpenRouter API Key</h4>3030 <h4 data-i18n="OpenRouter API Key">OpenRouter API Key</h4>
3055 <div>3031 <div>
@@ -3119,36 +3095,6 @@
3119 <span data-i18n="To use instruct formatting, switch to OpenRouter under Text Completion API.">To use instruct formatting, switch to OpenRouter under Text Completion API.</span>3095 <span data-i18n="To use instruct formatting, switch to OpenRouter under Text Completion API.">To use instruct formatting, switch to OpenRouter under Text Completion API.</span>
3120 </small>3096 </small>
3121 </form>3097 </form>
3122 <form id="scale_form" data-source="scale" action="javascript:void(null);" method="post" enctype="multipart/form-data">
3123 <div id="normal_scale_form">
3124 <h4 data-i18n="Scale API Key">Scale API Key</h4>
3125 <div class="flex-container">
3126 <input id="api_key_scale" name="api_key_scale" class="text_pole flex1" value="" autocomplete="off">
3127 <div title="Manage API keys" data-i18n="[title]Manage API keys" class="menu_button fa-solid fa-key fa-fw manage-api-keys" data-key="api_key_scale"></div>
3128 </div>
3129 <div data-for="api_key_scale" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">
3130 For privacy reasons, your API key will be hidden after you click 'Connect'.
3131 </div>
3132 <h4>Scale API URL</h4>
3133 <input id="api_url_scale" name="api_url_scale" class="text_pole" value="" autocomplete="off" placeholder="https://dashboard.scale.com/spellbook/api/v2/deploy/xxxxxxx">
3134 </div>
3135 <div id="alt_scale_form">
3136 <h4>Scale Cookie (_jwt)</h4>
3137 <div class="flex-container">
3138 <input id="scale_cookie" name="scale_cookie" class="text_pole flex1" value="" autocomplete="off">
3139 <div title="Clear your cookie" data-i18n="[title]Clear your cookie" class="menu_button fa-solid fa-key fa-fw manage-api-keys" data-key="scale_cookie"></div>
3140 </div>
3141 <div data-for="scale_cookie" class="neutral_warning">
3142 For privacy reasons, your cookie will be hidden after you click 'Connect'.
3143 </div>
3144 </div>
3145 <!-- Its only purpose is to trigger max context size check -->
3146 <select id="model_scale_select" class="displayNone"></select>
3147 <label for="scale-alt" class="checkbox_label">
3148 <input id="scale-alt" type="checkbox" checked>
3149 <span data-i18n="Alt Method">Alt Method</span>
3150 </label>
3151 </form>
3152 <form id="ai21_form" data-source="ai21" action="javascript:void(null);" method="post" enctype="multipart/form-data">3098 <form id="ai21_form" data-source="ai21" action="javascript:void(null);" method="post" enctype="multipart/form-data">
3153 <h4 data-i18n="AI21 API Key">AI21 API Key</h4>3099 <h4 data-i18n="AI21 API Key">AI21 API Key</h4>
3154 <div class="flex-container">3100 <div class="flex-container">
@@ -3165,7 +3111,11 @@
3165 <option value="jamba-mini">jamba-mini</option>3111 <option value="jamba-mini">jamba-mini</option>
3166 <option value="jamba-large">jamba-large</option>3112 <option value="jamba-large">jamba-large</option>
3167 </optgroup>3113 </optgroup>
3168 <optgroup label="Jamba 1.6">3114 <optgroup label="Jamba 1.7">
3115 <option value="jamba-1.7-mini">jamba-1.7-mini</option>
3116 <option value="jamba-1.7-large">jamba-1.7-large</option>
3117 </optgroup>
3118 <optgroup label="Jamba 1.6 (Deprecated)">
3169 <option value="jamba-1.6-mini">jamba-1.6-mini</option>3119 <option value="jamba-1.6-mini">jamba-1.6-mini</option>
3170 <option value="jamba-1.6-large">jamba-1.6-large</option>3120 <option value="jamba-1.6-large">jamba-1.6-large</option>
3171 </optgroup>3121 </optgroup>
@@ -3415,6 +3365,7 @@
3415 <option value="pixtral-12b-latest">pixtral-12b-latest</option>3365 <option value="pixtral-12b-latest">pixtral-12b-latest</option>
3416 <option value="pixtral-large-latest">pixtral-large-latest</option>3366 <option value="pixtral-large-latest">pixtral-large-latest</option>
3417 <option value="devstral-small-latest">devstral-small-latest</option>3367 <option value="devstral-small-latest">devstral-small-latest</option>
3368 <option value="devstral-medium-latest">devstral-medium-latest</option>
3418 <option value="magistral-small-latest">magistral-small-latest</option>3369 <option value="magistral-small-latest">magistral-small-latest</option>
3419 <option value="magistral-medium-latest">magistral-medium-latest</option>3370 <option value="magistral-medium-latest">magistral-medium-latest</option>
3420 </optgroup>3371 </optgroup>
@@ -3430,6 +3381,7 @@
3430 <option value="mistral-small-2409">mistral-small-2409</option>3381 <option value="mistral-small-2409">mistral-small-2409</option>
3431 <option value="mistral-small-2501">mistral-small-2501</option>3382 <option value="mistral-small-2501">mistral-small-2501</option>
3432 <option value="mistral-small-2503">mistral-small-2503</option>3383 <option value="mistral-small-2503">mistral-small-2503</option>
3384 <option value="mistral-small-2506">mistral-small-2506</option>
3433 <option value="mistral-medium-2312">mistral-medium-2312</option>3385 <option value="mistral-medium-2312">mistral-medium-2312</option>
3434 <option value="mistral-medium-2505">mistral-medium-2505</option>3386 <option value="mistral-medium-2505">mistral-medium-2505</option>
3435 <option value="mistral-large-2402">mistral-large-2402</option>3387 <option value="mistral-large-2402">mistral-large-2402</option>
@@ -3446,8 +3398,12 @@
3446 <option value="pixtral-12b-2409">pixtral-12b-2409</option>3398 <option value="pixtral-12b-2409">pixtral-12b-2409</option>
3447 <option value="pixtral-large-2411">pixtral-large-2411</option>3399 <option value="pixtral-large-2411">pixtral-large-2411</option>
3448 <option value="devstral-small-2505">devstral-small-2505</option>3400 <option value="devstral-small-2505">devstral-small-2505</option>
3401 <option value="devstral-small-2507">devstral-small-2507</option>
3402 <option value="devstral-medium-2507">devstral-medium-2507</option>
3449 <option value="magistral-small-2506">magistral-small-2506</option>3403 <option value="magistral-small-2506">magistral-small-2506</option>
3450 <option value="magistral-medium-2506">magistral-medium-2506</option>3404 <option value="magistral-medium-2506">magistral-medium-2506</option>
3405 <option value="magistral-small-2507">magistral-small-2507</option>
3406 <option value="magistral-medium-2507">magistral-medium-2507</option>
3451 </optgroup>3407 </optgroup>
3452 <optgroup id="mistralai_other_models" label="Other"></optgroup>3408 <optgroup id="mistralai_other_models" label="Other"></optgroup>
3453 </select>3409 </select>
@@ -3464,35 +3420,17 @@
3464 </div>3420 </div>
3465 <h4 data-i18n="Groq Model">Groq Model</h4>3421 <h4 data-i18n="Groq Model">Groq Model</h4>
3466 <select id="model_groq_select">3422 <select id="model_groq_select">
3467 <optgroup label="Alibaba Cloud">3423 <option value="qwen/qwen3-32b">qwen/qwen3-32b</option>
3468 <option value="qwen-2.5-32b">qwen-2.5-32b</option>3424 <option value="deepseek-r1-distill-llama-70b">deepseek-r1-distill-llama-70b</option>
3469 <option value="qwen-2.5-coder-32b">qwen-2.5-coder-32b</option>3425 <option value="gemma2-9b-it">gemma2-9b-it</option>
3470 </optgroup>3426 <option value="meta-llama/llama-4-scout-17b-16e-instruct">meta-llama/llama-4-scout-17b-16e-instruct</option>
3471 <optgroup label="DeepSeek">3427 <option value="meta-llama/llama-4-maverick-17b-128e-instruct">meta-llama/llama-4-maverick-17b-128e-instruct</option>
3472 <option value="deepseek-r1-distill-qwen-32b">deepseek-r1-distill-qwen-32b</option>3428 <option value="llama-3.1-8b-instant">llama-3.1-8b-instant</option>
3473 <option value="deepseek-r1-distill-llama-70b">deepseek-r1-distill-llama-70b</option>3429 <option value="llama-3.3-70b-versatile">llama-3.3-70b-versatile </option>
3474 </optgroup>3430 <option value="llama-guard-3-8b">llama-guard-3-8b</option>
3475 <optgroup label="Google">3431 <option value="llama3-70b-8192">llama3-70b-8192</option>
3476 <option value="gemma2-9b-it">gemma2-9b-it</option>3432 <option value="llama3-8b-8192">llama3-8b-8192</option>
3477 </optgroup>3433 <option value="mistral-saba-24b">mistral-saba-24b</option>
3478 <optgroup label="Meta">
3479 <option value="meta-llama/llama-4-scout-17b-16e-instruct">meta-llama/llama-4-scout-17b-16e-instruct</option>
3480 <option value="meta-llama/llama-4-maverick-17b-128e-instruct">meta-llama/llama-4-maverick-17b-128e-instruct</option>
3481 <option value="llama-3.1-8b-instant">llama-3.1-8b-instant</option>
3482 <option value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview </option>
3483 <option value="llama-3.2-1b-preview">llama-3.2-1b-preview </option>
3484 <option value="llama-3.2-3b-preview">llama-3.2-3b-preview </option>
3485 <option value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview </option>
3486 <option value="llama-3.3-70b-specdec">llama-3.3-70b-specdec</option>
3487 <option value="llama-3.3-70b-versatile">llama-3.3-70b-versatile </option>
3488 <option value="llama-guard-3-8b">llama-guard-3-8b</option>
3489 <option value="llama3-70b-8192">llama3-70b-8192</option>
3490 <option value="llama3-8b-8192">llama3-8b-8192</option>
3491 </optgroup>
3492 <optgroup label="Mistral AI">
3493 <option value="mistral-saba-24b">mistral-saba-24b</option>
3494 <option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
3495 </optgroup>
3496 </select>3434 </select>
3497 </div>3435 </div>
3498 <div id="nanogpt_form" data-source="nanogpt">3436 <div id="nanogpt_form" data-source="nanogpt">
@@ -3545,6 +3483,7 @@
3545 <option value="sonar-pro">sonar-pro</option>3483 <option value="sonar-pro">sonar-pro</option>
3546 <option value="sonar-reasoning">sonar-reasoning</option>3484 <option value="sonar-reasoning">sonar-reasoning</option>
3547 <option value="sonar-reasoning-pro">sonar-reasoning-pro</option>3485 <option value="sonar-reasoning-pro">sonar-reasoning-pro</option>
3486 <option value="sonar-deep-research">sonar-deep-research</option>
3548 </optgroup>3487 </optgroup>
3549 <optgroup label="Offline Models">3488 <optgroup label="Offline Models">
3550 <option value="r1-1776">r1-1776</option>3489 <option value="r1-1776">r1-1776</option>
@@ -3659,6 +3598,7 @@
3659 </div>3598 </div>
3660 <h4 data-i18n="xAI Model">xAI Model</h4>3599 <h4 data-i18n="xAI Model">xAI Model</h4>
3661 <select id="model_xai_select">3600 <select id="model_xai_select">
3601 <option value="grok-4-0709">grok-4-0709</option>
3662 <option value="grok-3-beta">grok-3-beta</option>3602 <option value="grok-3-beta">grok-3-beta</option>
3663 <option value="grok-3-fast-beta">grok-3-fast-beta</option>3603 <option value="grok-3-fast-beta">grok-3-fast-beta</option>
3664 <option value="grok-3-mini-beta">grok-3-mini-beta</option>3604 <option value="grok-3-mini-beta">grok-3-mini-beta</option>
@@ -3728,7 +3668,7 @@
3728 <div class="api_loading menu_button menu_button_icon" data-i18n="Cancel">Cancel</div>3668 <div class="api_loading menu_button menu_button_icon" data-i18n="Cancel">Cancel</div>
3729 <div data-source="custom" id="customize_additional_parameters" class="menu_button menu_button_icon" data-i18n="Additional Parameters">Additional Parameters</div>3669 <div data-source="custom" id="customize_additional_parameters" class="menu_button menu_button_icon" data-i18n="Additional Parameters">Additional Parameters</div>
3730 <div data-source="openrouter" class="menu_button menu_button_icon openrouter_authorize" title="Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai" data-i18n="Authorize;[title]Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai">Authorize</div>3670 <div data-source="openrouter" class="menu_button menu_button_icon openrouter_authorize" title="Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai" data-i18n="Authorize;[title]Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai">Authorize</div>
3731 <div id="test_api_button" class="menu_button menu_button_icon" title="Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!" data-i18n="[title]Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!"><span data-i18n="Test Message">Test Message</span></div>3671 <div id="test_api_button" class="menu_button menu_button_icon" title="Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!" data-i18n="Test Message;[title]Verifies your API connection by sending a short test message. Be aware that you'll be credited for it!">Test Message</div>
3732 </div>3672 </div>
3733 <div class="online_status">3673 <div class="online_status">
3734 <div class="online_status_indicator"></div>3674 <div class="online_status_indicator"></div>
@@ -4241,6 +4181,13 @@
4241 </div>4181 </div>
4242 <div>4182 <div>
4243 <h4 class="standoutHeader" data-i18n="Miscellaneous">Miscellaneous</h4>4183 <h4 class="standoutHeader" data-i18n="Miscellaneous">Miscellaneous</h4>
4184 <div name="bindModelPresetBlock">
4185 <label for="bind_model_templates" class="checkbox_label">
4186 <input id="bind_model_templates" type="checkbox" />
4187 <small data-i18n="Bind Model to Templates">Bind Model to Templates</small>
4188 <span class="fa-solid fa-circle-question" data-i18n="[title]bind_model_templates_desc" title="When connecting to an API or choosing a model, automatically activate the current Instruct and Context templates if the model name or its chat template matches the currently loaded model."></span>
4189 </label>
4190 </div>
4244 <div>4191 <div>
4245 <small>4192 <small>
4246 <span data-i18n="Non-markdown strings">4193 <span data-i18n="Non-markdown strings">
@@ -4307,7 +4254,7 @@
4307 </span>4254 </span>
4308 </div>4255 </div>
4309 <div class="range-block-range">4256 <div class="range-block-range">
4310 <select id="world_info" class="select2_multi_sameline" multiple>4257 <select id="world_info" multiple>
4311 <option value="" data-i18n="-- World Info not found --">-- World Info not found -- </option>4258 <option value="" data-i18n="-- World Info not found --">-- World Info not found -- </option>
4312 </select>4259 </select>
4313 </div>4260 </div>
@@ -4800,10 +4747,10 @@
4800 <input id="never_resize_avatars" type="checkbox" />4747 <input id="never_resize_avatars" type="checkbox" />
4801 <small data-i18n="Never resize avatars">Never resize avatars</small>4748 <small data-i18n="Never resize avatars">Never resize avatars</small>
4802 </label>4749 </label>
4803 <label data-i18n="[title]Allow animations for WEBP backgrounds. This is only a change for the selection menu." for="background_thumbnails_animation" class="checkbox_label" title="Allow animations for WEBP backgrounds. This is only a change for the selection menu.">4750 <label data-i18n="[title]Allow animations for WEBP backgrounds. This is only a change for the selection menu." for="background_thumbnails_animation" class="checkbox_label" title="Allow animations for WEBP backgrounds. This is only a change for the selection menu.">
4804 <input id="background_thumbnails_animation" type="checkbox" />4751 <input id="background_thumbnails_animation" type="checkbox" />
4805 <small data-i18n="Animated background thumbnails">Animated background thumbnails</small>4752 <small data-i18n="Animated background thumbnails">Animated background thumbnails</small>
4806 </label>4753 </label>
4807 <label class="checkbox_label" for="show_card_avatar_urls" title="Show actual file names on the disk, in the characters list display only." data-i18n="[title]Show actual file names on the disk, in the characters list display only">4754 <label class="checkbox_label" for="show_card_avatar_urls" title="Show actual file names on the disk, in the characters list display only." data-i18n="[title]Show actual file names on the disk, in the characters list display only">
4808 <input id="show_card_avatar_urls" type="checkbox" />4755 <input id="show_card_avatar_urls" type="checkbox" />
4809 <small data-i18n="Show avatar filenames">Show avatar filenames</small>4756 <small data-i18n="Show avatar filenames">Show avatar filenames</small>
@@ -5522,7 +5469,6 @@
5522 <div id="HotSwapWrapper" class="alignitemscenter flex-container margin0auto wide100p">5469 <div id="HotSwapWrapper" class="alignitemscenter flex-container margin0auto wide100p">
5523 <div class="hotswap avatars_inline scroll-reset-container expander" data-i18n="[no_favs]Favorite characters to add them to HotSwaps" no_favs="Favorite characters to add them to HotSwaps">5470 <div class="hotswap avatars_inline scroll-reset-container expander" data-i18n="[no_favs]Favorite characters to add them to HotSwaps" no_favs="Favorite characters to add them to HotSwaps">
5524 </div>5471 </div>
5525 <hr class="wide100p">
5526 </div>5472 </div>
5527 </div>5473 </div>
55285474
@@ -5638,19 +5584,26 @@
5638 </div>5584 </div>
5639 </div>5585 </div>
5640 </div>5586 </div>
5641 <hr>5587 <div id="spoiler_free_desc" class="inline-drawer flex-container flexFlowColumn flexNoGap">
5642 <div id="spoiler_free_desc" class="flex-container flexFlowColumn flex1 flexNoGap">5588 <div class="inline-drawer-toggle inline-drawer-header padding0 gap5px standoutHeader">
5643 <div id="creators_notes_div" class="title_restorable flexGap5">5589 <div id="creators_notes_div" class="title_restorable flexGap5 wide100p ">
5644 <span class="flex1" data-i18n="Creator's Notes">Creator's Notes</span>5590 <span class="flex1" data-i18n="Creator's Notes">Creator's Notes</span>
5645 <small id="creators_note_desc_hidden" data-i18n="Character details are hidden.">Character details are hidden.</small>5591 <div id="creators_note_styles_button" class="margin0 menu_button fa-solid fa-palette fa-fw" title="Allow / Forbid the use of global styles for this character." data-i18n="[title]Allow / Forbid the use of global styles for this character."></div>
5646 <div id="creators_note_styles_button" class="margin0 menu_button fa-solid fa-palette fa-fw" title="Allow / Forbid the use of global styles for this character." data-i18n="[title]Allow / Forbid the use of global styles for this character."></div>5592 <div id="spoiler_free_desc_button" class="margin0 menu_button fa-solid fa-eye fa-fw" title="Show / Hide Description and First Message" data-i18n="[title]Show / Hide Description and First Message"></div>
5647 <div id="spoiler_free_desc_button" class="margin0 menu_button fa-solid fa-eye fa-fw" title="Show / Hide Description and First Message" data-i18n="[title]Show / Hide Description and First Message"></div>5593 </div>
5594 <div class="flex-container widthFitContent">
5595 <div class="inline-drawer-icon fa-solid fa-circle-chevron-down down interactable"></div>
5596 </div>
5597 </div>
5598 <div class="inline-drawer-content">
5599 <div id="creator_notes_spoiler"></div>
5600 <div id="creator_notes_empty" data-i18n="No Creator's Notes provided.">
5601 No Creator's Notes provided.
5602 </div>
5648 </div>5603 </div>
5649 <div id="creator_notes_spoiler" class="flex1"></div>
5650 <!-- A button to show / hide description_div and description_textarea and first_message_div and firstmessage_textarea-->
5651 </div>5604 </div>
5605 <small id="creators_note_desc_hidden" data-i18n="Character details are hidden.">Character details are hidden.</small>
5652 <div id="descriptionWrapper" class="flex-container flexFlowColumn flex1">5606 <div id="descriptionWrapper" class="flex-container flexFlowColumn flex1">
5653 <hr>
5654 <div id="description_div" class="title_restorable">5607 <div id="description_div" class="title_restorable">
5655 <div class="flex-container alignitemscenter">5608 <div class="flex-container alignitemscenter">
5656 <span data-i18n="Character Description" class="mdhotkey_location">Description</span>5609 <span data-i18n="Character Description" class="mdhotkey_location">Description</span>
@@ -5843,20 +5796,22 @@
5843 </div>5796 </div>
5844 <div id="rm_character_import" class="right_menu" style="display: none;">5797 <div id="rm_character_import" class="right_menu" style="display: none;">
5845 <form id="form_import" action="javascript:void(null);" method="post" enctype="multipart/form-data">5798 <form id="form_import" action="javascript:void(null);" method="post" enctype="multipart/form-data">
5846 <input multiple type="file" id="character_import_file" accept=".json, image/png, .yaml, .yml, .charx" name="avatar">5799 <input multiple type="file" id="character_import_file" accept=".json, image/png, .yaml, .yml, .charx, .byaf" name="avatar">
5847 <input id="character_import_file_type" name="file_type" class="text_pole" value="" autocomplete="off">5800 <input id="character_import_file_type" name="file_type" class="text_pole" value="" autocomplete="off">
5848 </form>5801 </form>
5849 <input type="file" id="character_replace_file" accept=".json, image/png, .yaml, .yml, .charx" name="replace_avatar" hidden>5802 <input type="file" id="character_replace_file" accept=".json, image/png, .yaml, .yml, .charx, .byaf" name="replace_avatar" hidden>
5850 </div>5803 </div>
5851 <div name="Character List Panel" id="rm_characters_block" class="right_menu">5804 <div name="Character List Panel" id="rm_characters_block" class="right_menu">
5852 <div id="charListFixedTop">5805 <div id="charListFixedTop">
5853 <form id="form_character_search_form" action="javascript:void(null);">5806 <div id="rm_button_bar">
5854 <div id="rm_button_create" title="Create New Character" data-i18n="[title]Create New Character" class="menu_button fa-solid fa-user-plus "></div>5807 <div id="rm_button_create" title="Create New Character" data-i18n="[title]Create New Character" class="menu_button fa-solid fa-user-plus"></div>
5855 <div id="character_import_button" title="Import Character from File" data-i18n="[title]Import Character from File" class="menu_button fa-solid fa-file-import faSmallFontSquareFix"></div>5808 <div id="character_import_button" title="Import Character from File" data-i18n="[title]Import Character from File" class="menu_button fa-solid fa-file-import"></div>
5856 <div id="external_import_button" title="Import content from external URL" data-i18n="[title]Import content from external URL" class="menu_button fa-solid fa-cloud-arrow-down faSmallFontSquareFix"></div>5809 <div id="external_import_button" title="Import content from external URL" data-i18n="[title]Import content from external URL" class="menu_button fa-solid fa-cloud-arrow-down"></div>
5857 <div id="rm_button_group_chats" title="Create New Chat Group" data-i18n="[title]Create New Chat Group" class="menu_button fa-solid fa-users-gear "></div>5810 <div id="rm_button_group_chats" title="Create New Chat Group" data-i18n="[title]Create New Chat Group" class="menu_button fa-solid fa-users-gear"></div>
5858 <input id="character_search_bar" class="text_pole width100p" type="search" data-i18n="[placeholder]Search..." placeholder="Search..." />5811 <div id="rm_buttons_container">
5859 <select id="character_sort_order" title="Characters sorting order" data-i18n="[title]Characters sorting order">5812 <!-- Container for additional buttons added by extensions -->
5813 </div>
5814 <select id="character_sort_order" class="flex1 text_pole textarea_compact" title="Characters sorting order" data-i18n="[title]Characters sorting order">
5860 <option data-field="search" data-order="desc" data-i18n="Search" hidden>Search</option>5815 <option data-field="search" data-order="desc" data-i18n="Search" hidden>Search</option>
5861 <option data-field="name" data-order="asc" data-i18n="A-Z">A-Z</option>5816 <option data-field="name" data-order="asc" data-i18n="A-Z">A-Z</option>
5862 <option data-field="name" data-order="desc" data-i18n="Z-A">Z-A</option>5817 <option data-field="name" data-order="desc" data-i18n="Z-A">Z-A</option>
@@ -5870,12 +5825,15 @@
5870 <option data-field="data_size" data-order="asc" data-i18n="Least tokens">Least tokens</option>5825 <option data-field="data_size" data-order="asc" data-i18n="Least tokens">Least tokens</option>
5871 <option data-field="name" data-order="random" data-i18n="Random">Random</option>5826 <option data-field="name" data-order="random" data-i18n="Random">Random</option>
5872 </select>5827 </select>
5873 </form>5828 <div id="rm_button_search" class="right_menu_button fa-fw fa-solid fa-search" title="Toggle search bar" data-i18n="[title]Toggle search bar"></div>
5829 </div>
5830 <div id="form_character_search_form">
5831 <input id="character_search_bar" class="text_pole textarea_compact width100p" type="search" data-i18n="[placeholder]Search..." placeholder="Search..." />
5832 </div>
5874 <div class="rm_tag_controls">5833 <div class="rm_tag_controls">
5875 <div class="tags rm_tag_filter"></div>5834 <div class="tags rm_tag_filter"></div>
5876 <div class="tags rm_tag_bogus_drilldown"></div>5835 <div class="tags rm_tag_bogus_drilldown"></div>
5877 </div>5836 </div>
5878 <hr>
5879 </div>5837 </div>
58805838
5881 <div id="rm_print_characters_pagination">5839 <div id="rm_print_characters_pagination">
@@ -5927,6 +5885,7 @@
5927 <div class="flex-container wide100pLess70px character_select_container">5885 <div class="flex-container wide100pLess70px character_select_container">
5928 <div class="wide100p character_name_block">5886 <div class="wide100p character_name_block">
5929 <span class="ch_name flex1"></span>5887 <span class="ch_name flex1"></span>
5888 <small class="ch_additional_info"></small>
5930 </div>5889 </div>
5931 <div class="ch_description"></div>5890 <div class="ch_description"></div>
5932 <div class="avatar_container_states buttons_block">5891 <div class="avatar_container_states buttons_block">
@@ -6273,7 +6232,7 @@
6273 </span>6232 </span>
6274 </small>6233 </small>
6275 <small class="textAlignCenter" data-i18n="Primary Keywords">Primary Keywords</small>6234 <small class="textAlignCenter" data-i18n="Primary Keywords">Primary Keywords</small>
6276 <select class="keyprimaryselect keyselect select2_multi_sameline" name="key" data-i18n="[placeholder]Keywords or Regexes" placeholder="Keywords or Regexes" multiple="multiple"></select>6235 <select class="keyprimaryselect keyselect" name="key" data-i18n="[placeholder]Keywords or Regexes" placeholder="Keywords or Regexes" multiple="multiple"></select>
6277 <textarea class="text_pole keyprimarytextpole mobile" name="key" rows="1" data-i18n="[placeholder]Comma separated list" placeholder="Comma separated list" style="display: none;"></textarea>6236 <textarea class="text_pole keyprimarytextpole mobile" name="key" rows="1" data-i18n="[placeholder]Comma separated list" placeholder="Comma separated list" style="display: none;"></textarea>
6278 <button type="button" class="switch_input_type_icon" tabindex="-1" title="Switch to plaintext mode" data-i18n="[title]Switch to plaintext mode" data-icon-on="✨" data-icon-off="⌨️" data-tooltip-on="Switch to fancy mode" data-tooltip-off="Switch to plaintext mode">6237 <button type="button" class="switch_input_type_icon" tabindex="-1" title="Switch to plaintext mode" data-i18n="[title]Switch to plaintext mode" data-icon-on="✨" data-icon-off="⌨️" data-tooltip-on="Switch to fancy mode" data-tooltip-off="Switch to plaintext mode">
6279 ⌨️6238 ⌨️
@@ -6295,7 +6254,7 @@
6295 </span>6254 </span>
6296 </small>6255 </small>
6297 <small class="textAlignCenter" data-i18n="Optional Filter">Optional Filter</small>6256 <small class="textAlignCenter" data-i18n="Optional Filter">Optional Filter</small>
6298 <select class="keysecondaryselect keyselect select2_multi_sameline" name="keysecondary" data-i18n="[placeholder]Keywords or Regexes (ignored if empty)" placeholder="Keywords or Regexes (ignored if empty)" multiple="multiple"></select>6257 <select class="keysecondaryselect keyselect" name="keysecondary" data-i18n="[placeholder]Keywords or Regexes (ignored if empty)" placeholder="Keywords or Regexes (ignored if empty)" multiple="multiple"></select>
6299 <textarea class="text_pole keysecondarytextpole mobile" name="keysecondary" rows="1" data-i18n="[placeholder]Comma separated list (ignored if empty)" placeholder="Comma separated list (ignored if empty)" style="display: none;"></textarea>6258 <textarea class="text_pole keysecondarytextpole mobile" name="keysecondary" rows="1" data-i18n="[placeholder]Comma separated list (ignored if empty)" placeholder="Comma separated list (ignored if empty)" style="display: none;"></textarea>
6300 <button type="button" class="switch_input_type_icon" tabindex="-1" title="Switch to plaintext mode" data-i18n="[title]Switch to plaintext mode" data-icon-on="✨" data-icon-off="⌨️" data-tooltip-on="Switch to fancy mode" data-tooltip-off="Switch to plaintext mode">6259 <button type="button" class="switch_input_type_icon" tabindex="-1" title="Switch to plaintext mode" data-i18n="[title]Switch to plaintext mode" data-icon-on="✨" data-icon-off="⌨️" data-tooltip-on="Switch to fancy mode" data-tooltip-off="Switch to plaintext mode">
6301 ⌨️6260 ⌨️
@@ -6482,13 +6441,37 @@
6482 </label>6441 </label>
6483 </div>6442 </div>
6484 <div class="range-block-range">6443 <div class="range-block-range">
6485 <select name="characterFilter" class="select2_multi_sameline" multiple>6444 <select name="characterFilter" multiple>
6486 <option value="" data-i18n="-- Characters not found --">6445 <option value="" data-i18n="-- Characters not found --">
6487 -- Characters not found --6446 -- Characters not found --
6488 </option>6447 </option>
6489 </select>6448 </select>
6490 </div>6449 </div>
6491 </div>6450 </div>
6451 <div class="flex4">
6452 <div class="flex-container justifySpaceBetween">
6453 <small>
6454 <span data-i18n="Filter to Generation Triggers">
6455 Filter to Generation Triggers
6456 </span>
6457 </small>
6458 <!-- Not a real control. Used to make label heights even. -->
6459 <label class="checkbox_label" for="__invisible">
6460 <input type="checkbox" name="__invisible">
6461 <span><small>&nbsp;</small></span>
6462 </label>
6463 </div>
6464 <div class="range-block-range">
6465 <select name="triggers" multiple>
6466 <option data-i18n="Normal" value="normal">Normal</option>
6467 <option data-i18n="Continue" value="continue">Continue</option>
6468 <option data-i18n="Impersonate" value="impersonate">Impersonate</option>
6469 <option data-i18n="Swipe" value="swipe">Swipe</option>
6470 <option data-i18n="Regenerate" value="regenerate">Regenerate</option>
6471 <option data-i18n="Quiet" value="quiet">Quiet</option>
6472 </select>
6473 </div>
6474 </div>
6492 </div>6475 </div>
6493 <div name="WIEntryBottomControls" class="flex-container flex1 justifySpaceBetween world_entry_form_horizontal">6476 <div name="WIEntryBottomControls" class="flex-container flex1 justifySpaceBetween world_entry_form_horizontal">
6494 <div class="flex-container flexFlowColumn flexNoGap wi-enter-footer-text">6477 <div class="flex-container flexFlowColumn flexNoGap wi-enter-footer-text">
@@ -6719,6 +6702,22 @@
6719 </select>6702 </select>
6720 <div class="text_muted" data-i18n="To whom this message will be attributed.">To whom this message will be attributed.</div>6703 <div class="text_muted" data-i18n="To whom this message will be attributed.">To whom this message will be attributed.</div>
6721 </div>6704 </div>
6705 <div class="completion_prompt_manager_popup_entry_form_control flex1">
6706 <label for="completion_prompt_manager_popup_entry_form_injection_trigger">
6707 <span data-i18n="Triggers">Triggers</span>
6708 </label>
6709 <select id="completion_prompt_manager_popup_entry_form_injection_trigger" class="text_pole" name="injection_trigger" multiple>
6710 <option data-i18n="Normal" value="normal">Normal</option>
6711 <option data-i18n="Continue" value="continue">Continue</option>
6712 <option data-i18n="Impersonate" value="impersonate">Impersonate</option>
6713 <option data-i18n="Swipe" value="swipe">Swipe</option>
6714 <option data-i18n="Regenerate" value="regenerate">Regenerate</option>
6715 <option data-i18n="Quiet" value="quiet">Quiet</option>
6716 </select>
6717 <div class="text_muted" data-i18n="Filter to specific generation types.">
6718 Filter to specific generation types.
6719 </div>
6720 </div>
6722 </div>6721 </div>
6723 <div class="flex-container gap10px">6722 <div class="flex-container gap10px">
6724 <div class="completion_prompt_manager_popup_entry_form_control flex1">6723 <div class="completion_prompt_manager_popup_entry_form_control flex1">
@@ -7066,6 +7065,16 @@
7066 <div class="right_menu_button mes_file_delete fa-solid fa-trash-can" title="Remove the file" data-i18n="[title]Remove the file"></div>7065 <div class="right_menu_button mes_file_delete fa-solid fa-trash-can" title="Remove the file" data-i18n="[title]Remove the file"></div>
7067 </div>7066 </div>
7068 </div>7067 </div>
7068
7069 <div id="message_video_template" class="template_element">
7070 <div class="mes_video_container">
7071 <div class="mes_video_controls">
7072 <div><!-- Placeholder --></div>
7073 <div title="Delete" class="right_menu_button fa-lg fa-solid fa-trash-can mes_video_delete" data-i18n="[title]Delete"></div>
7074 </div>
7075 <video class="mes_video" controls preload="metadata"></video>
7076 </div>
7077 </div>
7069 </div>7078 </div>
7070 <div id="movingDivs">7079 <div id="movingDivs">
7071 <div id="floatingPrompt" class="drawer-content flexGap5">7080 <div id="floatingPrompt" class="drawer-content flexGap5">
public/lib.js+3 -0
@@ -21,6 +21,7 @@ import droll from 'droll';
21import morphdom from 'morphdom';21import morphdom from 'morphdom';
22import { toggle as slideToggle } from 'slidetoggle';22import { toggle as slideToggle } from 'slidetoggle';
23import chalk from 'chalk';23import chalk from 'chalk';
24import yaml from 'yaml';
2425
25/**26/**
26 * Expose the libraries to the 'window' object.27 * Expose the libraries to the 'window' object.
@@ -98,6 +99,7 @@ export default {
98 morphdom,99 morphdom,
99 slideToggle,100 slideToggle,
100 chalk,101 chalk,
102 yaml,
101};103};
102104
103export {105export {
@@ -121,4 +123,5 @@ export {
121 morphdom,123 morphdom,
122 slideToggle,124 slideToggle,
123 chalk,125 chalk,
126 yaml,
124};127};
public/locales/ar-sa.json+1 -3
@@ -375,9 +375,6 @@
375 "Group by vendors Description": "ضع نماذج OpenAI في مجموعة واحدة، والنماذج الإنسانية في مجموعة أخرى، وما إلى ذلك. ويمكن دمجها مع الفرز.",375 "Group by vendors Description": "ضع نماذج OpenAI في مجموعة واحدة، والنماذج الإنسانية في مجموعة أخرى، وما إلى ذلك. ويمكن دمجها مع الفرز.",
376 "Allow fallback routes": "السماح بمسارات الاحتياط",376 "Allow fallback routes": "السماح بمسارات الاحتياط",
377 "Allow fallback routes Description": "يختار النموذج البديل تلقائيًا إذا كان النموذج المحدد غير قادر على تلبية طلبك.",377 "Allow fallback routes Description": "يختار النموذج البديل تلقائيًا إذا كان النموذج المحدد غير قادر على تلبية طلبك.",
378 "Scale API Key": "مفتاح API لـ Scale",
379 "Clear your cookie": "امسح ملف تعريف الارتباط الخاص بك",
380 "Alt Method": "طريقة بديلة",
381 "AI21 API Key": "مفتاح API لـ AI21",378 "AI21 API Key": "مفتاح API لـ AI21",
382 "AI21 Model": "نموذج AI21",379 "AI21 Model": "نموذج AI21",
383 "Google AI Studio API Key": "مفتاح واجهة برمجة تطبيقات Google AI Studio",380 "Google AI Studio API Key": "مفتاح واجهة برمجة تطبيقات Google AI Studio",
@@ -1380,6 +1377,7 @@
1380 "char_import_7": "للمضيفين المسموح بهم)",1377 "char_import_7": "للمضيفين المسموح بهم)",
1381 "char_import_8": "شخصية RisuRealm (رابط مباشر)",1378 "char_import_8": "شخصية RisuRealm (رابط مباشر)",
1382 "char_import_9": "شخصية Soulkyn (رابط مباشر)",1379 "char_import_9": "شخصية Soulkyn (رابط مباشر)",
1380 "char_import_10": "شخصية Perchance (رابط مباشر أو UUID + .gz)",
1383 "Supports importing multiple characters.": "يدعم استيراد أحرف متعددة.",1381 "Supports importing multiple characters.": "يدعم استيراد أحرف متعددة.",
1384 "Write each URL or ID into a new line.": "اكتب كل عنوان URL أو معرف في سطر جديد.",1382 "Write each URL or ID into a new line.": "اكتب كل عنوان URL أو معرف في سطر جديد.",
1385 "Export for character": "تصدير للشخصية",1383 "Export for character": "تصدير للشخصية",
public/locales/de-de.json+1 -3
@@ -375,9 +375,6 @@
375 "Group by vendors Description": "Platzieren Sie OpenAI-Modelle in einer Gruppe, anthropogene Modelle in einer anderen Gruppe usw. Kann mit Sortierung kombiniert werden.",375 "Group by vendors Description": "Platzieren Sie OpenAI-Modelle in einer Gruppe, anthropogene Modelle in einer anderen Gruppe usw. Kann mit Sortierung kombiniert werden.",
376 "Allow fallback routes": "Fallback-Routen zulassen",376 "Allow fallback routes": "Fallback-Routen zulassen",
377 "Allow fallback routes Description": "Das alternative Modell wird automatisch ausgewählt, wenn das ausgewählte Modell Ihre Anfrage nicht erfüllen kann.",377 "Allow fallback routes Description": "Das alternative Modell wird automatisch ausgewählt, wenn das ausgewählte Modell Ihre Anfrage nicht erfüllen kann.",
378 "Scale API Key": "Scale API-Schlüssel",
379 "Clear your cookie": "Löschen Sie Ihre Cookies",
380 "Alt Method": "Alternative Methode",
381 "AI21 API Key": "AI21 API-Schlüssel",378 "AI21 API Key": "AI21 API-Schlüssel",
382 "AI21 Model": "AI21-Modell",379 "AI21 Model": "AI21-Modell",
383 "Google AI Studio API Key": "Google AI Studio API-Schlüssel",380 "Google AI Studio API Key": "Google AI Studio API-Schlüssel",
@@ -1380,6 +1377,7 @@
1380 "char_import_7": "für erlaubte Hosts)",1377 "char_import_7": "für erlaubte Hosts)",
1381 "char_import_8": "RisuRealm-Charakter (Direktlink)",1378 "char_import_8": "RisuRealm-Charakter (Direktlink)",
1382 "char_import_9": "Soulkyn-Charakter (Direktlink)",1379 "char_import_9": "Soulkyn-Charakter (Direktlink)",
1380 "char_import_10": "Perchance-Charakter (Direktlink oder UUID + .gz)",
1383 "Supports importing multiple characters.": "Unterstützt den Import mehrerer Zeichen.",1381 "Supports importing multiple characters.": "Unterstützt den Import mehrerer Zeichen.",
1384 "Write each URL or ID into a new line.": "Schreiben Sie jede URL oder ID in eine neue Zeile.",1382 "Write each URL or ID into a new line.": "Schreiben Sie jede URL oder ID in eine neue Zeile.",
1385 "Export for character": "Export für Zeichen",1383 "Export for character": "Export für Zeichen",
public/locales/es-es.json+1 -3
@@ -375,9 +375,6 @@
375 "Group by vendors Description": "Coloque los modelos OpenAI en un grupo, los modelos antrópicos en otro grupo, etc. Se puede combinar con la clasificación.",375 "Group by vendors Description": "Coloque los modelos OpenAI en un grupo, los modelos antrópicos en otro grupo, etc. Se puede combinar con la clasificación.",
376 "Allow fallback routes": "Permitir rutas de respaldo",376 "Allow fallback routes": "Permitir rutas de respaldo",
377 "Allow fallback routes Description": "El modelo alternativo se elige automáticamente si el modelo seleccionado no puede cumplir con tu solicitud.",377 "Allow fallback routes Description": "El modelo alternativo se elige automáticamente si el modelo seleccionado no puede cumplir con tu solicitud.",
378 "Scale API Key": "Clave API de Scale",
379 "Clear your cookie": "Limpia tu cookie",
380 "Alt Method": "Método alternativo",
381 "AI21 API Key": "Clave API de AI21",378 "AI21 API Key": "Clave API de AI21",
382 "AI21 Model": "Modelo de AI21",379 "AI21 Model": "Modelo de AI21",
383 "Google AI Studio API Key": "Clave API de Google AI Studio",380 "Google AI Studio API Key": "Clave API de Google AI Studio",
@@ -1380,6 +1377,7 @@
1380 "char_import_7": "para hosts permitidos)",1377 "char_import_7": "para hosts permitidos)",
1381 "char_import_8": "Personaje RisuRealm (Enlace directo)",1378 "char_import_8": "Personaje RisuRealm (Enlace directo)",
1382 "char_import_9": "Personaje Soulkyn (Enlace directo)",1379 "char_import_9": "Personaje Soulkyn (Enlace directo)",
1380 "char_import_10": "Personaje Perchance (enlace directo o UUID + .gz)",
1383 "Supports importing multiple characters.": "Admite la importación de múltiples caracteres.",1381 "Supports importing multiple characters.": "Admite la importación de múltiples caracteres.",
1384 "Write each URL or ID into a new line.": "Escriba cada URL o ID en una nueva línea.",1382 "Write each URL or ID into a new line.": "Escriba cada URL o ID en una nueva línea.",
1385 "Export for character": "Exportar para personaje",1383 "Export for character": "Exportar para personaje",
public/locales/fr-fr.json+1 -3
@@ -355,9 +355,6 @@
355 "Group by vendors": "Regrouper par fournisseurs",355 "Group by vendors": "Regrouper par fournisseurs",
356 "Group by vendors Description": "Placez les modèles OpenAI dans un groupe, les modèles Anthropic dans un autre groupe, etc. Peut être combiné avec le tri.",356 "Group by vendors Description": "Placez les modèles OpenAI dans un groupe, les modèles Anthropic dans un autre groupe, etc. Peut être combiné avec le tri.",
357 "Allow fallback routes Description": "Le modèle alternatif est automatiquement sélectionné si le modèle choisi ne peut pas répondre à votre demande.",357 "Allow fallback routes Description": "Le modèle alternatif est automatiquement sélectionné si le modèle choisi ne peut pas répondre à votre demande.",
358 "Scale API Key": "Clé API Scale",
359 "Clear your cookie": "Effacer vos cookies",
360 "Alt Method": "Méthode alternative",
361 "AI21 API Key": "Clé API AI21",358 "AI21 API Key": "Clé API AI21",
362 "AI21 Model": "Modèle AI21",359 "AI21 Model": "Modèle AI21",
363 "Google AI Studio API Key": "Clé API Google AI Studio",360 "Google AI Studio API Key": "Clé API Google AI Studio",
@@ -1302,6 +1299,7 @@
1302 "char_import_7": "pour les hôtes autorisés)",1299 "char_import_7": "pour les hôtes autorisés)",
1303 "char_import_8": "Personnage de RisuRealm (lien direct)",1300 "char_import_8": "Personnage de RisuRealm (lien direct)",
1304 "char_import_9": "Personnage de Soulkyn (lien direct)",1301 "char_import_9": "Personnage de Soulkyn (lien direct)",
1302 "char_import_10": "Personnage de Perchance (lien direct ou UUID + .gz)",
1305 "Supports importing multiple characters.": "Prend en charge l'importation de plusieurs caractères.",1303 "Supports importing multiple characters.": "Prend en charge l'importation de plusieurs caractères.",
1306 "Write each URL or ID into a new line.": "Écrivez chaque URL ou identifiant dans une nouvelle ligne.",1304 "Write each URL or ID into a new line.": "Écrivez chaque URL ou identifiant dans une nouvelle ligne.",
1307 "Export for character": "Exportation pour le personnage",1305 "Export for character": "Exportation pour le personnage",
public/locales/is-is.json+1 -3
@@ -375,9 +375,6 @@
375 "Group by vendors Description": "Setjið OpenAI módel í einn hóp, Anthropic módel í annan hóp osfrv. Hægt að sameina við flokkun.",375 "Group by vendors Description": "Setjið OpenAI módel í einn hóp, Anthropic módel í annan hóp osfrv. Hægt að sameina við flokkun.",
376 "Allow fallback routes": "Leyfa bakfallssvæði",376 "Allow fallback routes": "Leyfa bakfallssvæði",
377 "Allow fallback routes Description": "Veldur hlutleysa vélbúnaðarinn við val þinn ef valið módel getur ekki uppfyllt beiðni þína.",377 "Allow fallback routes Description": "Veldur hlutleysa vélbúnaðarinn við val þinn ef valið módel getur ekki uppfyllt beiðni þína.",
378 "Scale API Key": "Lykill API fyrir Scale",
379 "Clear your cookie": "Hreinsaðu kökuna þína",
380 "Alt Method": "Aðferð Bakmenn",
381 "AI21 API Key": "Lykill API fyrir AI21",378 "AI21 API Key": "Lykill API fyrir AI21",
382 "AI21 Model": "AI21 Módel",379 "AI21 Model": "AI21 Módel",
383 "Google AI Studio API Key": "Google AI Studio API lykill",380 "Google AI Studio API Key": "Google AI Studio API lykill",
@@ -1380,6 +1377,7 @@
1380 "char_import_7": "fyrir leyfilega gestgjafa)",1377 "char_import_7": "fyrir leyfilega gestgjafa)",
1381 "char_import_8": "RisuRealm karakter (beinn hlekkur)",1378 "char_import_8": "RisuRealm karakter (beinn hlekkur)",
1382 "char_import_9": "Soulkyn karakter (beinn hlekkur)",1379 "char_import_9": "Soulkyn karakter (beinn hlekkur)",
1380 "char_import_10": "Perchance karakter (beinn hlekkur eða UUID + .gz)",
1383 "Supports importing multiple characters.": "Styður innflutning á mörgum stöfum.",1381 "Supports importing multiple characters.": "Styður innflutning á mörgum stöfum.",
1384 "Write each URL or ID into a new line.": "Skrifaðu hverja vefslóð eða auðkenni í nýja línu.",1382 "Write each URL or ID into a new line.": "Skrifaðu hverja vefslóð eða auðkenni í nýja línu.",
1385 "Export for character": "Flytja út fyrir persónu",1383 "Export for character": "Flytja út fyrir persónu",
public/locales/it-it.json+1 -3
@@ -375,9 +375,6 @@
375 "Group by vendors Description": "Metti i modelli OpenAI in un gruppo, i modelli antropici in un altro gruppo, ecc. Può essere combinato con l'ordinamento.",375 "Group by vendors Description": "Metti i modelli OpenAI in un gruppo, i modelli antropici in un altro gruppo, ecc. Può essere combinato con l'ordinamento.",
376 "Allow fallback routes": "Consenti percorsi alternativi",376 "Allow fallback routes": "Consenti percorsi alternativi",
377 "Allow fallback routes Description": "Il modello alternativo viene automaticamente scelto se il modello selezionato non può soddisfare la tua richiesta.",377 "Allow fallback routes Description": "Il modello alternativo viene automaticamente scelto se il modello selezionato non può soddisfare la tua richiesta.",
378 "Scale API Key": "Chiave API di Scale",
379 "Clear your cookie": "Cancella il tuo cookie",
380 "Alt Method": "Metodo alternativo",
381 "AI21 API Key": "Chiave API di AI21",378 "AI21 API Key": "Chiave API di AI21",
382 "AI21 Model": "Modello AI21",379 "AI21 Model": "Modello AI21",
383 "Google AI Studio API Key": "Chiave API Google AI Studio",380 "Google AI Studio API Key": "Chiave API Google AI Studio",
@@ -1380,6 +1377,7 @@
1380 "char_import_7": "per gli host consentiti)",1377 "char_import_7": "per gli host consentiti)",
1381 "char_import_8": "Personaggio RisuRealm (collegamento diretto)",1378 "char_import_8": "Personaggio RisuRealm (collegamento diretto)",
1382 "char_import_9": "Personaggio Soulkyn (collegamento diretto)",1379 "char_import_9": "Personaggio Soulkyn (collegamento diretto)",
1380 "char_import_10": "Perchance Personaggio (collegamento diretto o UUID + .gz)",
1383 "Supports importing multiple characters.": "Supporta l'importazione di più caratteri.",1381 "Supports importing multiple characters.": "Supporta l'importazione di più caratteri.",
1384 "Write each URL or ID into a new line.": "Scrivi ogni URL o ID in una nuova riga.",1382 "Write each URL or ID into a new line.": "Scrivi ogni URL o ID in una nuova riga.",
1385 "Export for character": "Esporta per carattere",1383 "Export for character": "Esporta per carattere",
public/locales/ja-jp.json+1 -3
@@ -375,9 +375,6 @@
375 "Group by vendors Description": "OpenAI モデルを 1 つのグループに、Anthropic モデルを別のグループに配置するなどします。ソートと組み合わせることができます。",375 "Group by vendors Description": "OpenAI モデルを 1 つのグループに、Anthropic モデルを別のグループに配置するなどします。ソートと組み合わせることができます。",
376 "Allow fallback routes": "フォールバックルートを許可",376 "Allow fallback routes": "フォールバックルートを許可",
377 "Allow fallback routes Description": "選択したモデルが要求を満たせない場合、代替モデルが自動的に選択されます。",377 "Allow fallback routes Description": "選択したモデルが要求を満たせない場合、代替モデルが自動的に選択されます。",
378 "Scale API Key": "ScaleのAPIキー",
379 "Clear your cookie": "クッキーを消去する",
380 "Alt Method": "代替手法",
381 "AI21 API Key": "AI21のAPIキー",378 "AI21 API Key": "AI21のAPIキー",
382 "AI21 Model": "AI21モデル",379 "AI21 Model": "AI21モデル",
383 "Google AI Studio API Key": "Google AI Studio APIキー",380 "Google AI Studio API Key": "Google AI Studio APIキー",
@@ -1382,6 +1379,7 @@
1382 "char_import_7": "許可されたホストの場合)",1379 "char_import_7": "許可されたホストの場合)",
1383 "char_import_8": "RisuRealm キャラクター (直接リンク)",1380 "char_import_8": "RisuRealm キャラクター (直接リンク)",
1384 "char_import_9": "Soulkyn キャラクター (直接リンク)",1381 "char_import_9": "Soulkyn キャラクター (直接リンク)",
1382 "char_import_10": "Perchance キャラクター (直接リンクまたは UUID + .gz)",
1385 "Supports importing multiple characters.": "複数のキャラクターのインポートをサポートします。",1383 "Supports importing multiple characters.": "複数のキャラクターのインポートをサポートします。",
1386 "Write each URL or ID into a new line.": "各 URL または ID を新しい行に入力します。",1384 "Write each URL or ID into a new line.": "各 URL または ID を新しい行に入力します。",
1387 "Export for character": "キャラクターのエクスポート",1385 "Export for character": "キャラクターのエクスポート",
public/locales/ko-kr.json+1 -3
@@ -384,9 +384,6 @@
384 "LEGACY": "유산",384 "LEGACY": "유산",
385 "Force Instruct Mode formatting": "강제 지시 모드 형식",385 "Force Instruct Mode formatting": "강제 지시 모드 형식",
386 "Force_Instruct_Mode_formatting_Description": "Instruct Mode와 이 모드가 모두 활성화된 경우 프롬프트는 SillyTavern에 의해 현재 형식을 사용하여 형식화됩니다.\n 고급 형식 설정(시스템 프롬프트 지시 제외) 비활성화된 경우 프롬프트는 OpenRouter에 의해 형식화됩니다.",386 "Force_Instruct_Mode_formatting_Description": "Instruct Mode와 이 모드가 모두 활성화된 경우 프롬프트는 SillyTavern에 의해 현재 형식을 사용하여 형식화됩니다.\n 고급 형식 설정(시스템 프롬프트 지시 제외) 비활성화된 경우 프롬프트는 OpenRouter에 의해 형식화됩니다.",
387 "Scale API Key": "Scale API 키",
388 "Clear your cookie": "쿠키 지우기",
389 "Alt Method": "대체 방법",
390 "AI21 API Key": "AI21 API 키",387 "AI21 API Key": "AI21 API 키",
391 "AI21 Model": "AI21 모델",388 "AI21 Model": "AI21 모델",
392 "Google AI Studio API Key": "Google AI Studio API 키",389 "Google AI Studio API Key": "Google AI Studio API 키",
@@ -1399,6 +1396,7 @@
1399 "char_import_7": "허용된 호스트의 경우)",1396 "char_import_7": "허용된 호스트의 경우)",
1400 "char_import_8": "RisuRealm 캐릭터 (직접링크)",1397 "char_import_8": "RisuRealm 캐릭터 (직접링크)",
1401 "char_import_9": "Soulkyn 캐릭터 (직접링크)",1398 "char_import_9": "Soulkyn 캐릭터 (직접링크)",
1399 "char_import_10": "Perchance 캐릭터 (직접 링크 또는 UUID + .gz)",
1402 "Supports importing multiple characters.": "여러 문자 가져오기를 지원합니다.",1400 "Supports importing multiple characters.": "여러 문자 가져오기를 지원합니다.",
1403 "Write each URL or ID into a new line.": "각 URL 또는 ID를 새 줄에 작성합니다.",1401 "Write each URL or ID into a new line.": "각 URL 또는 ID를 새 줄에 작성합니다.",
1404 "Export for character": "캐릭터 내보내기",1402 "Export for character": "캐릭터 내보내기",
public/locales/nl-nl.json+1 -3
@@ -375,9 +375,6 @@
375 "Group by vendors Description": "Plaats OpenAI-modellen in één groep, antropische modellen in een andere groep, enz. Kan worden gecombineerd met sorteren.",375 "Group by vendors Description": "Plaats OpenAI-modellen in één groep, antropische modellen in een andere groep, enz. Kan worden gecombineerd met sorteren.",
376 "Allow fallback routes": "Fallback-routes toestaan",376 "Allow fallback routes": "Fallback-routes toestaan",
377 "Allow fallback routes Description": "Het alternatieve model wordt automatisch gekozen als het geselecteerde model niet aan uw verzoek kan voldoen.",377 "Allow fallback routes Description": "Het alternatieve model wordt automatisch gekozen als het geselecteerde model niet aan uw verzoek kan voldoen.",
378 "Scale API Key": "Scale API-sleutel",
379 "Clear your cookie": "Wis uw cookie",
380 "Alt Method": "Alternatieve methode",
381 "AI21 API Key": "AI21 API-sleutel",378 "AI21 API Key": "AI21 API-sleutel",
382 "AI21 Model": "AI21-model",379 "AI21 Model": "AI21-model",
383 "MakerSuite API Key": "MakerSuite API-sleutel",380 "MakerSuite API Key": "MakerSuite API-sleutel",
@@ -1380,6 +1377,7 @@
1380 "char_import_7": "voor toegestane hosts)",1377 "char_import_7": "voor toegestane hosts)",
1381 "char_import_8": "RisuRealm-personage (directe link)",1378 "char_import_8": "RisuRealm-personage (directe link)",
1382 "char_import_9": "Soulkyn-personage (directe link)",1379 "char_import_9": "Soulkyn-personage (directe link)",
1380 "char_import_10": "Perchance-personage (directe link of UUID + .gz)",
1383 "Supports importing multiple characters.": "Ondersteunt het importeren van meerdere tekens.",1381 "Supports importing multiple characters.": "Ondersteunt het importeren van meerdere tekens.",
1384 "Write each URL or ID into a new line.": "Schrijf elke URL of ID op een nieuwe regel.",1382 "Write each URL or ID into a new line.": "Schrijf elke URL of ID op een nieuwe regel.",
1385 "Export for character": "Exporteren voor karakter",1383 "Export for character": "Exporteren voor karakter",
public/locales/pt-pt.json+1 -3
@@ -375,9 +375,6 @@
375 "Group by vendors Description": "Coloque os modelos OpenAI em um grupo, os modelos antrópicos em outro grupo, etc.",375 "Group by vendors Description": "Coloque os modelos OpenAI em um grupo, os modelos antrópicos em outro grupo, etc.",
376 "Allow fallback routes": "Permitir rotas de fallback",376 "Allow fallback routes": "Permitir rotas de fallback",
377 "Allow fallback routes Description": "O modelo alternativo será escolhido automaticamente se o modelo selecionado não puder atender à sua solicitação.",377 "Allow fallback routes Description": "O modelo alternativo será escolhido automaticamente se o modelo selecionado não puder atender à sua solicitação.",
378 "Scale API Key": "Chave da API Scale",
379 "Clear your cookie": "Limpe seu cookie",
380 "Alt Method": "Método Alternativo",
381 "AI21 API Key": "Chave da API AI21",378 "AI21 API Key": "Chave da API AI21",
382 "AI21 Model": "Modelo AI21",379 "AI21 Model": "Modelo AI21",
383 "Google AI Studio API Key": "Chave API Google AI Studio",380 "Google AI Studio API Key": "Chave API Google AI Studio",
@@ -1380,6 +1377,7 @@
1380 "char_import_7": "para hosts permitidos)",1377 "char_import_7": "para hosts permitidos)",
1381 "char_import_8": "Personagem RisuRealm (link direto)",1378 "char_import_8": "Personagem RisuRealm (link direto)",
1382 "char_import_9": "Personagem Soulkyn (link direto)",1379 "char_import_9": "Personagem Soulkyn (link direto)",
1380 "char_import_10": "Personagem Perchance (Link Direto ou UUID + .gz)",
1383 "Supports importing multiple characters.": "Suporta importação de vários caracteres.",1381 "Supports importing multiple characters.": "Suporta importação de vários caracteres.",
1384 "Write each URL or ID into a new line.": "Escreva cada URL ou ID em uma nova linha.",1382 "Write each URL or ID into a new line.": "Escreva cada URL ou ID em uma nova linha.",
1385 "Export for character": "Exportar para personagem",1383 "Export for character": "Exportar para personagem",
public/locales/ru-ru.json+1 -5
@@ -142,8 +142,6 @@
142 "Get your key from": "Получите ключ в",142 "Get your key from": "Получите ключ в",
143 "Anthropic's developer console": "консоли разработчика Anthropic",143 "Anthropic's developer console": "консоли разработчика Anthropic",
144 "Claude Model": "Модель Claude",144 "Claude Model": "Модель Claude",
145 "Scale API Key": "Ключ от Scale API",
146 "Alt Method": "Альтернативный метод",
147 "AI21 API Key": "Ключ от API AI21",145 "AI21 API Key": "Ключ от API AI21",
148 "AI21 Model": "Модель AI21",146 "AI21 Model": "Модель AI21",
149 "View API Usage Metrics": "Посмотреть статистику использования API",147 "View API Usage Metrics": "Посмотреть статистику использования API",
@@ -966,6 +964,7 @@
966 "char_import_6": "Прямая ссылка на PNG-файл (список разрешённых хостов находится в",964 "char_import_6": "Прямая ссылка на PNG-файл (список разрешённых хостов находится в",
967 "char_import_7": ")",965 "char_import_7": ")",
968 "char_import_9": "Персонаж с Soulkyn (прямая ссылка)",966 "char_import_9": "Персонаж с Soulkyn (прямая ссылка)",
967 "char_import_10": "Персонаж с Perchance (прямая ссылка или UUID + .gz)",
969 "Grammar String": "Грамматика",968 "Grammar String": "Грамматика",
970 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF или EBNF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.",969 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF или EBNF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.",
971 "Account": "Аккаунт",970 "Account": "Аккаунт",
@@ -1254,7 +1253,6 @@
1254 "vLLM Model": "Модель vLLM",1253 "vLLM Model": "Модель vLLM",
1255 "Aphrodite Model": "Модель Aphrodite",1254 "Aphrodite Model": "Модель Aphrodite",
1256 "Peek a password": "Посмотреть пароль",1255 "Peek a password": "Посмотреть пароль",
1257 "Clear your cookie": "Clear your cookie",
1258 "Add Chat Start and Example Separator to a list of stopping strings.": "Использовать Начало чата и Разделитель примеров сообщений в качестве стоп-строк.",1256 "Add Chat Start and Example Separator to a list of stopping strings.": "Использовать Начало чата и Разделитель примеров сообщений в качестве стоп-строк.",
1259 "Context Order": "Context Order",1257 "Context Order": "Context Order",
1260 "Summary": "Summary",1258 "Summary": "Summary",
@@ -1748,8 +1746,6 @@
1748 "dot quota_error": "имеется достаточно кредитов.",1746 "dot quota_error": "имеется достаточно кредитов.",
1749 "If you have sufficient credits, please try again later.": "Если кредитов достаточно, то повторите попытку позднее.",1747 "If you have sufficient credits, please try again later.": "Если кредитов достаточно, то повторите попытку позднее.",
1750 "Proxy preset '${0}' not found": "Пресет '${0}' не найден",1748 "Proxy preset '${0}' not found": "Пресет '${0}' не найден",
1751 "Window.ai returned an error": "Window.ai ответил ошибкой",
1752 "Extension is not installed": "Расширение не установлено",
1753 "Update or remove your reverse proxy settings.": "Измените или удалите ваши настройки прокси.",1749 "Update or remove your reverse proxy settings.": "Измените или удалите ваши настройки прокси.",
1754 "An error occurred while importing prompts. More info available in console.": "В процессе импорта произошла ошибка. Подробную информацию см. в консоли.",1750 "An error occurred while importing prompts. More info available in console.": "В процессе импорта произошла ошибка. Подробную информацию см. в консоли.",
1755 "Could not import prompts. Export failed validation.": "Не удалось импортировать промпты. Не пройдена валидация при экспорте.",1751 "Could not import prompts. Export failed validation.": "Не удалось импортировать промпты. Не пройдена валидация при экспорте.",
public/locales/uk-ua.json+1 -3
@@ -375,9 +375,6 @@
375 "Group by vendors Description": "Помістіть моделі OpenAI в одну групу, моделі Anthropic в іншу групу тощо. Можна поєднати з сортуванням.",375 "Group by vendors Description": "Помістіть моделі OpenAI в одну групу, моделі Anthropic в іншу групу тощо. Можна поєднати з сортуванням.",
376 "Allow fallback routes": "Дозволити резервні маршрути",376 "Allow fallback routes": "Дозволити резервні маршрути",
377 "Allow fallback routes Description": "Автоматично вибирає альтернативну модель, якщо вибрана модель не може задовольнити ваш запит.",377 "Allow fallback routes Description": "Автоматично вибирає альтернативну модель, якщо вибрана модель не може задовольнити ваш запит.",
378 "Scale API Key": "Ключ API для Scale",
379 "Clear your cookie": "Очистіть файл cookie",
380 "Alt Method": "Альтернативний метод",
381 "AI21 API Key": "Ключ API для AI21",378 "AI21 API Key": "Ключ API для AI21",
382 "AI21 Model": "Модель AI21",379 "AI21 Model": "Модель AI21",
383 "Google AI Studio API Key": "Ключ API Google AI Studio",380 "Google AI Studio API Key": "Ключ API Google AI Studio",
@@ -1380,6 +1377,7 @@
1380 "char_import_7": "для дозволених хостів)",1377 "char_import_7": "для дозволених хостів)",
1381 "char_import_8": "Персонаж RisuRealm (пряме посилання)",1378 "char_import_8": "Персонаж RisuRealm (пряме посилання)",
1382 "char_import_9": "Персонаж Soulkyn (пряме посилання)",1379 "char_import_9": "Персонаж Soulkyn (пряме посилання)",
1380 "char_import_10": "Персонаж Perchance (пряме посилання або UUID + .gz)",
1383 "Supports importing multiple characters.": "Підтримується імпорт кількох символів.",1381 "Supports importing multiple characters.": "Підтримується імпорт кількох символів.",
1384 "Write each URL or ID into a new line.": "Напишіть кожну URL-адресу або ідентифікатор у новому рядку.",1382 "Write each URL or ID into a new line.": "Напишіть кожну URL-адресу або ідентифікатор у новому рядку.",
1385 "Export for character": "Експорт для персонажа",1383 "Export for character": "Експорт для персонажа",
public/locales/vi-vn.json+1 -3
@@ -375,9 +375,6 @@
375 "Group by vendors Description": "Xếp các mô hình OpenAI vào một nhóm, các mô hình Anthropic vào một nhóm khác, v.v. Có thể kết hợp với việc sắp xếp.",375 "Group by vendors Description": "Xếp các mô hình OpenAI vào một nhóm, các mô hình Anthropic vào một nhóm khác, v.v. Có thể kết hợp với việc sắp xếp.",
376 "Allow fallback routes": "Cho phép các tuyến đường phụ",376 "Allow fallback routes": "Cho phép các tuyến đường phụ",
377 "Allow fallback routes Description": "Bot thay thế tự động nếu mô hình được chọn không thể đáp ứng yêu cầu của bạn.",377 "Allow fallback routes Description": "Bot thay thế tự động nếu mô hình được chọn không thể đáp ứng yêu cầu của bạn.",
378 "Scale API Key": "Scale API Key",
379 "Clear your cookie": "Xóa cookie",
380 "Alt Method": "Phương pháp thay thế",
381 "AI21 API Key": "AI21 API Key",378 "AI21 API Key": "AI21 API Key",
382 "AI21 Model": "Model của AI21",379 "AI21 Model": "Model của AI21",
383 "Google AI Studio API Key": "Google AI Studio API Key",380 "Google AI Studio API Key": "Google AI Studio API Key",
@@ -1380,6 +1377,7 @@
1380 "char_import_7": "đối với các máy chủ được phép)",1377 "char_import_7": "đối với các máy chủ được phép)",
1381 "char_import_8": "RisuRealm (URL trực tiếp)",1378 "char_import_8": "RisuRealm (URL trực tiếp)",
1382 "char_import_9": "Soulkyn (URL trực tiếp)",1379 "char_import_9": "Soulkyn (URL trực tiếp)",
1380 "char_import_10": "Perchance (Nhập URL trực tiếp hoặc UUID + .gz)",
1383 "Supports importing multiple characters.": "Hỗ trợ nhập nhiều ký tự.",1381 "Supports importing multiple characters.": "Hỗ trợ nhập nhiều ký tự.",
1384 "Write each URL or ID into a new line.": "Viết mỗi URL hoặc ID vào một dòng mới.",1382 "Write each URL or ID into a new line.": "Viết mỗi URL hoặc ID vào một dòng mới.",
1385 "Export for character": "Xuất cho nhân vật",1383 "Export for character": "Xuất cho nhân vật",
public/locales/zh-cn.json+1 -3
@@ -429,9 +429,6 @@
429 "Group by vendors": "按厂商分组",429 "Group by vendors": "按厂商分组",
430 "Group by vendors Description": "将 OpenAI 模型放在一组,将 Anthropic 模型放在另一组,等等。可以与排序结合。",430 "Group by vendors Description": "将 OpenAI 模型放在一组,将 Anthropic 模型放在另一组,等等。可以与排序结合。",
431 "To use instruct formatting, switch to OpenRouter under Text Completion API.": "To use instruct formatting, switch to OpenRouter under Text Completion API.",431 "To use instruct formatting, switch to OpenRouter under Text Completion API.": "To use instruct formatting, switch to OpenRouter under Text Completion API.",
432 "Scale API Key": "Scale API密钥",
433 "Clear your cookie": "清除你的 Cookie",
434 "Alt Method": "备用方法",
435 "AI21 API Key": "AI21 API 密钥",432 "AI21 API Key": "AI21 API 密钥",
436 "AI21 Model": "AI21 模型",433 "AI21 Model": "AI21 模型",
437 "Google AI Studio API Key": "Google AI Studio API 密钥",434 "Google AI Studio API Key": "Google AI Studio API 密钥",
@@ -1894,6 +1891,7 @@
1894 "char_import_7": ")",1891 "char_import_7": ")",
1895 "char_import_8": "RisuRealm 角色(直链)",1892 "char_import_8": "RisuRealm 角色(直链)",
1896 "char_import_9": "Soulkyn 角色(直链)",1893 "char_import_9": "Soulkyn 角色(直链)",
1894 "char_import_10": "Perchance 角色(直链或UUID + .gz)",
1897 "Supports importing multiple characters.": "支持导入多个角色。",1895 "Supports importing multiple characters.": "支持导入多个角色。",
1898 "Write each URL or ID into a new line.": "将每个 URL 或 ID 写入新行。",1896 "Write each URL or ID into a new line.": "将每个 URL 或 ID 写入新行。",
1899 "Enter the Git URL of the extension to install": "输入扩展程序的 Git URL 以安装",1897 "Enter the Git URL of the extension to install": "输入扩展程序的 Git URL 以安装",
public/locales/zh-tw.json+1 -103
@@ -376,9 +376,6 @@
376 "Group by vendors Description": "將 OpenAI、Anthropic 等模型放各自供應商的群組中。可以與排序功能結合使用。",376 "Group by vendors Description": "將 OpenAI、Anthropic 等模型放各自供應商的群組中。可以與排序功能結合使用。",
377 "Allow fallback routes": "允許備援路徑",377 "Allow fallback routes": "允許備援路徑",
378 "Allow fallback routes Description": "如果選擇的模型無法滿足要求,會自動選擇替代模型。",378 "Allow fallback routes Description": "如果選擇的模型無法滿足要求,會自動選擇替代模型。",
379 "Scale API Key": "Scale API 金鑰",
380 "Clear your cookie": "清除您的 Cookie",
381 "Alt Method": "替代方法",
382 "AI21 API Key": "AI21 API 金鑰",379 "AI21 API Key": "AI21 API 金鑰",
383 "AI21 Model": "AI21 模型",380 "AI21 Model": "AI21 模型",
384 "Google AI Studio API Key": "Google AI Studio API 金鑰",381 "Google AI Studio API Key": "Google AI Studio API 金鑰",
@@ -1385,6 +1382,7 @@
1385 "char_import_7": "對於允許的主機)",1382 "char_import_7": "對於允許的主機)",
1386 "char_import_8": "RisuRealm 角色(直接連結)",1383 "char_import_8": "RisuRealm 角色(直接連結)",
1387 "char_import_9": "Soulkyn 角色(直接連結)",1384 "char_import_9": "Soulkyn 角色(直接連結)",
1385 "char_import_10": "Perchance 角色(直接連結或 ID + .gz)",
1388 "Supports importing multiple characters.": "支援匯入多個字元。",1386 "Supports importing multiple characters.": "支援匯入多個字元。",
1389 "Write each URL or ID into a new line.": "將每個 URL 或 ID 寫入新行。",1387 "Write each URL or ID into a new line.": "將每個 URL 或 ID 寫入新行。",
1390 "Export for character": "匯出字元",1388 "Export for character": "匯出字元",
@@ -2515,106 +2513,6 @@
2515 "Note: Themes can be made/applied by going to the ": "注意:主題可通過前往以下位置進行創建/應用",2513 "Note: Themes can be made/applied by going to the ": "注意:主題可通過前往以下位置進行創建/應用",
2516 "Theme Name": "主題名稱",2514 "Theme Name": "主題名稱",
2517 "An unknown error occurred while counting tokens. Further information may be available in console.": "計算符元時發生未知錯誤。更多資訊可能可在主控台(console)中查看。",2515 "An unknown error occurred while counting tokens. Further information may be available in console.": "計算符元時發生未知錯誤。更多資訊可能可在主控台(console)中查看。",
2518 "Qvink Memory": "Qvink Memory(進階聊天記憶)",
2519 "Toggle whether memory is enabled for this chat specifically (overrides all settings).": "切換是否為此聊天啟用記憶功能(將覆蓋所有設定)。",
2520 "Toggle Chat Memory": "切換聊天記憶",
2521 "Preview current memory state (the exact text that will be injected into your context).": "預覽目前記憶狀態(包含將嵌入上下文的具體內容)。",
2522 "Copy ALL memories to clipboard (all memories in the entire chat, not just those injected).": "將所有記憶複製到剪貼簿(包含整個聊天的所有記憶,而非僅限於注入的部分)。",
2523 "Just refreshes which memories are included and re-renders the memories under each message, doesn't change summaries. This is done automatically all the time, the button is here just in case.": "不影響摘要,僅更新已包含的聊天記憶,並重新顯示在每則訊息下方。此過程通常會自動執行,按鈕只是備用選項。",
2524 "Active Settings Profile ": "目前設定檔",
2525 "Create, edit, and save configuration profiles for this extension.": "建立、編輯及儲存此擴充功能的設定檔。",
2526 "The currently selected profile": "目前選取的設定檔",
2527 "Save current profile": "儲存此設定檔",
2528 "Rename current profile": "重新命名此設定檔",
2529 "Create new profile": "建立新設定檔",
2530 "Restore current profile": "還原此設定檔",
2531 "Delete current profile": "刪除此設定檔",
2532 "Set as default profile for current character": "設為目前角色的預設設定檔",
2533 "Summarization": "摘要",
2534 "Customize the prompt used to summarize a given message": "自訂用於摘要指定訊息的提示詞",
2535 "Edit the summary prompt": "編輯摘要提示",
2536 "Preview the filled-in summary prompt, using the last message as an example.": "以最後一則訊息為例,預覽填充完成的摘要提示",
2537 "Mass re-summarization. Brings up dialog to choose subsets of messages to summarize or re-summarize.": "批量重新摘要:開啟對話框以選擇訊息子集進行摘要或重新摘要。",
2538 "Stop all summarization immediately.": "立即停止所有摘要。",
2539 "New messages will be automatically summarized if they will be included in short-term memory.": "如果新訊息將被納入短期記憶,將自動進行摘要。",
2540 "Auto Summarize": "自動摘要",
2541 "Auto-summarization will be triggered before a new message is sent instead of after.": "自動摘要將在發送新訊息之前觸發。",
2542 "Auto Summarize Before Generation": "在生成內容前自動摘要",
2543 "Show the progress bar when auto-summarizing more than 1 message.": "在自動摘要多於 1 則訊息時顯示進度條。",
2544 "Auto Summarize Progress Bar": "自動摘要進度條",
2545 "Number of messages to delay summarization (0 = summarize up to the most recent message, 1 = lag behind by one message, etc.)": "延遲摘要的訊息數量(0 = 摘要至最新訊息,1 = 延遲摘要 1 則訊息,以此類推)。",
2546 "Auto Summarize Message Lag": "自動摘要訊息延遲",
2547 "Wait until this many messages before auto-summarizing them all in sequence (1 = summarize every message immediately, 2 = summarize when you have two ready, etc). Still summarizes one at a time.": "在訊息數量達到此設定值後,依序自動摘要(1 = 即時摘要每則訊息,2 = 等待 2 則訊息後再摘要,以此類推)。摘要將逐條執行。",
2548 "Auto Summarize Batch Size": "自動摘要批次大小",
2549 "The maximum number of messages back that auto-summarization will apply (-1 to disable).": "自動摘要可回溯的訊息最大數量(-1 表示禁用此功能)。",
2550 "Auto Summarize Message Limit": "自動摘要訊息上限",
2551 "Time in seconds to wait between summarizations. May be needed if you are using a external API with a rate limit.": "每次摘要的間隔時間(秒)。此設定適用於使用具有請求速率限制的外部 API。",
2552 "Summarization Time Delay": "摘要時間延遲",
2553 "The maximum token length a summary is allowed to be before cutting it off. Use the {{words}} macro in the summarization prompt to get this value.": "摘要在被截斷前允許的最大符元(token)長度。可在摘要提示中使用 {{words}} 巨集以取得此數值。",
2554 "Summary Max Token Length": "摘要允許的最大符元長度",
2555 "Editing a message will automatically trigger a re-summarization if it has already been summarized.": "編輯訊息時,若該訊息已被摘要,將自動觸發重新摘要。",
2556 "Re-summarize on Edit": "編輯後重新摘要",
2557 "Swiping a message will automatically trigger a re-summarization if it has already been summarized.": "滑動訊息後若已進行摘要,將自動觸發重新摘要。",
2558 "Re-summarize on Swipe": "滑動後重新摘要",
2559 "Block chat input while summarizing.": "在摘要進行時暫時禁用聊天訊息輸入。",
2560 "Block Chat": "鎖定訊息輸入",
2561 "Whether to use messages and/or summaries as context for summarization. You must use {{history}} in the summary prompt.": "決定是否在摘要中使用訊息及/或過往摘要作為背景資訊。需於摘要提示詞中,使用 {{history}}。",
2562 "Message History": "訊息歷史",
2563 "Messages": "僅訊息",
2564 "Summaries": "僅摘要",
2565 "Both": "訊息與摘要",
2566 "Preview what the message history will look like": "預覽訊息歷史的顯示效果",
2567 "How many previous messages to include in the summarization prompt as context.": "摘要提示中要包含多少先前訊息作為上下文。",
2568 "Number of Previous Messages": "先前訊息數量",
2569 "When including previous messages, also include user messages.": "包含先前訊息時,也包含使用者訊息。",
2570 "Include Previous User Messages": "包含先前使用者訊息",
2571 "The message to summarize will be inside the system instruct template itself. In unchecked (default), the message will instead be added separately after the prompt. Some models benefit from this, but it is not recommended.": "系統指令模板內將直接包含需要摘要的訊息。若未啟用此選項(預設設定),訊息會在提示後分開添加。儘管某些模型可能更適合此設定,但一般不建議使用。",
2572 "Nest Message in Summary Prompt": "在摘要提示中內嵌訊息",
2573 "WARNING: doesn't work great. Attempts to preserve context-shifting by including all the content that is sent in regular prompts (world info, description, personas, example messages, message history, etc). If your regular prompts are static, this can allow Context Shifting to work between summarizations, but it decreases the accuracy of summarization due to all the extra stuff in the prompt. It also can't be previewed as this injection is handled by ST, not the extension.": "警告:效果不佳。此功能嘗試透過在摘要時包含所有常規提示內容(如世界資訊、描述、角色設定、示範訊息、聊天歷史等)來保留上下文轉換。若您的常規提示為靜態內容,則可在摘要之間維持上下文轉換,但由於提示中包含大量額外資訊,將降低摘要的準確性。此外,此內容注入由 ST 處理,而非此擴充功能,因此無法進行預覽。",
2574 "Include All Context Content": "包含所有上下文內容",
2575 "Short-term Memory Injection": "短期記憶注入",
2576 "Determines which messages are included in the short-term memory injection and where. If you change this and include messages that weren't summarized previously, you can either manually trigger a re-summarization or just wait until automatic summarization triggers.": "設定短期記憶注入中所包含的訊息及其插入位置。若更改此設定並包含先前未摘要的訊息,您可手動觸發重新摘要,或等待自動摘要啟動。",
2577 "Edit the short-term memory prompt": "編輯短期記憶提示",
2578 "Include User Messages": "包含使用者訊息",
2579 "Include System Messages": "包含系統訊息",
2580 "Include Thought Message": "包含思考訊息",
2581 "Message Length Threshold": "訊息長度閾值",
2582 "The minimum token length a message has to be in order to get summarized.": "可被摘要的訊息最小符元長度。",
2583 "The max percent of the context that short-term memory can take up.": "短期記憶可佔用上下文的最大百分比。",
2584 "Short-Term Context %": "短期記憶上下文%",
2585 "Include short-term memory in the World Info Scan": "在世界資訊掃描中包含短期記憶",
2586 "Do not inject": "不注入",
2587 "Before main prompt": "主提示之前",
2588 "After main prompt": "主提示之後",
2589 "In chat at depth": "在對話中位於深度",
2590 "Long-Term Memory Injection": "長期記憶注入",
2591 "Determines where long-term messages are injected.": "決定長期訊息注入的位置。",
2592 "Edit the long-term memory prompt": "編輯長期記憶提示",
2593 "The max percent of the context that long-term memory can take up.": "長期記憶可佔用上下文的最大百分比。",
2594 "Long-Term Context %": "長期記憶上下文%",
2595 "Include long-term memory in the World Info Scan": "在世界資訊掃描中包含長期記憶",
2596 "Misc.": "其他",
2597 "Fill your console with debug messages": "將偵錯訊息填入主控台",
2598 "Debug Mode": "偵錯模式",
2599 "Display summarizations below each message": "在每則訊息下顯示摘要",
2600 "Display Memories": "顯示記憶",
2601 "Enable Memory in New Chats": "在新對話中啟用記憶",
2602 "Limit Message History": "限制訊息歷史",
2603 "Revert Settings": "還原設定",
2604 "Auto-summarize user messages and include summaries in memory.": "自動摘要使用者訊息,並將該摘要納入記憶。",
2605 "Auto-summarize system messages and include summaries in memory.": "自動摘要系統訊息,並將該摘要納入記憶。",
2606 "Auto-summarize thought messages and include summaries in memory (from the Stepped Thinking extension).": "自動摘要思考訊息並將摘要納入記憶(來自 Stepped Thinking 擴充功能)。",
2607 "Revert all settings to default (not the default profile, just the default that comes with the extension). Your other profiles won't be affected.": "將所有設定恢復為預設值(並非恢復至「預設設定檔」,而是擴充功能隨附的原始預設值)。其他設定檔將不受影響。",
2608 "Limit the number of messages to send in regular prompts to this number (-1 for no limit). Message memories will still be sent.": "限制常規提示中傳送的訊息數量至此數值(-1 表示無限制)。訊息記憶仍將一併傳送。",
2609 "Whether memory is enabled by default for new chats.": "是否在新對話中預設啟用記憶。",
2610 "Summarize Chat": "聊天摘要",
2611 "Choose settings for the chat summarization. All message inclusion/exclusion settings from the main config profile are used, in addition to the following options.": "選擇聊天摘要的設定。摘要時將使用主要設定檔中的所有訊息包含/排除規則,並可額外設定以下選項。",
2612 "Currently preparing to summarize:": "目前正在準備摘要:",
2613 "Summarize messages with no existing summary": "摘要尚無摘要的訊息",
2614 "Re-summarize messages with existing short-term memories": "重新摘要具有現有短期記憶的訊息",
2615 "Re-summarize messages with existing long-term memories": "重新摘要具有現有長期記憶的訊息",
2616 "Re-summarize messages with existing memories, but which are currently excluded from short-term and long-term memory": "重新摘要具有現有記憶,但目前被排除在短期和長期記憶之外的訊息",
2617 "Re-summarize messages with existing memories that have been manually edited.": "重新摘要已手動編輯的訊息記憶",
2618 "Type the folder name of the theme you want to apply.": "輸入您想套用的主題資料夾名稱。",2516 "Type the folder name of the theme you want to apply.": "輸入您想套用的主題資料夾名稱。",
2619 "Place your theme data in a folder.": "請將主題資料存於該資料夾內。",2517 "Place your theme data in a folder.": "請將主題資料存於該資料夾內。",
2620 "Unsure where to start? Type ": "不確定如何開始?輸入:",2518 "Unsure where to start? Type ": "不確定如何開始?輸入:",
public/script.js+601 -2004
@@ -1,12 +1,9 @@
1import {1import {
2 showdown,2 showdown,
3 moment,3 moment,
4 Fuse,
5 DOMPurify,4 DOMPurify,
6 hljs,5 hljs,
7 localforage,
8 Handlebars,6 Handlebars,
9 DiffMatchPatch,
10 SVGInject,7 SVGInject,
11 Popper,8 Popper,
12 initLibraryShims,9 initLibraryShims,
@@ -19,10 +16,11 @@ import {
19 generateKoboldWithStreaming,16 generateKoboldWithStreaming,
20 kai_settings,17 kai_settings,
21 loadKoboldSettings,18 loadKoboldSettings,
22 formatKoboldUrl,
23 getKoboldGenerationData,19 getKoboldGenerationData,
24 kai_flags,20 kai_flags,
25 setKoboldFlags,21 koboldai_settings,
22 koboldai_setting_names,
23 initKoboldSettings,
26} from './scripts/kai-settings.js';24} from './scripts/kai-settings.js';
2725
28import {26import {
@@ -31,10 +29,9 @@ import {
31 generateTextGenWithStreaming,29 generateTextGenWithStreaming,
32 getTextGenGenerationData,30 getTextGenGenerationData,
33 textgen_types,31 textgen_types,
34 getTextGenServer,
35 validateTextGenUrl,
36 parseTextgenLogprobs,32 parseTextgenLogprobs,
37 parseTabbyLogprobs,33 parseTabbyLogprobs,
34 initTextGenSettings,
38} from './scripts/textgen-settings.js';35} from './scripts/textgen-settings.js';
3936
40import {37import {
@@ -91,7 +88,6 @@ import {
91 sortEntitiesList,88 sortEntitiesList,
92 registerDebugFunction,89 registerDebugFunction,
93 flushEphemeralStoppingStrings,90 flushEphemeralStoppingStrings,
94 context_presets,
95 resetMovableStyles,91 resetMovableStyles,
96 forceCharacterEditorTokenize,92 forceCharacterEditorTokenize,
97 applyPowerUserSettings,93 applyPowerUserSettings,
@@ -120,13 +116,13 @@ import {
120 generateNovelWithStreaming,116 generateNovelWithStreaming,
121 getNovelGenerationData,117 getNovelGenerationData,
122 getKayraMaxContextTokens,118 getKayraMaxContextTokens,
123 getNovelTier,
124 loadNovelPreset,
125 loadNovelSettings,119 loadNovelSettings,
126 nai_settings,120 nai_settings,
127 adjustNovelInstructionPrompt,121 adjustNovelInstructionPrompt,
128 loadNovelSubscriptionData,
129 parseNovelAILogprobs,122 parseNovelAILogprobs,
123 novelai_settings,
124 novelai_setting_names,
125 initNovelAISettings,
130} from './scripts/nai-settings.js';126} from './scripts/nai-settings.js';
131127
132import {128import {
@@ -139,9 +135,10 @@ import {
139 horde_settings,135 horde_settings,
140 loadHordeSettings,136 loadHordeSettings,
141 generateHorde,137 generateHorde,
142 checkHordeStatus,138 getStatusHorde,
143 getHordeModels,139 getHordeModels,
144 adjustHordeGenerationParams,140 adjustHordeGenerationParams,
141 isHordeGenerationNotAllowed,
145 MIN_LENGTH,142 MIN_LENGTH,
146 initHorde,143 initHorde,
147} from './scripts/horde.js';144} from './scripts/horde.js';
@@ -168,7 +165,6 @@ import {
168 isValidUrl,165 isValidUrl,
169 ensureImageFormatSupported,166 ensureImageFormatSupported,
170 flashHighlight,167 flashHighlight,
171 isTrueBoolean,
172 toggleDrawer,168 toggleDrawer,
173 isElementInViewport,169 isElementInViewport,
174 copyText,170 copyText,
@@ -180,10 +176,10 @@ import {
180 renderPaginationDropdown,176 renderPaginationDropdown,
181 paginationDropdownChangeHandler,177 paginationDropdownChangeHandler,
182} from './scripts/utils.js';178} from './scripts/utils.js';
183import { debounce_timeout, IGNORE_SYMBOL } from './scripts/constants.js';179import { debounce_timeout, GENERATION_TYPE_TRIGGERS, IGNORE_SYMBOL } from './scripts/constants.js';
184180
185import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors, saveMetadataDebounced } from './scripts/extensions.js';181import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors, saveMetadataDebounced } from './scripts/extensions.js';
186import { COMMENT_NAME_DEFAULT, executeSlashCommandsOnChatInput, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, stopScriptExecution } from './scripts/slash-commands.js';182import { COMMENT_NAME_DEFAULT, CONNECT_API_MAP, executeSlashCommandsOnChatInput, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, stopScriptExecution, UNIQUE_APIS } from './scripts/slash-commands.js';
187import {183import {
188 tag_map,184 tag_map,
189 tags,185 tags,
@@ -205,15 +201,9 @@ import {
205 applyTagsOnCharacterSelect,201 applyTagsOnCharacterSelect,
206 applyTagsOnGroupSelect,202 applyTagsOnGroupSelect,
207 tag_import_setting,203 tag_import_setting,
204 applyCharacterTagsToMessageDivs,
208} from './scripts/tags.js';205} from './scripts/tags.js';
209import {206import { initSecrets, readSecretState } from './scripts/secrets.js';
210 SECRET_KEYS,
211 initSecrets,
212 readSecretState,
213 secret_state,
214 writeSecret,
215} from './scripts/secrets.js';
216import { EventEmitter } from './lib/eventemitter.js';
217import { markdownExclusionExt } from './scripts/showdown-exclusion.js';207import { markdownExclusionExt } from './scripts/showdown-exclusion.js';
218import { markdownUnderscoreExt } from './scripts/showdown-underscore.js';208import { markdownUnderscoreExt } from './scripts/showdown-underscore.js';
219import { NOTE_MODULE_NAME, initAuthorsNote, metadata_keys, setFloatingPrompt, shouldWIAddPrompt } from './scripts/authors-note.js';209import { NOTE_MODULE_NAME, initAuthorsNote, metadata_keys, setFloatingPrompt, shouldWIAddPrompt } from './scripts/authors-note.js';
@@ -228,14 +218,10 @@ import {
228 formatInstructModePrompt,218 formatInstructModePrompt,
229 formatInstructModeExamples,219 formatInstructModeExamples,
230 getInstructStoppingSequences,220 getInstructStoppingSequences,
231 autoSelectInstructPreset,
232 formatInstructModeSystemPrompt,221 formatInstructModeSystemPrompt,
233 selectInstructPreset,
234 instruct_presets,
235 selectContextPreset,
236} from './scripts/instruct-mode.js';222} from './scripts/instruct-mode.js';
237import { initLocales, t } from './scripts/i18n.js';223import { initLocales, t } from './scripts/i18n.js';
238import { getFriendlyTokenizerName, getTokenCount, getTokenCountAsync, initTokenizers, saveTokenCache, TOKENIZER_SUPPORTED_KEY } from './scripts/tokenizers.js';224import { getFriendlyTokenizerName, getTokenCount, getTokenCountAsync, initTokenizers, saveTokenCache } from './scripts/tokenizers.js';
239import {225import {
240 user_avatar,226 user_avatar,
241 getUserAvatars,227 getUserAvatars,
@@ -249,38 +235,19 @@ import {
249} from './scripts/personas.js';235} from './scripts/personas.js';
250import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';236import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';
251import { hideLoader, showLoader } from './scripts/loader.js';237import { hideLoader, showLoader } from './scripts/loader.js';
252import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';238import { BulkEditOverlay } from './scripts/BulkEditOverlay.js';
253import {239import { initTextGenModels } from './scripts/textgen-models.js';
254 loadFeatherlessModels,240import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, preserveNeutralChat, restoreNeutralChat, formatCreatorNotes, initChatUtilities, addDOMPurifyHooks } from './scripts/chats.js';
255 loadMancerModels,
256 loadOllamaModels,
257 loadTogetherAIModels,
258 loadInfermaticAIModels,
259 loadOpenRouterModels,
260 loadVllmModels,
261 loadAphroditeModels,
262 loadDreamGenModels,
263 initTextGenModels,
264 loadTabbyModels,
265 loadGenericModels,
266} from './scripts/textgen-models.js';
267import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat, formatCreatorNotes, initChatUtilities } from './scripts/chats.js';
268import { getPresetManager, initPresetManager } from './scripts/preset-manager.js';241import { getPresetManager, initPresetManager } from './scripts/preset-manager.js';
269import { evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js';242import { evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js';
270import { currentUser, setUserControls } from './scripts/user.js';243import { currentUser, setUserControls } from './scripts/user.js';
271import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';244import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
272import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';245import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
273import { initScrapers } from './scripts/scrapers.js';246import { initScrapers } from './scripts/scrapers.js';
274import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';
275import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';
276import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './scripts/slash-commands/SlashCommandArgument.js';
277import { SlashCommandBrowser } from './scripts/slash-commands/SlashCommandBrowser.js';
278import { initCustomSelectedSamplers, validateDisabledSamplers } from './scripts/samplerSelect.js';247import { initCustomSelectedSamplers, validateDisabledSamplers } from './scripts/samplerSelect.js';
279import { DragAndDropHandler } from './scripts/dragdrop.js';248import { DragAndDropHandler } from './scripts/dragdrop.js';
280import { INTERACTABLE_CONTROL_CLASS, initKeyboard } from './scripts/keyboard.js';249import { INTERACTABLE_CONTROL_CLASS, initKeyboard } from './scripts/keyboard.js';
281import { initDynamicStyles } from './scripts/dynamic-styles.js';250import { initDynamicStyles } from './scripts/dynamic-styles.js';
282import { SlashCommandEnumValue, enumTypes } from './scripts/slash-commands/SlashCommandEnumValue.js';
283import { commonEnumProviders, enumIcons } from './scripts/slash-commands/SlashCommandCommonEnumsProvider.js';
284import { initInputMarkdown } from './scripts/input-md-formatting.js';251import { initInputMarkdown } from './scripts/input-md-formatting.js';
285import { AbortReason } from './scripts/util/AbortReason.js';252import { AbortReason } from './scripts/util/AbortReason.js';
286import { initSystemPrompts } from './scripts/sysprompt.js';253import { initSystemPrompts } from './scripts/sysprompt.js';
@@ -291,12 +258,14 @@ import { applyBrowserFixes } from './scripts/browser-fixes.js';
291import { initServerHistory } from './scripts/server-history.js';258import { initServerHistory } from './scripts/server-history.js';
292import { initSettingsSearch } from './scripts/setting-search.js';259import { initSettingsSearch } from './scripts/setting-search.js';
293import { initBulkEdit } from './scripts/bulk-edit.js';260import { initBulkEdit } from './scripts/bulk-edit.js';
294import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
295import { getContext } from './scripts/st-context.js';261import { getContext } from './scripts/st-context.js';
296import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';262import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';
297import { accountStorage } from './scripts/util/AccountStorage.js';263import { accountStorage } from './scripts/util/AccountStorage.js';
298import { initWelcomeScreen, openPermanentAssistantChat, openPermanentAssistantCard, getPermanentAssistantAvatar } from './scripts/welcome-screen.js';264import { initWelcomeScreen, openPermanentAssistantChat, openPermanentAssistantCard, getPermanentAssistantAvatar } from './scripts/welcome-screen.js';
299import { initDataMaid } from './scripts/data-maid.js';265import { initDataMaid } from './scripts/data-maid.js';
266import { clearItemizedPrompts, deleteItemizedPrompts, findItemizedPromptSet, initItemizedPrompts, itemizedParams, itemizedPrompts, loadItemizedPrompts, promptItemize, replaceItemizedPromptText, saveItemizedPrompts } from './scripts/itemized-prompts.js';
267import { getSystemMessageByType, initSystemMessages, SAFETY_CHAT, sendSystemMessage, system_message_types, system_messages } from './scripts/system-messages.js';
268import { event_types, eventSource } from './scripts/events.js';
300269
301// API OBJECT FOR EXTERNAL WIRING270// API OBJECT FOR EXTERNAL WIRING
302globalThis.SillyTavern = {271globalThis.SillyTavern = {
@@ -304,7 +273,6 @@ globalThis.SillyTavern = {
304 getContext,273 getContext,
305};274};
306275
307//exporting functions and vars for mods
308export {276export {
309 user_avatar,277 user_avatar,
310 setUserAvatar,278 setUserAvatar,
@@ -314,6 +282,27 @@ export {
314 isOdd,282 isOdd,
315 countOccurrences,283 countOccurrences,
316 renderTemplate,284 renderTemplate,
285 promptItemize,
286 itemizedPrompts,
287 saveItemizedPrompts,
288 loadItemizedPrompts,
289 itemizedParams,
290 clearItemizedPrompts,
291 replaceItemizedPromptText,
292 deleteItemizedPrompts,
293 findItemizedPromptSet,
294 koboldai_settings,
295 koboldai_setting_names,
296 novelai_settings,
297 novelai_setting_names,
298 UNIQUE_APIS,
299 CONNECT_API_MAP,
300 system_messages,
301 system_message_types,
302 sendSystemMessage,
303 getSystemMessageByType,
304 event_types,
305 eventSource,
317};306};
318307
319/**308/**
@@ -351,225 +340,7 @@ toastr.options = {
351 },340 },
352};341};
353342
354// Allow target="_blank" in links
355DOMPurify.addHook('afterSanitizeAttributes', function (node) {
356 if ('target' in node) {
357 node.setAttribute('target', '_blank');
358 node.setAttribute('rel', 'noopener');
359 }
360});
361
362DOMPurify.addHook('uponSanitizeAttribute', (node, data, config) => {
363 if (!config['MESSAGE_SANITIZE']) {
364 return;
365 }
366
367 /* Retain the classes on UI elements of messages that interact with the main UI */
368 const permittedNodeTypes = ['BUTTON', 'DIV'];
369 if (config['MESSAGE_ALLOW_SYSTEM_UI'] && node.classList.contains('menu_button') && permittedNodeTypes.includes(node.nodeName)) {
370 return;
371 }
372
373 switch (data.attrName) {
374 case 'class': {
375 if (data.attrValue) {
376 data.attrValue = data.attrValue.split(' ').map((v) => {
377 if (v.startsWith('fa-') || v.startsWith('note-') || v === 'monospace') {
378 return v;
379 }
380
381 return 'custom-' + v;
382 }).join(' ');
383 }
384 break;
385 }
386 }
387});
388
389DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
390 if (!config['MESSAGE_SANITIZE']) {
391 return;
392 }
393
394 // Replace line breaks with <br> in unknown elements
395 if (node instanceof HTMLUnknownElement) {
396 node.innerHTML = node.innerHTML.trim().replaceAll('\n', '<br>');
397 }
398
399 const isMediaAllowed = isExternalMediaAllowed();
400 if (isMediaAllowed) {
401 return;
402 }
403
404 if (!(node instanceof Element)) {
405 return;
406 }
407
408 let mediaBlocked = false;
409
410 switch (node.tagName) {
411 case 'AUDIO':
412 case 'VIDEO':
413 case 'SOURCE':
414 case 'TRACK':
415 case 'EMBED':
416 case 'OBJECT':
417 case 'IMG': {
418 const isExternalUrl = (url) => (url.indexOf('://') > 0 || url.indexOf('//') === 0) && !url.startsWith(window.location.origin);
419 const src = node.getAttribute('src');
420 const data = node.getAttribute('data');
421 const srcset = node.getAttribute('srcset');
422
423 if (srcset) {
424 const srcsetUrls = srcset.split(',');
425
426 for (const srcsetUrl of srcsetUrls) {
427 const [url] = srcsetUrl.trim().split(' ');
428
429 if (isExternalUrl(url)) {
430 console.warn('External media blocked', url);
431 node.remove();
432 mediaBlocked = true;
433 break;
434 }
435 }
436 }
437
438 if (src && isExternalUrl(src)) {
439 console.warn('External media blocked', src);
440 mediaBlocked = true;
441 node.remove();
442 }
443
444 if (data && isExternalUrl(data)) {
445 console.warn('External media blocked', data);
446 mediaBlocked = true;
447 node.remove();
448 }
449
450 if (mediaBlocked && (node instanceof HTMLMediaElement)) {
451 node.autoplay = false;
452 node.pause();
453 }
454 }
455 break;
456 }
457
458 if (mediaBlocked) {
459 const entityId = getCurrentEntityId();
460 const warningShownKey = `mediaWarningShown:${entityId}`;
461
462 if (accountStorage.getItem(warningShownKey) === null) {
463 const warningToast = toastr.warning(
464 t`Use the 'Ext. Media' button to allow it. Click on this message to dismiss.`,
465 t`External media has been blocked`,
466 {
467 timeOut: 0,
468 preventDuplicates: true,
469 onclick: () => toastr.clear(warningToast),
470 },
471 );
472
473 accountStorage.setItem(warningShownKey, 'true');
474 }
475 }
476});
477
478// Event source init
479//MARK: event_types
480export const event_types = {
481 APP_READY: 'app_ready',
482 EXTRAS_CONNECTED: 'extras_connected',
483 MESSAGE_SWIPED: 'message_swiped',
484 MESSAGE_SENT: 'message_sent',
485 MESSAGE_RECEIVED: 'message_received',
486 MESSAGE_EDITED: 'message_edited',
487 MESSAGE_DELETED: 'message_deleted',
488 MESSAGE_UPDATED: 'message_updated',
489 MESSAGE_FILE_EMBEDDED: 'message_file_embedded',
490 MESSAGE_REASONING_EDITED: 'message_reasoning_edited',
491 MESSAGE_REASONING_DELETED: 'message_reasoning_deleted',
492 MORE_MESSAGES_LOADED: 'more_messages_loaded',
493 IMPERSONATE_READY: 'impersonate_ready',
494 CHAT_CHANGED: 'chat_id_changed',
495 GENERATION_AFTER_COMMANDS: 'GENERATION_AFTER_COMMANDS',
496 GENERATION_STARTED: 'generation_started',
497 GENERATION_STOPPED: 'generation_stopped',
498 GENERATION_ENDED: 'generation_ended',
499 SD_PROMPT_PROCESSING: 'sd_prompt_processing',
500 EXTENSIONS_FIRST_LOAD: 'extensions_first_load',
501 EXTENSION_SETTINGS_LOADED: 'extension_settings_loaded',
502 SETTINGS_LOADED: 'settings_loaded',
503 SETTINGS_UPDATED: 'settings_updated',
504 GROUP_UPDATED: 'group_updated',
505 MOVABLE_PANELS_RESET: 'movable_panels_reset',
506 SETTINGS_LOADED_BEFORE: 'settings_loaded_before',
507 SETTINGS_LOADED_AFTER: 'settings_loaded_after',
508 CHATCOMPLETION_SOURCE_CHANGED: 'chatcompletion_source_changed',
509 CHATCOMPLETION_MODEL_CHANGED: 'chatcompletion_model_changed',
510 OAI_PRESET_CHANGED_BEFORE: 'oai_preset_changed_before',
511 OAI_PRESET_CHANGED_AFTER: 'oai_preset_changed_after',
512 OAI_PRESET_EXPORT_READY: 'oai_preset_export_ready',
513 OAI_PRESET_IMPORT_READY: 'oai_preset_import_ready',
514 WORLDINFO_SETTINGS_UPDATED: 'worldinfo_settings_updated',
515 WORLDINFO_UPDATED: 'worldinfo_updated',
516 CHARACTER_EDITED: 'character_edited',
517 CHARACTER_PAGE_LOADED: 'character_page_loaded',
518 CHARACTER_GROUP_OVERLAY_STATE_CHANGE_BEFORE: 'character_group_overlay_state_change_before',
519 CHARACTER_GROUP_OVERLAY_STATE_CHANGE_AFTER: 'character_group_overlay_state_change_after',
520 USER_MESSAGE_RENDERED: 'user_message_rendered',
521 CHARACTER_MESSAGE_RENDERED: 'character_message_rendered',
522 FORCE_SET_BACKGROUND: 'force_set_background',
523 CHAT_DELETED: 'chat_deleted',
524 CHAT_CREATED: 'chat_created',
525 GROUP_CHAT_DELETED: 'group_chat_deleted',
526 GROUP_CHAT_CREATED: 'group_chat_created',
527 GENERATE_BEFORE_COMBINE_PROMPTS: 'generate_before_combine_prompts',
528 GENERATE_AFTER_COMBINE_PROMPTS: 'generate_after_combine_prompts',
529 GENERATE_AFTER_DATA: 'generate_after_data',
530 GROUP_MEMBER_DRAFTED: 'group_member_drafted',
531 GROUP_WRAPPER_STARTED: 'group_wrapper_started',
532 GROUP_WRAPPER_FINISHED: 'group_wrapper_finished',
533 WORLD_INFO_ACTIVATED: 'world_info_activated',
534 TEXT_COMPLETION_SETTINGS_READY: 'text_completion_settings_ready',
535 CHAT_COMPLETION_SETTINGS_READY: 'chat_completion_settings_ready',
536 CHAT_COMPLETION_PROMPT_READY: 'chat_completion_prompt_ready',
537 CHARACTER_FIRST_MESSAGE_SELECTED: 'character_first_message_selected',
538 // TODO: Naming convention is inconsistent with other events
539 CHARACTER_DELETED: 'characterDeleted',
540 CHARACTER_DUPLICATED: 'character_duplicated',
541 CHARACTER_RENAMED: 'character_renamed',
542 CHARACTER_RENAMED_IN_PAST_CHAT: 'character_renamed_in_past_chat',
543 /** @deprecated The event is aliased to STREAM_TOKEN_RECEIVED. */
544 SMOOTH_STREAM_TOKEN_RECEIVED: 'stream_token_received',
545 STREAM_TOKEN_RECEIVED: 'stream_token_received',
546 STREAM_REASONING_DONE: 'stream_reasoning_done',
547 FILE_ATTACHMENT_DELETED: 'file_attachment_deleted',
548 WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate',
549 OPEN_CHARACTER_LIBRARY: 'open_character_library',
550 ONLINE_STATUS_CHANGED: 'online_status_changed',
551 IMAGE_SWIPED: 'image_swiped',
552 CONNECTION_PROFILE_LOADED: 'connection_profile_loaded',
553 CONNECTION_PROFILE_CREATED: 'connection_profile_created',
554 CONNECTION_PROFILE_DELETED: 'connection_profile_deleted',
555 CONNECTION_PROFILE_UPDATED: 'connection_profile_updated',
556 TOOL_CALLS_PERFORMED: 'tool_calls_performed',
557 TOOL_CALLS_RENDERED: 'tool_calls_rendered',
558 CHARACTER_MANAGEMENT_DROPDOWN: 'charManagementDropdown',
559 SECRET_WRITTEN: 'secret_written',
560 SECRET_DELETED: 'secret_deleted',
561 SECRET_ROTATED: 'secret_rotated',
562 SECRET_EDITED: 'secret_edited',
563};
564
565export const eventSource = new EventEmitter([event_types.APP_READY]);
566
567eventSource.on(event_types.CHAT_CHANGED, processChatSlashCommands);
568
569export const characterGroupOverlay = new BulkEditOverlay();343export const characterGroupOverlay = new BulkEditOverlay();
570const characterContextMenu = new CharacterContextMenu(characterGroupOverlay);
571eventSource.on(event_types.CHARACTER_PAGE_LOADED, characterGroupOverlay.onPageLoad);
572console.debug('Character context menu initialized', characterContextMenu);
573344
574// Markdown converter345// Markdown converter
575export let mesForShowdownParse; //intended to be used as a context to compare showdown strings against346export let mesForShowdownParse; //intended to be used as a context to compare showdown strings against
@@ -577,9 +348,6 @@ export let mesForShowdownParse; //intended to be used as a context to compare sh
577export let converter;348export let converter;
578349
579// array for prompt token calculations350// array for prompt token calculations
580console.debug('initializing Prompt Itemization Array on Startup');
581const promptStorage = localforage.createInstance({ name: 'SillyTavern_Prompts' });
582export let itemizedPrompts = [];
583351
584export const systemUserName = 'SillyTavern System';352export const systemUserName = 'SillyTavern System';
585export const neutralCharacterName = 'Assistant';353export const neutralCharacterName = 'Assistant';
@@ -652,25 +420,6 @@ export const saveCharacterDebounced = debounce(() => $('#create_button').trigger
652export const printCharactersDebounced = debounce(() => { printCharacters(false); }, DEFAULT_PRINT_TIMEOUT);420export const printCharactersDebounced = debounce(() => { printCharacters(false); }, DEFAULT_PRINT_TIMEOUT);
653421
654/**422/**
655 * @enum {string} System message types
656 */
657export const system_message_types = {
658 HELP: 'help',
659 WELCOME: 'welcome',
660 EMPTY: 'empty',
661 GENERIC: 'generic',
662 NARRATOR: 'narrator',
663 COMMENT: 'comment',
664 SLASH_COMMANDS: 'slash_commands',
665 FORMATTING: 'formatting',
666 HOTKEYS: 'hotkeys',
667 MACROS: 'macros',
668 WELCOME_PROMPT: 'welcome_prompt',
669 ASSISTANT_NOTE: 'assistant_note',
670 ASSISTANT_MESSAGE: 'assistant_message',
671};
672
673/**
674 * @enum {number} Extension prompt types423 * @enum {number} Extension prompt types
675 */424 */
676export const extension_prompt_types = {425export const extension_prompt_types = {
@@ -691,106 +440,6 @@ export const extension_prompt_roles = {
691440
692export const MAX_INJECTION_DEPTH = 10000;441export const MAX_INJECTION_DEPTH = 10000;
693442
694// Initialized in getSystemMessages()
695const SAFETY_CHAT = [];
696
697export let system_messages = {};
698
699async function getSystemMessages() {
700 system_messages = {
701 help: {
702 name: systemUserName,
703 force_avatar: system_avatar,
704 is_user: false,
705 is_system: true,
706 mes: await renderTemplateAsync('help'),
707 },
708 slash_commands: {
709 name: systemUserName,
710 force_avatar: system_avatar,
711 is_user: false,
712 is_system: true,
713 mes: '',
714 },
715 hotkeys: {
716 name: systemUserName,
717 force_avatar: system_avatar,
718 is_user: false,
719 is_system: true,
720 mes: await renderTemplateAsync('hotkeys'),
721 },
722 formatting: {
723 name: systemUserName,
724 force_avatar: system_avatar,
725 is_user: false,
726 is_system: true,
727 mes: await renderTemplateAsync('formatting'),
728 },
729 macros: {
730 name: systemUserName,
731 force_avatar: system_avatar,
732 is_user: false,
733 is_system: true,
734 mes: await renderTemplateAsync('macros'),
735 },
736 welcome:
737 {
738 name: systemUserName,
739 force_avatar: system_avatar,
740 is_user: false,
741 is_system: true,
742 uses_system_ui: true,
743 mes: await renderTemplateAsync('welcome', { displayVersion }),
744 },
745 empty: {
746 name: systemUserName,
747 force_avatar: system_avatar,
748 is_user: false,
749 is_system: true,
750 mes: 'No one hears you. <b>Hint&#58;</b> add more members to the group!',
751 },
752 generic: {
753 name: systemUserName,
754 force_avatar: system_avatar,
755 is_user: false,
756 is_system: true,
757 mes: 'Generic system message. User `text` parameter to override the contents',
758 },
759 welcome_prompt: {
760 name: systemUserName,
761 force_avatar: system_avatar,
762 is_user: false,
763 is_system: true,
764 uses_system_ui: true,
765 mes: await renderTemplateAsync('welcomePrompt'),
766 extra: {
767 isSmallSys: true,
768 },
769 },
770 assistant_note: {
771 name: systemUserName,
772 force_avatar: system_avatar,
773 is_user: false,
774 is_system: true,
775 mes: await renderTemplateAsync('assistantNote'),
776 uses_system_ui: true,
777 extra: {
778 isSmallSys: true,
779 },
780 },
781 };
782
783 const safetyMessage = {
784 name: systemUserName,
785 force_avatar: system_avatar,
786 is_system: true,
787 is_user: false,
788 create_date: 0,
789 mes: t`You deleted a character/chat and arrived back here for safety reasons! Pick another character!`,
790 };
791 SAFETY_CHAT.splice(0, SAFETY_CHAT.length, safetyMessage);
792}
793
794async function getClientVersion() {443async function getClientVersion() {
795 try {444 try {
796 const response = await fetch('/version');445 const response = await fetch('/version');
@@ -882,6 +531,7 @@ export let create_save = {
882 depth_prompt_depth: depth_prompt_depth_default,531 depth_prompt_depth: depth_prompt_depth_default,
883 depth_prompt_role: depth_prompt_role_default,532 depth_prompt_role: depth_prompt_role_default,
884 extensions: {},533 extensions: {},
534 extra_books: [],
885};535};
886536
887//animation right menu537//animation right menu
@@ -892,8 +542,6 @@ let popup_type = '';
892let chat_file_for_del = '';542let chat_file_for_del = '';
893export let online_status = 'no_connection';543export let online_status = 'no_connection';
894544
895export let api_server = '';
896
897export let is_send_press = false; //Send generation545export let is_send_press = false; //Send generation
898546
899let this_del_mes = -1;547let this_del_mes = -1;
@@ -904,9 +552,6 @@ var this_edit_mes_id;
904552
905//settings553//settings
906export let settings;554export let settings;
907export let koboldai_settings;
908export let koboldai_setting_names;
909var preset_settings = 'gui';
910export let amount_gen = 80; //default max length of AI generated responses555export let amount_gen = 80; //default max length of AI generated responses
911export let max_context = 2048;556export let max_context = 2048;
912557
@@ -914,9 +559,6 @@ var swipes = true;
914export let extension_prompts = {};559export let extension_prompts = {};
915560
916export let main_api;// = "kobold";561export let main_api;// = "kobold";
917//novel settings
918export let novelai_settings;
919export let novelai_setting_names;
920/** @type {AbortController} */562/** @type {AbortController} */
921let abortController;563let abortController;
922564
@@ -927,8 +569,6 @@ var kobold_horde_model = '';
927569
928export let token;570export let token;
929571
930var PromptArrayItemForRawPromptDisplay;
931var priorPromptArrayItemForRawPromptDisplay;
932572
933/** The tag of the active character. (NOT the id) */573/** The tag of the active character. (NOT the id) */
934export let active_character = '';574export let active_character = '';
@@ -937,11 +577,17 @@ export let active_group = '';
937577
938export const entitiesFilter = new FilterHelper(printCharactersDebounced);578export const entitiesFilter = new FilterHelper(printCharactersDebounced);
939579
940export function getRequestHeaders() {580export function getRequestHeaders({ omitContentType = false } = {}) {
941 return {581 const headers = {
942 'Content-Type': 'application/json',582 'Content-Type': 'application/json',
943 'X-CSRF-Token': token,583 'X-CSRF-Token': token,
944 };584 };
585
586 if (omitContentType) {
587 delete headers['Content-Type'];
588 }
589
590 return headers;
945}591}
946592
947export function getSlideToggleOptions() {593export function getSlideToggleOptions() {
@@ -993,6 +639,7 @@ async function firstLoadInit() {
993 initStandaloneMode();639 initStandaloneMode();
994 initLibraryShims();640 initLibraryShims();
995 addShowdownPatch(showdown);641 addShowdownPatch(showdown);
642 addDOMPurifyHooks();
996 reloadMarkdownProcessor();643 reloadMarkdownProcessor();
997 applyBrowserFixes();644 applyBrowserFixes();
998 await getClientVersion();645 await getClientVersion();
@@ -1003,12 +650,15 @@ async function firstLoadInit() {
1003 initDefaultSlashCommands();650 initDefaultSlashCommands();
1004 initTextGenModels();651 initTextGenModels();
1005 initOpenAI();652 initOpenAI();
653 initTextGenSettings();
654 initKoboldSettings();
655 initNovelAISettings();
1006 initSystemPrompts();656 initSystemPrompts();
1007 initExtensions();657 initExtensions();
1008 initExtensionSlashCommands();658 initExtensionSlashCommands();
1009 ToolManager.initToolSlashCommands();659 ToolManager.initToolSlashCommands();
1010 await initPresetManager();660 await initPresetManager();
1011 await getSystemMessages();661 await initSystemMessages();
1012 await getSettings();662 await getSettings();
1013 initKeyboard();663 initKeyboard();
1014 initDynamicStyles();664 initDynamicStyles();
@@ -1037,6 +687,7 @@ async function firstLoadInit() {
1037 await initScrapers();687 await initScrapers();
1038 initCustomSelectedSamplers();688 initCustomSelectedSamplers();
1039 initDataMaid();689 initDataMaid();
690 initItemizedPrompts();
1040 addDebugFunctions();691 addDebugFunctions();
1041 doDailyExtensionUpdatesCheck();692 doDailyExtensionUpdatesCheck();
1042 await hideLoader();693 await hideLoader();
@@ -1101,303 +752,6 @@ export function setActiveGroup(entityOrKey) {
1101 if (active_group) active_character = null;752 if (active_group) active_character = null;
1102}753}
1103754
1104/**
1105 * Gets the itemized prompts for a chat.
1106 * @param {string} chatId Chat ID to load
1107 */
1108export async function loadItemizedPrompts(chatId) {
1109 try {
1110 if (!chatId) {
1111 itemizedPrompts = [];
1112 return;
1113 }
1114
1115 itemizedPrompts = await promptStorage.getItem(chatId);
1116
1117 if (!itemizedPrompts) {
1118 itemizedPrompts = [];
1119 }
1120 } catch {
1121 console.log('Error loading itemized prompts for chat', chatId);
1122 itemizedPrompts = [];
1123 }
1124}
1125
1126/**
1127 * Saves the itemized prompts for a chat.
1128 * @param {string} chatId Chat ID to save itemized prompts for
1129 */
1130export async function saveItemizedPrompts(chatId) {
1131 try {
1132 if (!chatId) {
1133 return;
1134 }
1135
1136 await promptStorage.setItem(chatId, itemizedPrompts);
1137 } catch {
1138 console.log('Error saving itemized prompts for chat', chatId);
1139 }
1140}
1141
1142/**
1143 * Replaces the itemized prompt text for a message.
1144 * @param {number} mesId Message ID to get itemized prompt for
1145 * @param {string} promptText New raw prompt text
1146 * @returns
1147 */
1148export async function replaceItemizedPromptText(mesId, promptText) {
1149 if (!Array.isArray(itemizedPrompts)) {
1150 itemizedPrompts = [];
1151 }
1152
1153 const itemizedPrompt = itemizedPrompts.find(x => x.mesId === mesId);
1154
1155 if (!itemizedPrompt) {
1156 return;
1157 }
1158
1159 itemizedPrompt.rawPrompt = promptText;
1160}
1161
1162/**
1163 * Deletes the itemized prompts for a chat.
1164 * @param {string} chatId Chat ID to delete itemized prompts for
1165 */
1166export async function deleteItemizedPrompts(chatId) {
1167 try {
1168 if (!chatId) {
1169 return;
1170 }
1171
1172 await promptStorage.removeItem(chatId);
1173 } catch {
1174 console.log('Error deleting itemized prompts for chat', chatId);
1175 }
1176}
1177
1178/**
1179 * Empties the itemized prompts array and caches.
1180 */
1181export async function clearItemizedPrompts() {
1182 try {
1183 await promptStorage.clear();
1184 itemizedPrompts = [];
1185 } catch {
1186 console.log('Error clearing itemized prompts');
1187 }
1188}
1189
1190async function getStatusHorde() {
1191 try {
1192 const hordeStatus = await checkHordeStatus();
1193 setOnlineStatus(hordeStatus ? t`Connected` : 'no_connection');
1194 }
1195 catch {
1196 setOnlineStatus('no_connection');
1197 }
1198
1199 return resultCheckStatus();
1200}
1201
1202async function getStatusKobold() {
1203 let endpoint = api_server;
1204
1205 if (!endpoint) {
1206 console.warn('No endpoint for status check');
1207 setOnlineStatus('no_connection');
1208 return resultCheckStatus();
1209 }
1210
1211 try {
1212 const response = await fetch('/api/backends/kobold/status', {
1213 method: 'POST',
1214 headers: getRequestHeaders(),
1215 body: JSON.stringify({
1216 main_api,
1217 api_server: endpoint,
1218 }),
1219 signal: abortStatusCheck.signal,
1220 });
1221
1222 const data = await response.json();
1223
1224 setOnlineStatus(data?.model ?? 'no_connection');
1225
1226 if (!data.koboldUnitedVersion) {
1227 throw new Error('Missing mandatory Kobold version in data:', data);
1228 }
1229
1230 // Determine instruct mode preset
1231 autoSelectInstructPreset(online_status);
1232
1233 // determine if we can use stop sequence and streaming
1234 setKoboldFlags(data.koboldUnitedVersion, data.koboldCppVersion);
1235
1236 // We didn't get a 200 status code, but the endpoint has an explanation. Which means it DID connect, but I digress.
1237 if (online_status === 'no_connection' && data.response) {
1238 toastr.error(data.response, t`API Error`, { timeOut: 5000, preventDuplicates: true });
1239 }
1240 } catch (err) {
1241 console.error('Error getting status', err);
1242 setOnlineStatus('no_connection');
1243 }
1244
1245 return resultCheckStatus();
1246}
1247
1248async function getStatusTextgen() {
1249 const url = '/api/backends/text-completions/status';
1250
1251 const endpoint = getTextGenServer();
1252
1253 if (!endpoint) {
1254 console.warn('No endpoint for status check');
1255 setOnlineStatus('no_connection');
1256 return resultCheckStatus();
1257 }
1258
1259 if ([textgen_types.GENERIC, textgen_types.OOBA].includes(textgen_settings.type) && textgen_settings.bypass_status_check) {
1260 setOnlineStatus(t`Status check bypassed`);
1261 return resultCheckStatus();
1262 }
1263
1264 try {
1265 const response = await fetch(url, {
1266 method: 'POST',
1267 headers: getRequestHeaders(),
1268 body: JSON.stringify({
1269 api_server: endpoint,
1270 api_type: textgen_settings.type,
1271 }),
1272 signal: abortStatusCheck.signal,
1273 });
1274
1275 const data = await response.json();
1276
1277 if (textgen_settings.type === textgen_types.MANCER) {
1278 loadMancerModels(data?.data);
1279 setOnlineStatus(textgen_settings.mancer_model);
1280 } else if (textgen_settings.type === textgen_types.TOGETHERAI) {
1281 loadTogetherAIModels(data?.data);
1282 setOnlineStatus(textgen_settings.togetherai_model);
1283 } else if (textgen_settings.type === textgen_types.OLLAMA) {
1284 loadOllamaModels(data?.data);
1285 setOnlineStatus(textgen_settings.ollama_model || t`Connected`);
1286 } else if (textgen_settings.type === textgen_types.INFERMATICAI) {
1287 loadInfermaticAIModels(data?.data);
1288 setOnlineStatus(textgen_settings.infermaticai_model);
1289 } else if (textgen_settings.type === textgen_types.DREAMGEN) {
1290 loadDreamGenModels(data?.data);
1291 setOnlineStatus(textgen_settings.dreamgen_model);
1292 } else if (textgen_settings.type === textgen_types.OPENROUTER) {
1293 loadOpenRouterModels(data?.data);
1294 setOnlineStatus(textgen_settings.openrouter_model);
1295 } else if (textgen_settings.type === textgen_types.VLLM) {
1296 loadVllmModels(data?.data);
1297 setOnlineStatus(textgen_settings.vllm_model);
1298 } else if (textgen_settings.type === textgen_types.APHRODITE) {
1299 loadAphroditeModels(data?.data);
1300 setOnlineStatus(textgen_settings.aphrodite_model);
1301 } else if (textgen_settings.type === textgen_types.FEATHERLESS) {
1302 loadFeatherlessModels(data?.data);
1303 setOnlineStatus(textgen_settings.featherless_model);
1304 } else if (textgen_settings.type === textgen_types.TABBY) {
1305 loadTabbyModels(data?.data);
1306 setOnlineStatus(textgen_settings.tabby_model || data?.result);
1307 } else if (textgen_settings.type === textgen_types.GENERIC) {
1308 loadGenericModels(data?.data);
1309 setOnlineStatus(textgen_settings.generic_model || data?.result || t`Connected`);
1310 } else {
1311 setOnlineStatus(data?.result);
1312 }
1313
1314 if (!online_status) {
1315 setOnlineStatus('no_connection');
1316 }
1317
1318 // Determine instruct mode preset
1319 autoSelectInstructPreset(online_status);
1320
1321 const supportsTokenization = response.headers.get('x-supports-tokenization') === 'true';
1322 supportsTokenization ? sessionStorage.setItem(TOKENIZER_SUPPORTED_KEY, 'true') : sessionStorage.removeItem(TOKENIZER_SUPPORTED_KEY);
1323
1324 const wantsInstructDerivation = (power_user.instruct.enabled && power_user.instruct.derived);
1325 const wantsContextDerivation = power_user.context_derived;
1326 const wantsContextSize = power_user.context_size_derived;
1327 const supportsChatTemplate = [textgen_types.KOBOLDCPP, textgen_types.LLAMACPP].includes(textgen_settings.type);
1328 if (supportsChatTemplate && (wantsInstructDerivation || wantsContextDerivation || wantsContextSize)) {
1329 const response = await fetch('/api/backends/text-completions/props', {
1330 method: 'POST',
1331 headers: getRequestHeaders(),
1332 body: JSON.stringify({
1333 api_server: endpoint,
1334 api_type: textgen_settings.type,
1335 }),
1336 });
1337
1338 if (response.ok) {
1339 const data = await response.json();
1340 if (data) {
1341 const { chat_template, chat_template_hash } = data;
1342 if (wantsContextSize && 'default_generation_settings' in data) {
1343 const backend_max_context = data['default_generation_settings']['n_ctx'];
1344 const old_value = max_context;
1345 if (max_context !== backend_max_context) {
1346 setGenerationParamsFromPreset({ max_length: backend_max_context });
1347 }
1348 if (old_value !== max_context) {
1349 console.log(`Auto-switched max context from ${old_value} to ${max_context}`);
1350 toastr.info(`${old_value} ⇒ ${max_context}`, 'Context Size Changed');
1351 }
1352 }
1353 console.log(`We have chat template ${chat_template.split('\n')[0]}...`);
1354 const templates = await deriveTemplatesFromChatTemplate(chat_template, chat_template_hash);
1355 if (templates) {
1356 const { context, instruct } = templates;
1357 if (wantsContextDerivation) {
1358 selectContextPreset(context, { isAuto: true });
1359 }
1360 if (wantsInstructDerivation) {
1361 selectInstructPreset(instruct, { isAuto: true });
1362 }
1363 }
1364 }
1365 }
1366 }
1367
1368 // We didn't get a 200 status code, but the endpoint has an explanation. Which means it DID connect, but I digress.
1369 if (online_status === 'no_connection' && data.response) {
1370 toastr.error(data.response, t`API Error`, { timeOut: 5000, preventDuplicates: true });
1371 }
1372 } catch (err) {
1373 if (err instanceof AbortReason) {
1374 console.info('Status check aborted.', err.reason);
1375 } else {
1376 console.error('Error getting status', err);
1377
1378 }
1379 setOnlineStatus('no_connection');
1380 }
1381
1382 return resultCheckStatus();
1383}
1384
1385async function getStatusNovel() {
1386 try {
1387 const result = await loadNovelSubscriptionData();
1388
1389 if (!result) {
1390 throw new Error('Could not load subscription data');
1391 }
1392
1393 setOnlineStatus(getNovelTier());
1394 } catch {
1395 setOnlineStatus('no_connection');
1396 }
1397
1398 resultCheckStatus();
1399}
1400
1401export function startStatusLoading() {755export function startStatusLoading() {
1402 $('.api_loading').show();756 $('.api_loading').show();
1403 $('.api_button').addClass('disabled');757 $('.api_button').addClass('disabled');
@@ -1836,6 +1190,12 @@ function getCharacterSource(chId = this_chid) {
1836 return `https://realm.risuai.net/character/${realmId}`;1190 return `https://realm.risuai.net/character/${realmId}`;
1837 }1191 }
18381192
1193 const perchanceSlug = characters[chId]?.data?.extensions?.perchance_data?.slug;
1194
1195 if (perchanceSlug) {
1196 return `https://perchance.org/ai-character-chat?data=${perchanceSlug}`;
1197 }
1198
1839 return '';1199 return '';
1840}1200}
18411201
@@ -1845,7 +1205,7 @@ export async function getCharacters() {
1845 headers: getRequestHeaders(),1205 headers: getRequestHeaders(),
1846 body: JSON.stringify({}),1206 body: JSON.stringify({}),
1847 });1207 });
1848 if (response.ok === true) {1208 if (response.ok) {
1849 const previousAvatar = this_chid !== undefined ? characters[this_chid]?.avatar : null;1209 const previousAvatar = this_chid !== undefined ? characters[this_chid]?.avatar : null;
1850 characters.splice(0, characters.length);1210 characters.splice(0, characters.length);
1851 const getData = await response.json();1211 const getData = await response.json();
@@ -1874,6 +1234,12 @@ export async function getCharacters() {
18741234
1875 await getGroups();1235 await getGroups();
1876 await printCharacters(true);1236 await printCharacters(true);
1237 } else {
1238 console.error('Failed to fetch characters:', response.statusText);
1239 const errorData = await response.json();
1240 if (errorData?.overflow) {
1241 await Popup.show.text(t`Character data length limit reached`, t`To resolve this, set "performance.lazyLoadCharacters" to "true" in config.yaml and restart the server.`);
1242 }
1877 }1243 }
1878}1244}
18791245
@@ -2080,7 +1446,7 @@ export async function clearChat() {
2080 } else { console.debug('saw no avatars'); }1446 } else { console.debug('saw no avatars'); }
20811447
2082 await saveItemizedPrompts(getCurrentChatId());1448 await saveItemizedPrompts(getCurrentChatId());
2083 itemizedPrompts = [];1449 itemizedPrompts.length = 0;
2084}1450}
20851451
2086export async function deleteLastMessage() {1452export async function deleteLastMessage() {
@@ -2496,20 +1862,20 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) {
2496 eventSource.emit(event_types.IMAGE_SWIPED, { message: mes, element: messageElement, direction: 'right' });1862 eventSource.emit(event_types.IMAGE_SWIPED, { message: mes, element: messageElement, direction: 'right' });
2497 });1863 });
2498 }1864 }
1865 } else {
1866 const container = messageElement.find('.mes_img_container');
1867 container.removeClass('img_extra img_swipes');
1868 const text = messageElement.find('.mes_text');
1869 text.removeClass('displayNone');
2499 }1870 }
25001871
2501 // Add video to message1872 // Add video to message
2502 if (mes.extra?.video) {1873 if (mes.extra?.video) {
2503 const container = messageElement.find('.mes_block');1874 const container = $('#message_video_template .mes_video_container').clone();
1875 messageElement.find('.mes_video_container').remove();
1876 messageElement.find('.mes_block').append(container);
2504 const chatHeight = $('#chat').prop('scrollHeight');1877 const chatHeight = $('#chat').prop('scrollHeight');
25051878 const video = container.find('.mes_video');
2506 // Create video element if it doesn't exist
2507 let video = messageElement.find('.mes_video');
2508 if (video.length === 0) {
2509 video = $('<video class="mes_video" controls preload="metadata"></video>');
2510 container.append(video);
2511 }
2512
2513 video.off('loadedmetadata').on('loadedmetadata', function () {1879 video.off('loadedmetadata').on('loadedmetadata', function () {
2514 if (!adjustScroll) {1880 if (!adjustScroll) {
2515 return;1881 return;
@@ -2521,6 +1887,8 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) {
2521 });1887 });
25221888
2523 video.attr('src', mes.extra?.video);1889 video.attr('src', mes.extra?.video);
1890 } else {
1891 messageElement.find('.mes_video_container').remove();
2524 }1892 }
25251893
2526 // Add file to message1894 // Add file to message
@@ -2553,6 +1921,9 @@ export function addCopyToCodeBlocks(messageElement) {
2553 copyButton.classList.add('fa-solid', 'fa-copy', 'code-copy', 'interactable');1921 copyButton.classList.add('fa-solid', 'fa-copy', 'code-copy', 'interactable');
2554 copyButton.title = 'Copy code';1922 copyButton.title = 'Copy code';
2555 codeBlocks.get(i).appendChild(copyButton);1923 codeBlocks.get(i).appendChild(copyButton);
1924 copyButton.addEventListener('click', function (e) {
1925 e.stopPropagation();
1926 });
2556 copyButton.addEventListener('pointerup', async function () {1927 copyButton.addEventListener('pointerup', async function () {
2557 const text = codeBlocks.get(i).innerText;1928 const text = codeBlocks.get(i).innerText;
2558 await copyText(text);1929 await copyText(text);
@@ -2590,7 +1961,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2590 mes.swipes = [mes.mes];1961 mes.swipes = [mes.mes];
2591 }1962 }
25921963
2593 let avatarImg = getUserAvatar(user_avatar);1964 let avatarImg = getThumbnailUrl('persona', user_avatar);
2594 const isSystem = mes.is_system;1965 const isSystem = mes.is_system;
2595 const title = mes.title;1966 const title = mes.title;
2596 generatedPromptCache = '';1967 generatedPromptCache = '';
@@ -2740,6 +2111,8 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2740 if (!insertAfter && !insertBefore && scroll) {2111 if (!insertAfter && !insertBefore && scroll) {
2741 scrollChatToBottom();2112 scrollChatToBottom();
2742 }2113 }
2114
2115 applyCharacterTagsToMessageDivs({ mesIds: newMessageId });
2743}2116}
27442117
2745/**2118/**
@@ -2963,35 +2336,43 @@ export function getStoppingStrings(isImpersonate, isContinue) {
29632336
2964/**2337/**
2965 * Background generation based on the provided prompt.2338 * Background generation based on the provided prompt.
2966 * @param {string} quiet_prompt Instruction prompt for the AI2339 * @typedef {object} GenerateQuietPromptParams
2967 * @param {boolean} quietToLoud Whether the message should be sent in a foreground (loud) or background (quiet) mode2340 * @prop {string} [quietPrompt] Instruction prompt for the AI
2968 * @param {boolean} skipWIAN whether to skip addition of World Info and Author's Note into the prompt2341 * @prop {boolean} [quietToLoud] Whether the message should be sent in a foreground (loud) or background (quiet) mode
2969 * @param {string} quietImage Image to use for the quiet prompt2342 * @prop {boolean} [skipWIAN] Whether to skip addition of World Info and Author's Note into the prompt
2970 * @param {string} quietName Name to use for the quiet prompt (defaults to "System:")2343 * @prop {string} [quietImage] Image to use for the quiet prompt
2971 * @param {number} [responseLength] Maximum response length. If unset, the global default value is used.2344 * @prop {string} [quietName] Name to use for the quiet prompt (defaults to "System:")
2972 * @param {number} force_chid Character ID to use for this generation run. Works in groups only.2345 * @prop {number} [responseLength] Maximum response length. If unset, the global default value is used.
2973 * @returns2346 * @prop {number} [forceChId] Character ID to use for this generation run. Works in groups only.
2347 * @prop {object} [jsonSchema] JSON schema to use for the structured generation. Usually requires a special instruction.
2348 * @param {GenerateQuietPromptParams} params Parameters for the quiet prompt generation
2349 * @returns {Promise<string>} Generated text. If using structured output, will contain a serialized JSON object.
2974 */2350 */
2975export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, quietImage = null, quietName = null, responseLength = null, force_chid = null) {2351export async function generateQuietPrompt({ quietPrompt = '', quietToLoud = false, skipWIAN = false, quietImage = null, quietName = null, responseLength = null, forceChId = null, jsonSchema = null } = {}) {
2976 console.log('got into genQuietPrompt');2352 if (arguments.length > 0 && typeof arguments[0] !== 'object') {
2353 console.trace('generateQuietPrompt called with positional arguments. Please use an object instead.');
2354 [quietPrompt, quietToLoud, skipWIAN, quietImage, quietName, responseLength, forceChId, jsonSchema] = arguments;
2355 }
2356
2977 const responseLengthCustomized = typeof responseLength === 'number' && responseLength > 0;2357 const responseLengthCustomized = typeof responseLength === 'number' && responseLength > 0;
2978 let eventHook = () => { };2358 let eventHook = () => { };
2979 try {2359 try {
2980 /** @type {GenerateOptions} */2360 /** @type {GenerateOptions} */
2981 const options = {2361 const generateOptions = {
2982 quiet_prompt,2362 quiet_prompt: quietPrompt ?? '',
2983 quietToLoud,2363 quietToLoud: quietToLoud ?? false,
2984 skipWIAN: skipWIAN,2364 skipWIAN: skipWIAN ?? false,
2985 force_name2: true,2365 force_name2: true,
2986 quietImage: quietImage,2366 quietImage: quietImage ?? null,
2987 quietName: quietName,2367 quietName: quietName ?? null,
2988 force_chid: force_chid,2368 force_chid: forceChId ?? null,
2369 jsonSchema: jsonSchema ?? null,
2989 };2370 };
2990 if (responseLengthCustomized) {2371 if (responseLengthCustomized) {
2991 TempResponseLength.save(main_api, responseLength);2372 TempResponseLength.save(main_api, responseLength);
2992 eventHook = TempResponseLength.setupEventHook(main_api);2373 eventHook = TempResponseLength.setupEventHook(main_api);
2993 }2374 }
2994 const result = await Generate('quiet', options);2375 const result = await Generate('quiet', generateOptions);
2995 return removeReasoningFromString(result);2376 return removeReasoningFromString(result);
2996 } finally {2377 } finally {
2997 if (responseLengthCustomized && TempResponseLength.isCustomized()) {2378 if (responseLengthCustomized && TempResponseLength.isCustomized()) {
@@ -3017,67 +2398,13 @@ export async function processCommands(message) {
3017}2398}
30182399
3019/**2400/**
3020 * Gets a system message by type.2401 * Extracts the contents of bias macros from a message.
3021 * @param {string} type Type of system message2402 * @param {string} message Message text
3022 * @param {string} [text] Text to be sent2403 * @returns {string} Message bias extracted from the message (or an empty string if not found)
3023 * @param {object} [extra] Additional data to be added to the message
3024 * @returns {object} System message object
3025 */2404 */
3026export function getSystemMessageByType(type, text, extra = {}) {2405export function extractMessageBias(message) {
3027 const systemMessage = system_messages[type];2406 if (!message) {
30282407 return '';
3029 if (!systemMessage) {
3030 return;
3031 }
3032
3033 const newMessage = { ...systemMessage, send_date: getMessageTimeStamp() };
3034
3035 if (text) {
3036 newMessage.mes = text;
3037 }
3038
3039 if (type === system_message_types.SLASH_COMMANDS) {
3040 newMessage.mes = getSlashCommandsHelp();
3041 }
3042
3043 if (!newMessage.extra) {
3044 newMessage.extra = {};
3045 }
3046
3047 newMessage.extra = Object.assign(newMessage.extra, extra);
3048 newMessage.extra.type = type;
3049 return newMessage;
3050}
3051
3052/**
3053 * Sends a system message to the chat.
3054 * @param {string} type Type of system message
3055 * @param {string} [text] Text to be sent
3056 * @param {object} [extra] Additional data to be added to the message
3057 */
3058export function sendSystemMessage(type, text, extra = {}) {
3059 const newMessage = getSystemMessageByType(type, text, extra);
3060 chat.push(newMessage);
3061 addOneMessage(newMessage);
3062 is_send_press = false;
3063 if (type === system_message_types.SLASH_COMMANDS) {
3064 const browser = new SlashCommandBrowser();
3065 const spinner = document.querySelector('#chat .last_mes .custom-slashHelp');
3066 const parent = spinner.parentElement;
3067 spinner.remove();
3068 browser.renderInto(parent);
3069 browser.search.focus();
3070 }
3071}
3072
3073/**
3074 * Extracts the contents of bias macros from a message.
3075 * @param {string} message Message text
3076 * @returns {string} Message bias extracted from the message (or an empty string if not found)
3077 */
3078export function extractMessageBias(message) {
3079 if (!message) {
3080 return '';
3081 }2408 }
30822409
3083 try {2410 try {
@@ -3368,11 +2695,9 @@ export function parseMesExamples(examplesStr, isInstruct) {
3368}2695}
33692696
3370export function isStreamingEnabled() {2697export function isStreamingEnabled() {
3371 const noStreamSources = [chat_completion_sources.SCALE];
3372 return (2698 return (
3373 (main_api == 'openai' &&2699 (main_api == 'openai' &&
3374 oai_settings.stream_openai &&2700 oai_settings.stream_openai &&
3375 !noStreamSources.includes(oai_settings.chat_completion_source) &&
3376 !(oai_settings.chat_completion_source == chat_completion_sources.OPENAI && ['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model))2701 !(oai_settings.chat_completion_source == chat_completion_sources.OPENAI && ['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model))
3377 )2702 )
3378 || (main_api == 'kobold' && kai_settings.streaming_kobold && kai_flags.can_use_streaming)2703 || (main_api == 'kobold' && kai_settings.streaming_kobold && kai_flags.can_use_streaming)
@@ -3547,7 +2872,7 @@ class StreamingProcessor {
35472872
3548 // Token count update.2873 // Token count update.
3549 const tokenCountText = this.reasoningHandler.reasoning + processedText;2874 const tokenCountText = this.reasoningHandler.reasoning + processedText;
3550 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(tokenCountText, 0) : 0;2875 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? await getTokenCountAsync(tokenCountText, 0) : 0;
3551 if (currentTokenCount) {2876 if (currentTokenCount) {
3552 chat[messageId]['extra']['token_count'] = currentTokenCount;2877 chat[messageId]['extra']['token_count'] = currentTokenCount;
3553 if (this.messageTokenCounterDom instanceof HTMLElement) {2878 if (this.messageTokenCounterDom instanceof HTMLElement) {
@@ -3561,7 +2886,7 @@ class StreamingProcessor {
3561 'send_date': chat[messageId]['send_date'],2886 'send_date': chat[messageId]['send_date'],
3562 'gen_started': chat[messageId]['gen_started'],2887 'gen_started': chat[messageId]['gen_started'],
3563 'gen_finished': chat[messageId]['gen_finished'],2888 'gen_finished': chat[messageId]['gen_finished'],
3564 'extra': JSON.parse(JSON.stringify(chat[messageId]['extra'])),2889 'extra': structuredClone(chat[messageId]['extra']),
3565 };2890 };
3566 }2891 }
35672892
@@ -3666,7 +2991,7 @@ class StreamingProcessor {
3666 'send_date': chat[messageId]['send_date'],2991 'send_date': chat[messageId]['send_date'],
3667 'gen_started': chat[messageId]['gen_started'],2992 'gen_started': chat[messageId]['gen_started'],
3668 'gen_finished': chat[messageId]['gen_finished'],2993 'gen_finished': chat[messageId]['gen_finished'],
3669 'extra': JSON.parse(JSON.stringify(chat[messageId]['extra'])),2994 'extra': structuredClone(chat[messageId]['extra']),
3670 };2995 };
3671 }2996 }
3672 }2997 }
@@ -3740,37 +3065,101 @@ class StreamingProcessor {
3740}3065}
37413066
3742/**3067/**
3743 * Generates a message using the provided prompt.3068 * Constructs a prompt to be used for either Text Completion or Chat Completion. Input is format-agnostic.
3744 * @param {string} prompt Prompt to generate a message from3069 * @param {string | object[]} prompt Input prompt. Can be a string or an array of chat-style messages, i.e. [{role: '', content: ''}, ...]
3745 * @param {string} api API to use. Main API is used if not specified.3070 * @param {string} api API to use.
3746 * @param {boolean} instructOverride true to override instruct mode, false to use the default value3071 * @param {boolean} instructOverride true to override instruct mode, false to use the default value
3747 * @param {boolean} quietToLoud true to generate a message in system mode, false to generate a message in character mode3072 * @param {boolean} quietToLoud true to generate a message in system mode, false to generate a message in character mode
3748 * @param {string} [systemPrompt] System prompt to use. Only Instruct mode or OpenAI.3073 * @param {string} [systemPrompt] System prompt to use.
3749 * @param {number} [responseLength] Maximum response length. If unset, the global default value is used.3074 * @param {string} [prefill] Prefill for the prompt.
3750 * @param {boolean} [trimNames] Whether to allow trimming "{{user}}:" and "{{char}}:" from the response.3075 * @returns {string | object[]} Prompt ready for use in generation. If using TC, this will be a string. If using CC, this will be an array of chat-style messages.
3076 */
3077export function createRawPrompt(prompt, api, instructOverride, quietToLoud, systemPrompt, prefill) {
3078 const isInstruct = power_user.instruct.enabled && api !== 'openai' && api !== 'novel' && !instructOverride;
3079
3080 // If the prompt was given as a string, convert to a message-style object assuming user role
3081 if (typeof prompt === 'string') {
3082 const message = api === 'openai'
3083 ? { role: 'user', content: prompt.trim() }
3084 : { role: 'system', content: prompt };
3085 prompt = [message];
3086 } else { // checks for message-style object
3087 if (prompt.length === 0 && !systemPrompt) throw Error('No messages provided');
3088 }
3089
3090 // Substitute the prefill if provided
3091 prefill = substituteParams(prefill ?? '');
3092
3093 // Format each message in the prompt, accounting for the provided roles
3094 for (const message of prompt) {
3095 let name = '';
3096 if (message.role === 'user') name = message.name ?? name1;
3097 if (message.role === 'assistant') name = message.name ?? name2;
3098 if (message.role === 'system') name = message.name ?? '';
3099 const prefix = isInstruct || api === 'openai' ? '' : (name ? `${name}: ` : '');
3100 message.content = prefix + substituteParams(message.content ?? '');
3101 if (isInstruct) { // instruct formatting for text completion
3102 const isUser = message.role === 'user';
3103 const isNarrator = message.role === 'system';
3104 message.content = formatInstructModeChat(name, message.content, isUser, isNarrator, '', name1, name2, false);
3105 }
3106 }
3107
3108 // prepend system prompt, if provided
3109 if (systemPrompt) {
3110 systemPrompt = substituteParams(systemPrompt);
3111 systemPrompt = isInstruct ? (formatInstructModeSystemPrompt(systemPrompt) + '\n') : systemPrompt.trim();
3112 prompt.unshift({ role: 'system', content: systemPrompt });
3113 }
3114
3115 // with Chat Completion, the prefill is an additional assistant message at the end.
3116 if (api === 'openai' && prefill) {
3117 prompt.push({ role: 'assistant', content: prefill });
3118 }
3119
3120 // if text completion, convert to text prompt by concatenating all message contents and adding the prefill as a promptBias.
3121 if (api !== 'openai') {
3122 const joiner = isInstruct ? '' : '\n';
3123 prompt = prompt.map(message => message.content).join(joiner);
3124 prompt = api === 'novel' ? adjustNovelInstructionPrompt(prompt) : prompt;
3125 prompt = prompt + (isInstruct ? formatInstructModePrompt(name2, false, prefill, name1, name2, true, quietToLoud) : `\n${prefill}`); // add last line
3126 }
3127
3128 return prompt;
3129}
3130
3131/**
3132 * Generates a message using the provided prompt.
3133 * If the prompt is an array of chat-style messages and not using chat completion, it will be converted to a text prompt.
3134 * @typedef {object} GenerateRawParams
3135 * @prop {string | object[]} [prompt] Prompt to generate a message from. Can be a string or an array of chat-style messages, i.e. [{role: '', content: ''}, ...]
3136 * @prop {string} [api] API to use. Main API is used if not specified.
3137 * @prop {boolean} [instructOverride] true to override instruct mode, false to use the default value
3138 * @prop {boolean} [quietToLoud] true to generate a message in system mode, false to generate a message in character mode
3139 * @prop {string} [systemPrompt] System prompt to use.
3140 * @prop {number} [responseLength] Maximum response length. If unset, the global default value is used.
3141 * @prop {boolean} [trimNames] Whether to allow trimming "{{user}}:" and "{{char}}:" from the response.
3142 * @prop {string} [prefill] An optional prefill for the prompt.
3143 * @prop {object} [jsonSchema] JSON schema to use for the structured generation. Usually requires a special instruction.
3144 * @param {GenerateRawParams} params Parameters for generating a message
3751 * @returns {Promise<string>} Generated message3145 * @returns {Promise<string>} Generated message
3752 */3146 */
3753export async function generateRaw(prompt, api, instructOverride, quietToLoud, systemPrompt, responseLength, trimNames = true) {3147export async function generateRaw({ prompt = '', api = null, instructOverride = false, quietToLoud = false, systemPrompt = '', responseLength = null, trimNames = true, prefill = '', jsonSchema = null } = {}) {
3148 if (arguments.length > 0 && typeof arguments[0] !== 'object') {
3149 console.trace('generateRaw called with positional arguments. Please use an object instead.');
3150 [prompt, api, instructOverride, quietToLoud, systemPrompt, responseLength, trimNames, prefill, jsonSchema] = arguments;
3151 }
3152
3754 if (!api) {3153 if (!api) {
3755 api = main_api;3154 api = main_api;
3756 }3155 }
37573156
3758 const abortController = new AbortController();3157 const abortController = new AbortController();
3759 const responseLengthCustomized = typeof responseLength === 'number' && responseLength > 0;3158 const responseLengthCustomized = typeof responseLength === 'number' && responseLength > 0;
3760 const isInstruct = power_user.instruct.enabled && api !== 'openai' && api !== 'novel' && !instructOverride;
3761 const isQuiet = true;
3762 let eventHook = () => { };3159 let eventHook = () => { };
37633160
3764 if (systemPrompt) {3161 // construct final prompt from the input. Can either be a string or an array of chat-style messages.
3765 systemPrompt = substituteParams(systemPrompt);3162 prompt = createRawPrompt(prompt, api, instructOverride, quietToLoud, systemPrompt, prefill);
3766 systemPrompt = isInstruct ? formatInstructModeSystemPrompt(systemPrompt) : systemPrompt;
3767 prompt = api === 'openai' ? prompt : `${systemPrompt}\n${prompt}`;
3768 }
3769
3770 prompt = substituteParams(prompt);
3771 prompt = api == 'novel' ? adjustNovelInstructionPrompt(prompt) : prompt;
3772 prompt = isInstruct ? formatInstructModeChat(name1, prompt, false, true, '', name1, name2, false) : prompt;
3773 prompt = isInstruct ? (prompt + formatInstructModePrompt(name2, false, '', name1, name2, isQuiet, quietToLoud)) : (prompt + '\n');
37743163
3775 try {3164 try {
3776 if (responseLengthCustomized) {3165 if (responseLengthCustomized) {
@@ -3782,12 +3171,12 @@ export async function generateRaw(prompt, api, instructOverride, quietToLoud, sy
3782 switch (api) {3171 switch (api) {
3783 case 'kobold':3172 case 'kobold':
3784 case 'koboldhorde':3173 case 'koboldhorde':
3785 if (preset_settings === 'gui') {3174 if (kai_settings.preset_settings === 'gui') {
3786 generateData = { prompt: prompt, gui_settings: true, max_length: amount_gen, max_context_length: max_context, api_server };3175 generateData = { prompt: prompt, gui_settings: true, max_length: amount_gen, max_context_length: max_context, api_server: kai_settings.api_server };
3787 } else {3176 } else {
3788 const isHorde = api === 'koboldhorde';3177 const isHorde = api === 'koboldhorde';
3789 const koboldSettings = koboldai_settings[koboldai_setting_names[preset_settings]];3178 const koboldSettings = koboldai_settings[koboldai_setting_names[kai_settings.preset_settings]];
3790 generateData = getKoboldGenerationData(prompt, koboldSettings, amount_gen, max_context, isHorde, 'quiet');3179 generateData = getKoboldGenerationData(prompt.toString(), koboldSettings, amount_gen, max_context, isHorde, 'quiet');
3791 }3180 }
3792 TempResponseLength.restore(api);3181 TempResponseLength.restore(api);
3793 break;3182 break;
@@ -3802,10 +3191,7 @@ export async function generateRaw(prompt, api, instructOverride, quietToLoud, sy
3802 TempResponseLength.restore(api);3191 TempResponseLength.restore(api);
3803 break;3192 break;
3804 case 'openai': {3193 case 'openai': {
3805 generateData = [{ role: 'user', content: prompt.trim() }];3194 generateData = prompt; // generateData is just the chat message object
3806 if (systemPrompt) {
3807 generateData.unshift({ role: 'system', content: systemPrompt.trim() });
3808 }
3809 eventHook = TempResponseLength.setupEventHook(api);3195 eventHook = TempResponseLength.setupEventHook(api);
3810 } break;3196 } break;
3811 }3197 }
@@ -3813,9 +3199,9 @@ export async function generateRaw(prompt, api, instructOverride, quietToLoud, sy
3813 let data = {};3199 let data = {};
38143200
3815 if (api === 'koboldhorde') {3201 if (api === 'koboldhorde') {
3816 data = await generateHorde(prompt, generateData, abortController.signal, false);3202 data = await generateHorde(prompt.toString(), generateData, abortController.signal, false);
3817 } else if (api === 'openai') {3203 } else if (api === 'openai') {
3818 data = await sendOpenAIRequest('quiet', generateData, abortController.signal);3204 data = await sendOpenAIRequest('quiet', generateData, abortController.signal, { jsonSchema });
3819 } else {3205 } else {
3820 const generateUrl = getGenerateUrl(api);3206 const generateUrl = getGenerateUrl(api);
3821 const response = await fetch(generateUrl, {3207 const response = await fetch(generateUrl, {
@@ -3840,6 +3226,10 @@ export async function generateRaw(prompt, api, instructOverride, quietToLoud, sy
3840 throw new Error(data.response);3226 throw new Error(data.response);
3841 }3227 }
38423228
3229 if (jsonSchema) {
3230 return extractJsonFromData(data, { mainApi: api });
3231 }
3232
3843 // format result, exclude user prompt bias3233 // format result, exclude user prompt bias
3844 const message = cleanUpMessage({3234 const message = cleanUpMessage({
3845 getMessage: extractMessageFromData(data),3235 getMessage: extractMessageFromData(data),
@@ -3972,15 +3362,35 @@ function removeLastMessage() {
3972}3362}
39733363
3974/**3364/**
3365 * @typedef {object} JsonSchema
3366 * @property {string} name Name of the schema.
3367 * @property {object} value JSON schema value.
3368 * @property {string} [description] Description of the schema.
3369 * @property {boolean} [strict] If true, the schema will be used in strict mode, meaning that only the fields defined in the schema will be allowed.
3370 *
3371 * @typedef {object} GenerateOptions
3372 * @property {boolean} [automatic_trigger] If the generation was triggered automatically (e.g. group auto mode).
3373 * @property {boolean} [force_name2] If a char name should be forced to add to the prompt's last line (Text Completion, non-Instruct only).
3374 * @property {string} [quiet_prompt] A system instruction to use for the quiet prompt.
3375 * @property {boolean} [quietToLoud] Whether the system instruction should be sent in background (quiet) or a foreground (loud) mode.
3376 * @property {boolean} [skipWIAN] Skip adding World Info and Author's Note to the prompt.
3377 * @property {number} [force_chid] Force character ID to use for the generation. Only works in groups.
3378 * @property {AbortSignal} [signal] Abort signal to cancel the generation. If not provided, will create a new AbortController.
3379 * @property {string} [quietImage] Image URL to use for the quiet prompt (defaults to empty string)
3380 * @property {string} [quietName] Name to use for the quiet prompt (defaults to "System:")
3381 * @property {number} [depth] Recursion depth for the generation. Used to prevent infinite loops in tool calls.
3382 * @property {JsonSchema} [jsonSchema] JSON schema to use for the structured generation. Usually requires a special instruction.
3383 */
3384
3385/**
3975 * MARK:Generate()3386 * MARK:Generate()
3976 * Runs a generation using the current chat context.3387 * Runs a generation using the current chat context.
3977 * @param {string} type Generation type3388 * @param {string} type Generation type
3978 * @param {GenerateOptions} options Generation options3389 * @param {GenerateOptions} options Generation options
3979 * @param {boolean} dryRun Whether to actually generate a message or just assemble the prompt3390 * @param {boolean} dryRun Whether to actually generate a message or just assemble the prompt
3980 * @returns {Promise<any>} Returns a promise that resolves when the text is done generating.3391 * @returns {Promise<any>} Returns a promise that resolves when the text is done generating.
3981 * @typedef {{automatic_trigger?: boolean, force_name2?: boolean, quiet_prompt?: string, quietToLoud?: boolean, skipWIAN?: boolean, force_chid?: number, signal?: AbortSignal, quietImage?: string, quietName?: string, depth?: number }} GenerateOptions
3982 */3392 */
3983export async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName, depth = 0 } = {}, dryRun = false) {3393export async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName, jsonSchema = null, depth = 0 } = {}, dryRun = false) {
3984 console.log('Generate entered');3394 console.log('Generate entered');
3985 setGenerationProgress(0);3395 setGenerationProgress(0);
3986 generation_started = new Date();3396 generation_started = new Date();
@@ -4134,7 +3544,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4134 'impersonate',3544 'impersonate',
4135 'quiet',3545 'quiet',
4136 'continue',3546 'continue',
4137 'ask_command',
4138 ];3547 ];
4139 //for normal messages sent from user..3548 //for normal messages sent from user..
4140 if ((textareaText != '' || (hasPendingFileAttachment() && !noAttachTypes.includes(type))) && !automatic_trigger && type !== 'quiet' && !dryRun) {3549 if ((textareaText != '' || (hasPendingFileAttachment() && !noAttachTypes.includes(type))) && !automatic_trigger && type !== 'quiet' && !dryRun) {
@@ -4316,6 +3725,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4316 // Make quiet prompt available for WIAN3725 // Make quiet prompt available for WIAN
4317 setExtensionPrompt('QUIET_PROMPT', quiet_prompt || '', extension_prompt_types.IN_PROMPT, 0, true);3726 setExtensionPrompt('QUIET_PROMPT', quiet_prompt || '', extension_prompt_types.IN_PROMPT, 0, true);
4318 const chatForWI = coreChat.map(x => world_info_include_names ? `${x.name}: ${x.mes}` : x.mes).reverse();3727 const chatForWI = coreChat.map(x => world_info_include_names ? `${x.name}: ${x.mes}` : x.mes).reverse();
3728 /** @type {import('./scripts/world-info.js').WIGlobalScanData} */
4319 const globalScanData = {3729 const globalScanData = {
4320 personaDescription: persona,3730 personaDescription: persona,
4321 characterDescription: description,3731 characterDescription: description,
@@ -4323,6 +3733,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4323 characterDepthPrompt: charDepthPrompt,3733 characterDepthPrompt: charDepthPrompt,
4324 scenario: scenario,3734 scenario: scenario,
4325 creatorNotes: creatorNotes,3735 creatorNotes: creatorNotes,
3736 trigger: GENERATION_TYPE_TRIGGERS.includes(type) ? type : 'normal',
4326 };3737 };
4327 const { worldInfoString, worldInfoBefore, worldInfoAfter, worldInfoExamples, worldInfoDepth } = await getWorldInfoPrompt(chatForWI, this_max_context, dryRun, globalScanData);3738 const { worldInfoString, worldInfoBefore, worldInfoAfter, worldInfoExamples, worldInfoDepth } = await getWorldInfoPrompt(chatForWI, this_max_context, dryRun, globalScanData);
4328 setExtensionPrompt('QUIET_PROMPT', '', extension_prompt_types.IN_PROMPT, 0, true);3739 setExtensionPrompt('QUIET_PROMPT', '', extension_prompt_types.IN_PROMPT, 0, true);
@@ -4937,12 +4348,12 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4937 gui_settings: true,4348 gui_settings: true,
4938 max_length: maxLength,4349 max_length: maxLength,
4939 max_context_length: max_context,4350 max_context_length: max_context,
4940 api_server,4351 api_server: kai_settings.api_server,
4941 };4352 };
49424353
4943 if (preset_settings != 'gui') {4354 if (kai_settings.preset_settings != 'gui') {
4944 const isHorde = main_api == 'koboldhorde';4355 const isHorde = main_api == 'koboldhorde';
4945 const presetSettings = koboldai_settings[koboldai_setting_names[preset_settings]];4356 const presetSettings = koboldai_settings[koboldai_setting_names[kai_settings.preset_settings]];
4946 const maxContext = (adjustedParams && horde_settings.auto_adjust_context_length) ? adjustedParams.maxContextLength : max_context;4357 const maxContext = (adjustedParams && horde_settings.auto_adjust_context_length) ? adjustedParams.maxContextLength : max_context;
4947 generate_data = getKoboldGenerationData(finalPrompt, presetSettings, maxLength, maxContext, isHorde, type);4358 generate_data = getKoboldGenerationData(finalPrompt, presetSettings, maxLength, maxContext, isHorde, type);
4948 }4359 }
@@ -4974,7 +4385,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
4974 cyclePrompt: cyclePrompt,4385 cyclePrompt: cyclePrompt,
4975 systemPromptOverride: system,4386 systemPromptOverride: system,
4976 jailbreakPromptOverride: jailbreak,4387 jailbreakPromptOverride: jailbreak,
4977 personaDescription: persona,
4978 messages: oaiMessages,4388 messages: oaiMessages,
4979 messageExamples: oaiMessageExamples,4389 messageExamples: oaiMessageExamples,
4980 }, dryRun);4390 }, dryRun);
@@ -5122,7 +4532,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
5122 });4532 });
5123 }4533 }
5124 } else {4534 } else {
5125 return await sendGenerationRequest(type, generate_data);4535 return await sendGenerationRequest(type, generate_data, { jsonSchema });
5126 }4536 }
5127 }4537 }
51284538
@@ -5154,6 +4564,12 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
5154 throw new Error(data?.response);4564 throw new Error(data?.response);
5155 }4565 }
51564566
4567 if (jsonSchema) {
4568 unblockGeneration(type);
4569 generatedPromptCache = '';
4570 return extractJsonFromData(data);
4571 }
4572
5157 //const getData = await response.json();4573 //const getData = await response.json();
5158 let getMessage = extractMessageFromData(data);4574 let getMessage = extractMessageFromData(data);
5159 let title = extractTitleFromData(data);4575 let title = extractTitleFromData(data);
@@ -5567,7 +4983,7 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul
55674983
5568 // Lock user avatar to a persona.4984 // Lock user avatar to a persona.
5569 if (avatar in power_user.personas) {4985 if (avatar in power_user.personas) {
5570 message.force_avatar = getUserAvatar(avatar);4986 message.force_avatar = getThumbnailUrl('persona', avatar);
5571 }4987 }
55724988
5573 if (messageBias) {4989 if (messageBias) {
@@ -5680,7 +5096,7 @@ function addChatsSeparator(mesSendString) {
5680 }5096 }
5681}5097}
56825098
5683async function duplicateCharacter() {5099export async function duplicateCharacter() {
5684 if (this_chid === undefined || !characters[this_chid]) {5100 if (this_chid === undefined || !characters[this_chid]) {
5685 toastr.warning(t`You must first select a character to duplicate!`);5101 toastr.warning(t`You must first select a character to duplicate!`);
5686 return '';5102 return '';
@@ -5710,231 +5126,6 @@ async function duplicateCharacter() {
5710 return '';5126 return '';
5711}5127}
57125128
5713export async function itemizedParams(itemizedPrompts, thisPromptSet, incomingMesId) {
5714 const params = {
5715 charDescriptionTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].charDescription),
5716 charPersonalityTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].charPersonality),
5717 scenarioTextTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].scenarioText),
5718 userPersonaStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].userPersona),
5719 worldInfoStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].worldInfoString),
5720 allAnchorsTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].allAnchors),
5721 summarizeStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].summarizeString),
5722 authorsNoteStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].authorsNoteString),
5723 smartContextStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].smartContextString),
5724 beforeScenarioAnchorTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].beforeScenarioAnchor),
5725 afterScenarioAnchorTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].afterScenarioAnchor),
5726 zeroDepthAnchorTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].zeroDepthAnchor), // TODO: unused
5727 thisPrompt_padding: itemizedPrompts[thisPromptSet].padding,
5728 this_main_api: itemizedPrompts[thisPromptSet].main_api,
5729 chatInjects: await getTokenCountAsync(itemizedPrompts[thisPromptSet].chatInjects),
5730 chatVectorsStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].chatVectorsString),
5731 dataBankVectorsStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].dataBankVectorsString),
5732 modelUsed: chat[incomingMesId]?.extra?.model,
5733 apiUsed: chat[incomingMesId]?.extra?.api,
5734 presetName: itemizedPrompts[thisPromptSet].presetName || t`(Unknown)`,
5735 messagesCount: String(itemizedPrompts[thisPromptSet].messagesCount ?? ''),
5736 examplesCount: String(itemizedPrompts[thisPromptSet].examplesCount ?? ''),
5737 };
5738
5739 const getFriendlyName = (value) => $(`#rm_api_block select option[value="${value}"]`).first().text() || value;
5740
5741 if (params.apiUsed) {
5742 params.apiUsed = getFriendlyName(params.apiUsed);
5743 }
5744
5745 if (params.this_main_api) {
5746 params.mainApiFriendlyName = getFriendlyName(params.this_main_api);
5747 }
5748
5749 if (params.chatInjects) {
5750 params.ActualChatHistoryTokens = params.ActualChatHistoryTokens - params.chatInjects;
5751 }
5752
5753 if (params.this_main_api == 'openai') {
5754 //for OAI API
5755 //console.log('-- Counting OAI Tokens');
5756
5757 //params.finalPromptTokens = itemizedPrompts[thisPromptSet].oaiTotalTokens;
5758 params.oaiMainTokens = itemizedPrompts[thisPromptSet].oaiMainTokens;
5759 params.oaiStartTokens = itemizedPrompts[thisPromptSet].oaiStartTokens;
5760 params.ActualChatHistoryTokens = itemizedPrompts[thisPromptSet].oaiConversationTokens;
5761 params.examplesStringTokens = itemizedPrompts[thisPromptSet].oaiExamplesTokens;
5762 params.oaiPromptTokens = itemizedPrompts[thisPromptSet].oaiPromptTokens - (params.afterScenarioAnchorTokens + params.beforeScenarioAnchorTokens) + params.examplesStringTokens;
5763 params.oaiBiasTokens = itemizedPrompts[thisPromptSet].oaiBiasTokens;
5764 params.oaiJailbreakTokens = itemizedPrompts[thisPromptSet].oaiJailbreakTokens;
5765 params.oaiNudgeTokens = itemizedPrompts[thisPromptSet].oaiNudgeTokens;
5766 params.oaiImpersonateTokens = itemizedPrompts[thisPromptSet].oaiImpersonateTokens;
5767 params.oaiNsfwTokens = itemizedPrompts[thisPromptSet].oaiNsfwTokens;
5768 params.finalPromptTokens =
5769 params.oaiStartTokens +
5770 params.oaiPromptTokens +
5771 params.oaiMainTokens +
5772 params.oaiNsfwTokens +
5773 params.oaiBiasTokens +
5774 params.oaiImpersonateTokens +
5775 params.oaiJailbreakTokens +
5776 params.oaiNudgeTokens +
5777 params.ActualChatHistoryTokens +
5778 //charDescriptionTokens +
5779 //charPersonalityTokens +
5780 //allAnchorsTokens +
5781 params.worldInfoStringTokens +
5782 params.beforeScenarioAnchorTokens +
5783 params.afterScenarioAnchorTokens;
5784 // Max context size - max completion tokens
5785 params.thisPrompt_max_context = (oai_settings.openai_max_context - oai_settings.openai_max_tokens);
5786
5787 //console.log('-- applying % on OAI tokens');
5788 params.oaiStartTokensPercentage = ((params.oaiStartTokens / (params.finalPromptTokens)) * 100).toFixed(2);
5789 params.storyStringTokensPercentage = (((params.afterScenarioAnchorTokens + params.beforeScenarioAnchorTokens + params.oaiPromptTokens) / (params.finalPromptTokens)) * 100).toFixed(2);
5790 params.ActualChatHistoryTokensPercentage = ((params.ActualChatHistoryTokens / (params.finalPromptTokens)) * 100).toFixed(2);
5791 params.promptBiasTokensPercentage = ((params.oaiBiasTokens / (params.finalPromptTokens)) * 100).toFixed(2);
5792 params.worldInfoStringTokensPercentage = ((params.worldInfoStringTokens / (params.finalPromptTokens)) * 100).toFixed(2);
5793 params.allAnchorsTokensPercentage = ((params.allAnchorsTokens / (params.finalPromptTokens)) * 100).toFixed(2);
5794 params.selectedTokenizer = getFriendlyTokenizerName(params.this_main_api).tokenizerName;
5795 params.oaiSystemTokens = params.oaiImpersonateTokens + params.oaiJailbreakTokens + params.oaiNudgeTokens + params.oaiStartTokens + params.oaiNsfwTokens + params.oaiMainTokens;
5796 params.oaiSystemTokensPercentage = ((params.oaiSystemTokens / (params.finalPromptTokens)) * 100).toFixed(2);
5797 } else {
5798 //for non-OAI APIs
5799 //console.log('-- Counting non-OAI Tokens');
5800 params.finalPromptTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].finalPrompt);
5801 params.storyStringTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].storyString) - params.worldInfoStringTokens;
5802 params.examplesStringTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].examplesString);
5803 params.mesSendStringTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].mesSendString);
5804 params.ActualChatHistoryTokens = params.mesSendStringTokens - (params.allAnchorsTokens - (params.beforeScenarioAnchorTokens + params.afterScenarioAnchorTokens)) + power_user.token_padding;
5805 params.instructionTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].instruction);
5806 params.promptBiasTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].promptBias);
5807
5808 params.totalTokensInPrompt =
5809 params.storyStringTokens + //chardefs total
5810 params.worldInfoStringTokens +
5811 params.examplesStringTokens + // example messages
5812 params.ActualChatHistoryTokens + //chat history
5813 params.allAnchorsTokens + // AN and/or legacy anchors
5814 //afterScenarioAnchorTokens + //only counts if AN is set to 'after scenario'
5815 //zeroDepthAnchorTokens + //same as above, even if AN not on 0 depth
5816 params.promptBiasTokens; //{{}}
5817 //- thisPrompt_padding; //not sure this way of calculating is correct, but the math results in same value as 'finalPrompt'
5818 params.thisPrompt_max_context = itemizedPrompts[thisPromptSet].this_max_context;
5819 params.thisPrompt_actual = params.thisPrompt_max_context - params.thisPrompt_padding;
5820
5821 //console.log('-- applying % on non-OAI tokens');
5822 params.storyStringTokensPercentage = ((params.storyStringTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
5823 params.ActualChatHistoryTokensPercentage = ((params.ActualChatHistoryTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
5824 params.promptBiasTokensPercentage = ((params.promptBiasTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
5825 params.worldInfoStringTokensPercentage = ((params.worldInfoStringTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
5826 params.allAnchorsTokensPercentage = ((params.allAnchorsTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
5827 params.selectedTokenizer = itemizedPrompts[thisPromptSet]?.tokenizer || getFriendlyTokenizerName(params.this_main_api).tokenizerName;
5828 }
5829 return params;
5830}
5831
5832export function findItemizedPromptSet(itemizedPrompts, incomingMesId) {
5833 var thisPromptSet = undefined;
5834
5835 for (var i = 0; i < itemizedPrompts.length; i++) {
5836 console.log(`looking for ${incomingMesId} vs ${itemizedPrompts[i].mesId}`);
5837 if (itemizedPrompts[i].mesId === incomingMesId) {
5838 console.log(`found matching mesID ${i}`);
5839 thisPromptSet = i;
5840 PromptArrayItemForRawPromptDisplay = i;
5841 console.log(`wanting to raw display of ArrayItem: ${PromptArrayItemForRawPromptDisplay} which is mesID ${incomingMesId}`);
5842 console.log(itemizedPrompts[thisPromptSet]);
5843 break;
5844 } else if (itemizedPrompts[i].rawPrompt) {
5845 priorPromptArrayItemForRawPromptDisplay = i;
5846 }
5847 }
5848 return thisPromptSet;
5849}
5850
5851async function promptItemize(itemizedPrompts, requestedMesId) {
5852 console.log('PROMPT ITEMIZE ENTERED');
5853 var incomingMesId = Number(requestedMesId);
5854 console.debug(`looking for MesId ${incomingMesId}`);
5855 var thisPromptSet = findItemizedPromptSet(itemizedPrompts, incomingMesId);
5856
5857 if (thisPromptSet === undefined) {
5858 console.log(`couldnt find the right mesId. looked for ${incomingMesId}`);
5859 console.log(itemizedPrompts);
5860 return null;
5861 }
5862
5863 const params = await itemizedParams(itemizedPrompts, thisPromptSet, incomingMesId);
5864 const flatten = (rawPrompt) => Array.isArray(rawPrompt) ? rawPrompt.map(x => x.content).join('\n') : rawPrompt;
5865
5866 const template = params.this_main_api == 'openai'
5867 ? await renderTemplateAsync('itemizationChat', params)
5868 : await renderTemplateAsync('itemizationText', params);
5869
5870 const popup = new Popup(template, POPUP_TYPE.TEXT);
5871
5872 /** @type {HTMLElement} */
5873 const diffPrevPrompt = popup.dlg.querySelector('#diffPrevPrompt');
5874 if (priorPromptArrayItemForRawPromptDisplay) {
5875 diffPrevPrompt.style.display = '';
5876 diffPrevPrompt.addEventListener('click', function () {
5877 const dmp = new DiffMatchPatch();
5878 const text1 = flatten(itemizedPrompts[priorPromptArrayItemForRawPromptDisplay].rawPrompt);
5879 const text2 = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
5880
5881 dmp.Diff_Timeout = 2.0;
5882
5883 const d = dmp.diff_main(text1, text2);
5884 let ds = dmp.diff_prettyHtml(d);
5885 // make it readable
5886 ds = ds.replaceAll('background:#e6ffe6;', 'background:#b9f3b9; color:black;');
5887 ds = ds.replaceAll('background:#ffe6e6;', 'background:#f5b4b4; color:black;');
5888 ds = ds.replaceAll('&para;', '');
5889 const container = document.createElement('div');
5890 container.innerHTML = DOMPurify.sanitize(ds);
5891 const rawPromptWrapper = document.getElementById('rawPromptWrapper');
5892 rawPromptWrapper.replaceChildren(container);
5893 $('#rawPromptPopup').slideToggle();
5894 });
5895 } else {
5896 diffPrevPrompt.style.display = 'none';
5897 }
5898 popup.dlg.querySelector('#copyPromptToClipboard').addEventListener('pointerup', async function () {
5899 let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt;
5900 let rawPromptValues = rawPrompt;
5901
5902 if (Array.isArray(rawPrompt)) {
5903 rawPromptValues = rawPrompt.map(x => x.content).join('\n');
5904 }
5905
5906 await copyText(rawPromptValues);
5907 toastr.info(t`Copied!`);
5908 });
5909
5910 popup.dlg.querySelector('#showRawPrompt').addEventListener('click', async function () {
5911 //console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
5912 console.log(PromptArrayItemForRawPromptDisplay);
5913 console.log(itemizedPrompts);
5914 console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
5915
5916 const rawPrompt = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
5917
5918 // Mobile needs special handholding. The side-view on the popup wouldn't work,
5919 // so we just show an additional popup for this.
5920 if (isMobile()) {
5921 const content = document.createElement('div');
5922 content.classList.add('tokenItemizingMaintext');
5923 content.innerText = rawPrompt;
5924 const popup = new Popup(content, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true, leftAlign: true });
5925 await popup.show();
5926 return;
5927 }
5928
5929 //let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '<br>');
5930 const rawPromptWrapper = document.getElementById('rawPromptWrapper');
5931 rawPromptWrapper.innerText = rawPrompt;
5932 $('#rawPromptPopup').slideToggle();
5933 });
5934
5935 await popup.show();
5936}
5937
5938function setInContextMessages(msgInContextCount, type) {5129function setInContextMessages(msgInContextCount, type) {
5939 $('#chat .mes').removeClass('lastInContext');5130 $('#chat .mes').removeClass('lastInContext');
59405131
@@ -5956,15 +5147,21 @@ function setInContextMessages(msgInContextCount, type) {
5956}5147}
59575148
5958/**5149/**
5150 * @typedef {object} AdditionalRequestOptions
5151 * @property {JsonSchema} [jsonSchema]
5152 */
5153
5154/**
5959 * Sends a non-streaming request to the API.5155 * Sends a non-streaming request to the API.
5960 * @param {string} type Generation type5156 * @param {string} type Generation type
5961 * @param {object} data Generation data5157 * @param {object} data Generation data
5158 * @param {AdditionalRequestOptions} [options] Additional options for the generation request
5962 * @returns {Promise<object>} Response data from the API5159 * @returns {Promise<object>} Response data from the API
5963 * @throws {Error|object}5160 * @throws {Error|object}
5964 */5161 */
5965export async function sendGenerationRequest(type, data) {5162export async function sendGenerationRequest(type, data, options = {}) {
5966 if (main_api === 'openai') {5163 if (main_api === 'openai') {
5967 return await sendOpenAIRequest(type, data.prompt, abortController.signal);5164 return await sendOpenAIRequest(type, data.prompt, abortController.signal, options);
5968 }5165 }
59695166
5970 if (main_api === 'koboldhorde') {5167 if (main_api === 'koboldhorde') {
@@ -5990,16 +5187,17 @@ export async function sendGenerationRequest(type, data) {
5990 * Sends a streaming request to the API.5187 * Sends a streaming request to the API.
5991 * @param {string} type Generation type5188 * @param {string} type Generation type
5992 * @param {object} data Generation data5189 * @param {object} data Generation data
5190 * @param {AdditionalRequestOptions} [options] Additional options for the generation request
5993 * @returns {Promise<any>} Streaming generator5191 * @returns {Promise<any>} Streaming generator
5994 */5192 */
5995export async function sendStreamingRequest(type, data) {5193export async function sendStreamingRequest(type, data, options = {}) {
5996 if (abortController?.signal?.aborted) {5194 if (abortController?.signal?.aborted) {
5997 throw new Error('Generation was aborted.');5195 throw new Error('Generation was aborted.');
5998 }5196 }
59995197
6000 switch (main_api) {5198 switch (main_api) {
6001 case 'openai':5199 case 'openai':
6002 return await sendOpenAIRequest(type, data.prompt, streamingProcessor.abortController.signal);5200 return await sendOpenAIRequest(type, data.prompt, streamingProcessor.abortController.signal, options);
6003 case 'textgenerationwebui':5201 case 'textgenerationwebui':
6004 return await generateTextGenWithStreaming(data, streamingProcessor.abortController.signal);5202 return await generateTextGenWithStreaming(data, streamingProcessor.abortController.signal);
6005 case 'novel':5203 case 'novel':
@@ -6141,6 +5339,58 @@ export function extractMessageFromData(data, activeApi = null) {
6141}5339}
61425340
6143/**5341/**
5342 * Extracts JSON from the response data.
5343 * @param {object} data Response data
5344 * @returns {string} Extracted JSON string from the response data
5345 */
5346export function extractJsonFromData(data, { mainApi = null, chatCompletionSource = null } = {}) {
5347 mainApi = mainApi ?? main_api;
5348 chatCompletionSource = chatCompletionSource ?? oai_settings.chat_completion_source;
5349
5350 const tryParse = (/** @type {string} */ value) => {
5351 try {
5352 return JSON.parse(value);
5353 } catch (e) {
5354 console.debug('Failed to parse content as JSON.', e);
5355 }
5356 };
5357
5358 let result = {};
5359
5360 switch (mainApi) {
5361 case 'openai': {
5362 const text = extractMessageFromData(data, mainApi);
5363 switch (chatCompletionSource) {
5364 case chat_completion_sources.CLAUDE:
5365 result = data?.content?.find(x => x.type === 'tool_use')?.input;
5366 break;
5367 case chat_completion_sources.PERPLEXITY:
5368 result = tryParse(removeReasoningFromString(text));
5369 break;
5370 case chat_completion_sources.VERTEXAI:
5371 case chat_completion_sources.MAKERSUITE:
5372 case chat_completion_sources.DEEPSEEK:
5373 case chat_completion_sources.AI21:
5374 case chat_completion_sources.GROQ:
5375 case chat_completion_sources.POLLINATIONS:
5376 case chat_completion_sources.AIMLAPI:
5377 case chat_completion_sources.OPENAI:
5378 case chat_completion_sources.OPENROUTER:
5379 case chat_completion_sources.MISTRALAI:
5380 case chat_completion_sources.CUSTOM:
5381 case chat_completion_sources.COHERE:
5382 case chat_completion_sources.XAI:
5383 default:
5384 result = tryParse(text);
5385 break;
5386 }
5387 } break;
5388 }
5389
5390 return JSON.stringify(result ?? {});
5391}
5392
5393/**
6144 * Extracts multiswipe swipes from the response data.5394 * Extracts multiswipe swipes from the response data.
6145 * @param {Object} data Response data5395 * @param {Object} data Response data
6146 * @param {string} type Type of generation5396 * @param {string} type Type of generation
@@ -6551,7 +5801,7 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
6551 send_date: item['send_date'],5801 send_date: item['send_date'],
6552 gen_started: item['gen_started'],5802 gen_started: item['gen_started'],
6553 gen_finished: item['gen_finished'],5803 gen_finished: item['gen_finished'],
6554 extra: JSON.parse(JSON.stringify(item['extra'])),5804 extra: structuredClone(item['extra']),
6555 };5805 };
6556 } else {5806 } else {
6557 item['swipe_id'] = 0;5807 item['swipe_id'] = 0;
@@ -6561,7 +5811,7 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
6561 send_date: chat[chat.length - 1]['send_date'],5811 send_date: chat[chat.length - 1]['send_date'],
6562 gen_started: chat[chat.length - 1]['gen_started'],5812 gen_started: chat[chat.length - 1]['gen_started'],
6563 gen_finished: chat[chat.length - 1]['gen_finished'],5813 gen_finished: chat[chat.length - 1]['gen_finished'],
6564 extra: JSON.parse(JSON.stringify(chat[chat.length - 1]['extra'])),5814 extra: structuredClone(chat[chat.length - 1]['extra']),
6565 };5815 };
6566 }5816 }
65675817
@@ -7196,7 +6446,7 @@ async function read_avatar_load(input) {
7196 await delay(DEFAULT_SAVE_EDIT_TIMEOUT);6446 await delay(DEFAULT_SAVE_EDIT_TIMEOUT);
71976447
7198 const formData = new FormData(/** @type {HTMLFormElement} */($('#form_create').get(0)));6448 const formData = new FormData(/** @type {HTMLFormElement} */($('#form_create').get(0)));
7199 await fetch(getThumbnailUrl('avatar', formData.get('avatar_url')), {6449 await fetch(getThumbnailUrl('avatar', formData.get('avatar_url').toString()), {
7200 method: 'GET',6450 method: 'GET',
7201 cache: 'no-cache',6451 cache: 'no-cache',
7202 headers: {6452 headers: {
@@ -7225,8 +6475,15 @@ async function read_avatar_load(input) {
7225 }6475 }
7226}6476}
72276477
7228export function getThumbnailUrl(type, file) {6478/**
7229 return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}`;6479 * Gets the URL for a thumbnail of a specific type and file.
6480 * @param {import('../src/endpoints/thumbnails.js').ThumbnailType} type The type of the thumbnail to get
6481 * @param {string} file The file name or path for which to get the thumbnail URL
6482 * @param {boolean} [t=false] Whether to add a cache-busting timestamp to the URL
6483 * @returns {string} The URL for the thumbnail
6484 */
6485export function getThumbnailUrl(type, file, t = false) {
6486 return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}${t ? `&t=${Date.now()}` : ''}`;
7230}6487}
72316488
7232export function buildAvatarList(block, entities, { templateId = 'inline_avatar_template', empty = true, interactable = false, highlightFavs = true } = {}) {6489export function buildAvatarList(block, entities, { templateId = 'inline_avatar_template', empty = true, interactable = false, highlightFavs = true } = {}) {
@@ -7266,7 +6523,7 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t
7266 }6523 }
7267 else if (entity.type === 'persona') {6524 else if (entity.type === 'persona') {
7268 avatarTemplate.attr({ 'data-pid': id, 'data-chid': null });6525 avatarTemplate.attr({ 'data-pid': id, 'data-chid': null });
7269 avatarTemplate.find('img').attr('src', getUserAvatar(entity.item.avatar));6526 avatarTemplate.find('img').attr('src', getThumbnailUrl('persona', entity.item.avatar));
7270 avatarTemplate.attr('title', `[Persona] ${entity.item.name}\nFile: ${entity.item.avatar}`);6527 avatarTemplate.attr('title', `[Persona] ${entity.item.name}\nFile: ${entity.item.avatar}`);
7271 }6528 }
72726529
@@ -7518,6 +6775,8 @@ export function changeMainAPI() {
7518 $('#ai_module_block_novel').css('display', 'none');6775 $('#ai_module_block_novel').css('display', 'none');
7519 }6776 }
75206777
6778 $('#prompt_cost_block').toggle(selectedVal === 'textgenerationwebui');
6779
7521 // Hide common settings for OpenAI6780 // Hide common settings for OpenAI
7522 console.debug('value?', selectedVal);6781 console.debug('value?', selectedVal);
7523 if (selectedVal == 'openai') {6782 if (selectedVal == 'openai') {
@@ -7530,10 +6789,6 @@ export function changeMainAPI() {
7530 main_api = selectedVal;6789 main_api = selectedVal;
7531 setOnlineStatus('no_connection');6790 setOnlineStatus('no_connection');
75326791
7533 if (main_api == 'openai' && oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI) {
7534 $('#api_button_openai').trigger('click');
7535 }
7536
7537 if (main_api == 'koboldhorde') {6792 if (main_api == 'koboldhorde') {
7538 getStatusHorde();6793 getStatusHorde();
7539 getHordeModels(true);6794 getHordeModels(true);
@@ -7611,59 +6866,7 @@ export async function getSettings() {
7611 // Allow subscribers to mutate settings6866 // Allow subscribers to mutate settings
7612 await eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings);6867 await eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings);
76136868
7614 //Load KoboldAI settings
7615 koboldai_setting_names = data.koboldai_setting_names;
7616 koboldai_settings = data.koboldai_settings;
7617 koboldai_settings.forEach(function (item, i, arr) {
7618 koboldai_settings[i] = JSON.parse(item);
7619 });
7620
7621 let arr_holder = {};
7622
7623 $('#settings_preset').empty();
7624 $('#settings_preset').append(
7625 '<option value="gui">GUI KoboldAI Settings</option>',
7626 ); //adding in the GUI settings, since it is not loaded dynamically
7627
7628 koboldai_setting_names.forEach(function (item, i, arr) {
7629 arr_holder[item] = i;
7630 $('#settings_preset').append(`<option value=${i}>${item}</option>`);
7631 //console.log('loading preset #'+i+' -- '+item);
7632 });
7633 koboldai_setting_names = {};
7634 koboldai_setting_names = arr_holder;
7635 preset_settings = settings.preset_settings;
7636
7637 if (preset_settings == 'gui') {
7638 selectKoboldGuiPreset();
7639 } else {
7640 if (typeof koboldai_setting_names[preset_settings] !== 'undefined') {
7641 $(`#settings_preset option[value=${koboldai_setting_names[preset_settings]}]`)
7642 .attr('selected', 'true');
7643 } else {
7644 preset_settings = 'gui';
7645 selectKoboldGuiPreset();
7646 }
7647 }
7648
7649 novelai_setting_names = data.novelai_setting_names;
7650 novelai_settings = data.novelai_settings;
7651 novelai_settings.forEach(function (item, i, arr) {
7652 novelai_settings[i] = JSON.parse(item);
7653 });
7654 arr_holder = {};
7655
7656 $('#settings_preset_novel').empty();
7657
7658 novelai_setting_names.forEach(function (item, i, arr) {
7659 arr_holder[item] = i;
7660 $('#settings_preset_novel').append(`<option value=${i}>${item}</option>`);
7661 });
7662 novelai_setting_names = {};
7663 novelai_setting_names = arr_holder;
7664
7665 //Load AI model config settings6869 //Load AI model config settings
7666
7667 amount_gen = settings.amount_gen;6870 amount_gen = settings.amount_gen;
7668 if (settings.max_context !== undefined)6871 if (settings.max_context !== undefined)
7669 max_context = parseInt(settings.max_context);6872 max_context = parseInt(settings.max_context);
@@ -7674,16 +6877,14 @@ export async function getSettings() {
7674 showSwipeButtons();6877 showSwipeButtons();
76756878
7676 // Kobold6879 // Kobold
7677 loadKoboldSettings(settings.kai_settings ?? settings);6880 loadKoboldSettings(data, settings.kai_settings ?? settings, settings);
76786881
7679 // Novel6882 // Novel
7680 loadNovelSettings(settings.nai_settings ?? settings);6883 loadNovelSettings(data, settings.nai_settings ?? settings);
7681 $(`#settings_preset_novel option[value=${novelai_setting_names[nai_settings.preset_settings_novel]}]`).attr('selected', 'true');
76826884
7683 // TextGen6885 // TextGen
7684 loadTextGenSettings(data, settings);6886 loadTextGenSettings(data, settings);
76856887
7686
7687 // OpenAI6888 // OpenAI
7688 loadOpenAISettings(data, settings.oai_settings ?? settings);6889 loadOpenAISettings(data, settings.oai_settings ?? settings);
76896890
@@ -7726,13 +6927,9 @@ export async function getSettings() {
77266927
7727 main_api = settings.main_api;6928 main_api = settings.main_api;
7728 $('#main_api').val(main_api);6929 $('#main_api').val(main_api);
7729 $('#main_api option[value=' + main_api + ']').attr(6930 $(`#main_api option[value=${main_api}]`).attr('selected', 'true');
7730 'selected',
7731 'true',
7732 );
7733 changeMainAPI();6931 changeMainAPI();
77346932
7735
7736 //Load User's Name and Avatar6933 //Load User's Name and Avatar
7737 initUserAvatar(settings.user_avatar);6934 initUserAvatar(settings.user_avatar);
7738 setPersonaDescription();6935 setPersonaDescription();
@@ -7741,10 +6938,6 @@ export async function getSettings() {
7741 active_character = settings.active_character;6938 active_character = settings.active_character;
7742 active_group = settings.active_group;6939 active_group = settings.active_group;
77436940
7744 //Load the API server URL from settings
7745 api_server = settings.api_server;
7746 $('#api_url_text').val(api_server);
7747
7748 setWorldInfoSettings(settings.world_info_settings ?? settings, data);6941 setWorldInfoSettings(settings.world_info_settings ?? settings, data);
77496942
7750 selected_button = settings.selected_button;6943 selected_button = settings.selected_button;
@@ -7769,12 +6962,6 @@ export async function getSettings() {
7769 await eventSource.emit(event_types.SETTINGS_LOADED);6962 await eventSource.emit(event_types.SETTINGS_LOADED);
7770}6963}
77716964
7772function selectKoboldGuiPreset() {
7773 $('#settings_preset option[value=gui]')
7774 .attr('selected', 'true')
7775 .trigger('change');
7776}
7777
7778//MARK: saveSettings()6965//MARK: saveSettings()
7779export async function saveSettings(loopCounter = 0) {6966export async function saveSettings(loopCounter = 0) {
7780 if (!settingsReady) {6967 if (!settingsReady) {
@@ -7794,52 +6981,51 @@ export async function saveSettings(loopCounter = 0) {
7794 TempResponseLength.restore(null);6981 TempResponseLength.restore(null);
7795 }6982 }
77966983
7797 //console.log('Entering settings with name1 = '+name1);6984 const payload = {
7798 return jQuery.ajax({6985 firstRun: firstRun,
7799 type: 'POST',6986 accountStorage: accountStorage.getState(),
7800 url: '/api/settings/save',6987 currentVersion: currentVersion,
7801 data: JSON.stringify({6988 username: name1,
7802 firstRun: firstRun,6989 active_character: active_character,
7803 accountStorage: accountStorage.getState(),6990 active_group: active_group,
7804 currentVersion: currentVersion,6991 user_avatar: user_avatar,
7805 username: name1,6992 amount_gen: amount_gen,
7806 active_character: active_character,6993 max_context: max_context,
7807 active_group: active_group,6994 main_api: main_api,
7808 api_server: api_server,6995 world_info_settings: getWorldInfoSettings(),
7809 preset_settings: preset_settings,6996 textgenerationwebui_settings: textgen_settings,
7810 user_avatar: user_avatar,6997 swipes: swipes,
7811 amount_gen: amount_gen,6998 horde_settings: horde_settings,
7812 max_context: max_context,6999 power_user: power_user,
7813 main_api: main_api,7000 extension_settings: extension_settings,
7814 world_info_settings: getWorldInfoSettings(),7001 tags: tags,
7815 textgenerationwebui_settings: textgen_settings,7002 tag_map: tag_map,
7816 swipes: swipes,7003 nai_settings: nai_settings,
7817 horde_settings: horde_settings,7004 kai_settings: kai_settings,
7818 power_user: power_user,7005 oai_settings: oai_settings,
7819 extension_settings: extension_settings,7006 background: background_settings,
7820 tags: tags,7007 proxies: proxies,
7821 tag_map: tag_map,7008 selected_proxy: selected_proxy,
7822 nai_settings: nai_settings,7009 };
7823 kai_settings: kai_settings,7010
7824 oai_settings: oai_settings,7011 try {
7825 background: background_settings,7012 const result = await fetch('/api/settings/save', {
7826 proxies: proxies,7013 method: 'POST',
7827 selected_proxy: selected_proxy,7014 headers: getRequestHeaders(),
7828 }, null, 4),7015 body: JSON.stringify(payload),
7829 beforeSend: function () { },7016 cache: 'no-cache',
7830 cache: false,7017 });
7831 dataType: 'json',7018
7832 contentType: 'application/json',7019 if (!result.ok) {
7833 //processData: false,7020 throw new Error(`Failed to save settings: ${result.statusText}`);
7834 success: async function (data) {7021 }
7835 eventSource.emit(event_types.SETTINGS_UPDATED);7022
7836 },7023 settings = payload;
7837 error: function (jqXHR, exception) {7024 await eventSource.emit(event_types.SETTINGS_UPDATED);
7838 toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Settings could not be saved`);7025 } catch (error) {
7839 console.log(exception);7026 console.error('Error saving settings:', error);
7840 console.log(jqXHR);7027 toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Settings could not be saved`);
7841 },7028 }
7842 });
7843}7029}
78447030
7845/**7031/**
@@ -8094,7 +7280,7 @@ export async function getPastCharacterChats(characterId = null) {
8094/**7280/**
8095 * Helper for `displayPastChats`, to make the same info consistently available for other functions7281 * Helper for `displayPastChats`, to make the same info consistently available for other functions
8096 */7282 */
8097function getCurrentChatDetails() {7283export function getCurrentChatDetails() {
8098 if (!characters[this_chid] && !selected_group) {7284 if (!characters[this_chid] && !selected_group) {
8099 return { sessionName: '', group: null, characterName: '', avatarImgURL: '' };7285 return { sessionName: '', group: null, characterName: '', avatarImgURL: '' };
8100 }7286 }
@@ -8649,7 +7835,6 @@ export function showSwipeButtons() {
8649 !swipes ||7835 !swipes ||
8650 Number($('.mes:last').attr('mesid')) < 0 ||7836 Number($('.mes:last').attr('mesid')) < 0 ||
8651 chat[chat.length - 1].is_user ||7837 chat[chat.length - 1].is_user ||
8652 chat[chat.length - 1].extra?.image ||
8653 (selected_group && is_group_generating)7838 (selected_group && is_group_generating)
8654 ) { return; }7839 ) { return; }
86557840
@@ -8665,6 +7850,13 @@ export function showSwipeButtons() {
8665 chat[chat.length - 1]['swipe_id'] = 0; // set it to id 07850 chat[chat.length - 1]['swipe_id'] = 0; // set it to id 0
8666 chat[chat.length - 1]['swipes'] = []; // empty the array7851 chat[chat.length - 1]['swipes'] = []; // empty the array
8667 chat[chat.length - 1]['swipes'][0] = chat[chat.length - 1]['mes']; //assign swipe array with last message from chat7852 chat[chat.length - 1]['swipes'][0] = chat[chat.length - 1]['mes']; //assign swipe array with last message from chat
7853 chat[chat.length - 1]['swipe_info'] = [];
7854 chat[chat.length - 1]['swipe_info'][0] = {
7855 'send_date': chat[chat.length - 1]['send_date'],
7856 'gen_started': chat[chat.length - 1]['gen_started'],
7857 'gen_finished': chat[chat.length - 1]['gen_finished'],
7858 'extra': structuredClone(chat[chat.length - 1]['extra']),
7859 };
8668 }7860 }
86697861
8670 const currentMessage = $('#chat').children().filter(`[mesid="${chat.length - 1}"]`);7862 const currentMessage = $('#chat').children().filter(`[mesid="${chat.length - 1}"]`);
@@ -8791,12 +7983,10 @@ export async function saveChatConditional() {
8791 * @param {EventTarget} eventTarget Event target to trigger the event on.7983 * @param {EventTarget} eventTarget Event target to trigger the event on.
8792 */7984 */
8793async function importCharacterChat(formData, eventTarget) {7985async function importCharacterChat(formData, eventTarget) {
8794 const headers = getRequestHeaders();
8795 delete headers['Content-Type'];
8796 const fetchResult = await fetch('/api/chats/import', {7986 const fetchResult = await fetch('/api/chats/import', {
8797 method: 'POST',7987 method: 'POST',
8798 body: formData,7988 body: formData,
8799 headers: headers,7989 headers: getRequestHeaders({ omitContentType: true }),
8800 cache: 'no-cache',7990 cache: 'no-cache',
8801 });7991 });
88027992
@@ -8884,15 +8074,6 @@ export function setGenerationProgress(progress) {
8884 }8074 }
8885}8075}
88868076
8887function isHordeGenerationNotAllowed() {
8888 if (main_api == 'koboldhorde' && preset_settings == 'gui') {
8889 toastr.error(t`GUI Settings preset is not supported for Horde. Please select another preset.`);
8890 return true;
8891 }
8892
8893 return false;
8894}
8895
8896export function cancelTtsPlay() {8077export function cancelTtsPlay() {
8897 if ('speechSynthesis' in window) {8078 if ('speechSynthesis' in window) {
8898 speechSynthesis.cancel();8079 speechSynthesis.cancel();
@@ -8904,19 +8085,25 @@ function updateAlternateGreetingsHintVisibility(root) {
8904 $(root).find('.alternate_grettings_hint').toggle(numberOfGreetings == 0);8085 $(root).find('.alternate_grettings_hint').toggle(numberOfGreetings == 0);
8905}8086}
89068087
8907function openCharacterWorldPopup() {8088async function openCharacterWorldPopup() {
8908 const chid = $('#set_character_world').data('chid');8089 const chid = $('#set_character_world').data('chid');
8909
8910 if (menu_type != 'create' && chid === undefined) {8090 if (menu_type != 'create' && chid === undefined) {
8911 toastr.error('Does not have an Id for this character in world select menu.');8091 toastr.error('Does not have an Id for this character in world select menu.');
8912 return;8092 return;
8913 }8093 }
89148094
8915 async function onSelectCharacterWorld() {8095 // TODO: Maybe make this utility function not use the window context?
8916 const value = $('.character_world_info_selector').find('option:selected').val();8096 const fileName = getCharaFilename(chid);
8917 const worldIndex = value !== '' ? Number(value) : NaN;8097 const charName = (menu_type == 'create' ? create_save.name : characters[chid]?.data?.name) || 'Nameless';
8918 const name = !isNaN(worldIndex) ? world_names[worldIndex] : '';8098 const worldId = (menu_type == 'create' ? create_save.world : characters[chid]?.data?.extensions?.world) || '';
8099 const template = $('#character_world_template .character_world').clone();
8100 template.find('.character_name').text(charName);
89198101
8102 // --- Event Handlers ---
8103 async function handlePrimaryWorldSelect() {
8104 const selectedValue = $(this).val();
8105 const worldIndex = selectedValue !== '' ? Number(selectedValue) : NaN;
8106 const name = !isNaN(worldIndex) ? world_names[worldIndex] : '';
8920 const previousValue = $('#character_world').val();8107 const previousValue = $('#character_world').val();
8921 $('#character_world').val(name);8108 $('#character_world').val(name);
89228109
@@ -8944,26 +8131,26 @@ function openCharacterWorldPopup() {
8944 await createOrEditCharacter();8131 await createOrEditCharacter();
8945 }8132 }
89468133
8947 setWorldInfoButtonClass(undefined, !!value);8134 setWorldInfoButtonClass(undefined, !!name);
8948 }8135 }
89498136
8950 function onExtraWorldInfoChanged() {8137 function handleExtrasWorldSelect() {
8951 const selectorFieldValue = $('.character_extra_world_info_selector').val();8138 const selectedValues = $(this).val();
8952 const selectedWorlds = Array.isArray(selectorFieldValue) ? selectorFieldValue : [];8139 const selectedWorlds = Array.isArray(selectedValues) ? selectedValues : [];
8953 let charLore = world_info.charLore ?? [];8140 let charLore = world_info.charLore ?? [];
8141 const tempExtraBooks = selectedWorlds.map((index) => world_names[index]).filter(Boolean);
8142 const existingCharIndex = charLore.findIndex((e) => e.name === fileName);
89548143
8955 // TODO: Maybe make this utility function not use the window context?8144 if (menu_type == 'create') {
8956 const fileName = getCharaFilename(chid);8145 create_save.extra_books = tempExtraBooks;
8957 const tempExtraBooks = selectedWorlds.map((index) => world_names[index]).filter((e) => e !== undefined);8146 return;
8147 }
89588148
8959 const existingCharIndex = charLore.findIndex((e) => e.name === fileName);
8960 if (existingCharIndex === -1) {8149 if (existingCharIndex === -1) {
8961 const newCharLoreEntry = {8150 // Add record only if at least 1 lorebook is selected.
8962 name: fileName,8151 if (tempExtraBooks.length > 0) {
8963 extraBooks: tempExtraBooks,8152 charLore.push({ name: fileName, extraBooks: tempExtraBooks });
8964 };8153 }
8965
8966 charLore.push(newCharLoreEntry);
8967 } else if (tempExtraBooks.length === 0) {8154 } else if (tempExtraBooks.length === 0) {
8968 charLore.splice(existingCharIndex, 1);8155 charLore.splice(existingCharIndex, 1);
8969 } else {8156 } else {
@@ -8974,62 +8161,43 @@ function openCharacterWorldPopup() {
8974 saveSettingsDebounced();8161 saveSettingsDebounced();
8975 }8162 }
89768163
8977 const template = $('#character_world_template .character_world').clone();8164 // --- Populate Dropdowns ---
8978 const select = template.find('.character_world_info_selector');8165 // Append to primary dropdown.
8979 const extraSelect = template.find('.character_extra_world_info_selector');8166 const primarySelect = template.find('.character_world_info_selector');
8980 const name = (menu_type == 'create' ? create_save.name : characters[chid]?.data?.name) || 'Nameless';
8981 const worldId = (menu_type == 'create' ? create_save.world : characters[chid]?.data?.extensions?.world) || '';
8982 template.find('.character_name').text(name);
8983
8984 // Not needed on mobile
8985 if (!isMobile()) {
8986 $(extraSelect).select2({
8987 width: '100%',
8988 placeholder: t`No auxillary Lorebooks set. Click here to select.`,
8989 allowClear: true,
8990 closeOnSelect: false,
8991 });
8992 }
8993
8994 // Apped to base dropdown
8995 world_names.forEach((item, i) => {8167 world_names.forEach((item, i) => {
8996 const option = document.createElement('option');8168 primarySelect.append(new Option(item, String(i), item === worldId, item === worldId));
8997 option.value = String(i);
8998 option.innerText = item;
8999 option.selected = item === worldId;
9000 select.append(option);
9001 });8169 });
90028170
9003 // Append to extras dropdown8171 // Append to extras dropdown.
9004 if (world_names.length > 0) {8172 const extrasSelect = template.find('.character_extra_world_info_selector');
9005 extraSelect.empty();8173 const existingCharLore = world_info.charLore?.find((e) => e.name === fileName);
9006 }
9007 world_names.forEach((item, i) => {8174 world_names.forEach((item, i) => {
9008 const option = document.createElement('option');8175 const array = (menu_type == 'create' ? create_save.extra_books : existingCharLore?.extraBooks);
9009 option.value = String(i);8176 const isSelected = !!array?.includes(item);
9010 option.innerText = item;8177 extrasSelect.append(new Option(item, String(i), isSelected, isSelected));
9011
9012 const existingCharLore = world_info.charLore?.find((e) => e.name === getCharaFilename());
9013 if (existingCharLore) {
9014 option.selected = existingCharLore.extraBooks.includes(item);
9015 } else {
9016 option.selected = false;
9017 }
9018 extraSelect.append(option);
9019 });8178 });
90208179
9021 select.on('change', onSelectCharacterWorld);8180 const popup = new Popup(template, POPUP_TYPE.TEXT, '', {
9022 extraSelect.on('mousedown change', async function (e) {8181 onOpen: function (popup) {
9023 // If there's no world names, don't do anything8182 const popupDialog = $(popup.dlg);
9024 if (world_names.length === 0) {8183
9025 e.preventDefault();8184 primarySelect.on('change', handlePrimaryWorldSelect);
9026 return;8185 extrasSelect.on('change', handleExtrasWorldSelect);
9027 }8186
90288187 // Not needed on mobile.
9029 onExtraWorldInfoChanged();8188 if (!isMobile()) {
8189 extrasSelect.select2({
8190 width: '100%',
8191 placeholder: t`No auxiliary Lorebooks set. Click here to select.`,
8192 allowClear: true,
8193 closeOnSelect: false,
8194 dropdownParent: popupDialog,
8195 });
8196 }
8197 },
9030 });8198 });
90318199
9032 callPopup(template, 'text');8200 await popup.show();
9033}8201}
90348202
9035function openAlternateGreetings() {8203function openAlternateGreetings() {
@@ -9122,8 +8290,7 @@ async function createOrEditCharacter(e) {
9122 formData.set('avatar', convertedFile);8290 formData.set('avatar', convertedFile);
9123 }8291 }
91248292
9125 const headers = getRequestHeaders();8293 const headers = getRequestHeaders({ omitContentType: true });
9126 delete headers['Content-Type'];
91278294
9128 if ($('#form_create').attr('actiontype') == 'createcharacter') {8295 if ($('#form_create').attr('actiontype') == 'createcharacter') {
9129 if (String($('#character_name_pole').val()).length === 0) {8296 if (String($('#character_name_pole').val()).length === 0) {
@@ -9192,6 +8359,15 @@ async function createOrEditCharacter(e) {
9192 field.callback && field.callback(fieldValue);8359 field.callback && field.callback(fieldValue);
9193 });8360 });
91948361
8362 if (Array.isArray(create_save.extra_books) && create_save.extra_books.length > 0) {
8363 const fileName = getCharaFilename(null, { manualAvatarKey: avatarId });
8364 const charLore = world_info.charLore ?? [];
8365 charLore.push({ name: fileName, extraBooks: create_save.extra_books });
8366 Object.assign(world_info, { charLore: charLore });
8367 saveSettingsDebounced();
8368 }
8369 create_save.extra_books = [];
8370
9195 $('#character_popup-button-h3').text('Create character');8371 $('#character_popup-button-h3').text('Create character');
91968372
9197 create_save.avatar = null;8373 create_save.avatar = null;
@@ -9339,7 +8515,7 @@ export function swipe_left(_event, { source, repeated } = {}) {
9339 const this_mes_block_height = this_mes_block[0].scrollHeight;8515 const this_mes_block_height = this_mes_block[0].scrollHeight;
9340 chat[chat.length - 1]['mes'] = chat[chat.length - 1]['swipes'][chat[chat.length - 1]['swipe_id']];8516 chat[chat.length - 1]['mes'] = chat[chat.length - 1]['swipes'][chat[chat.length - 1]['swipe_id']];
9341 chat[chat.length - 1]['send_date'] = chat[chat.length - 1].swipe_info[chat[chat.length - 1]['swipe_id']]?.send_date || chat[chat.length - 1].send_date; //load the last mes box with the latest generation8517 chat[chat.length - 1]['send_date'] = chat[chat.length - 1].swipe_info[chat[chat.length - 1]['swipe_id']]?.send_date || chat[chat.length - 1].send_date; //load the last mes box with the latest generation
9342 chat[chat.length - 1]['extra'] = JSON.parse(JSON.stringify(chat[chat.length - 1].swipe_info[chat[chat.length - 1]['swipe_id']]?.extra || chat[chat.length - 1].extra));8518 chat[chat.length - 1]['extra'] = structuredClone(chat[chat.length - 1].swipe_info[chat[chat.length - 1]['swipe_id']]?.extra || chat[chat.length - 1].extra);
93438519
9344 if (chat[chat.length - 1].extra) {8520 if (chat[chat.length - 1].extra) {
9345 // if message has memory attached - remove it to allow regen8521 // if message has memory attached - remove it to allow regen
@@ -9401,6 +8577,7 @@ export function swipe_left(_event, { source, repeated } = {}) {
9401 easing: animation_easing,8577 easing: animation_easing,
9402 queue: false,8578 queue: false,
9403 complete: async function () {8579 complete: async function () {
8580 appendMediaToMessage(chat[chat.length - 1], $(this).parent().children('.mes_block'));
9404 await eventSource.emit(event_types.MESSAGE_SWIPED, (chat.length - 1));8581 await eventSource.emit(event_types.MESSAGE_SWIPED, (chat.length - 1));
9405 saveChatDebounced();8582 saveChatDebounced();
9406 },8583 },
@@ -9476,7 +8653,7 @@ export function swipe_right(_event = null, { source, repeated } = {}) {
9476 'send_date': chat[chat.length - 1]['send_date'],8653 'send_date': chat[chat.length - 1]['send_date'],
9477 'gen_started': chat[chat.length - 1]['gen_started'],8654 'gen_started': chat[chat.length - 1]['gen_started'],
9478 'gen_finished': chat[chat.length - 1]['gen_finished'],8655 'gen_finished': chat[chat.length - 1]['gen_finished'],
9479 'extra': JSON.parse(JSON.stringify(chat[chat.length - 1]['extra'])),8656 'extra': structuredClone(chat[chat.length - 1]['extra']),
9480 };8657 };
9481 //assign swipe info array with last message from chat8658 //assign swipe info array with last message from chat
9482 }8659 }
@@ -9500,6 +8677,11 @@ export function swipe_right(_event = null, { source, repeated } = {}) {
9500 if (chat[chat.length - 1].extra.display_text) {8677 if (chat[chat.length - 1].extra.display_text) {
9501 delete chat[chat.length - 1].extra.display_text;8678 delete chat[chat.length - 1].extra.display_text;
9502 }8679 }
8680
8681 delete chat[chat.length - 1].extra.image;
8682 delete chat[chat.length - 1].extra.image_swipes;
8683 delete chat[chat.length - 1].extra.video;
8684 delete chat[chat.length - 1].extra.inline_image;
9503 }8685 }
9504 if (!Array.isArray(chat[chat.length - 1]['swipe_info'])) {8686 if (!Array.isArray(chat[chat.length - 1]['swipe_info'])) {
9505 chat[chat.length - 1]['swipe_info'] = [];8687 chat[chat.length - 1]['swipe_info'] = [];
@@ -9512,7 +8694,7 @@ export function swipe_right(_event = null, { source, repeated } = {}) {
9512 } else if (parseInt(chat[chat.length - 1]['swipe_id']) < chat[chat.length - 1]['swipes'].length) { //otherwise, if the id is less than the number of swipes8694 } else if (parseInt(chat[chat.length - 1]['swipe_id']) < chat[chat.length - 1]['swipes'].length) { //otherwise, if the id is less than the number of swipes
9513 chat[chat.length - 1]['mes'] = chat[chat.length - 1]['swipes'][chat[chat.length - 1]['swipe_id']]; //load the last mes box with the latest generation8695 chat[chat.length - 1]['mes'] = chat[chat.length - 1]['swipes'][chat[chat.length - 1]['swipe_id']]; //load the last mes box with the latest generation
9514 chat[chat.length - 1]['send_date'] = chat[chat.length - 1]?.swipe_info[chat[chat.length - 1]['swipe_id']]?.send_date || chat[chat.length - 1]['send_date']; //update send date8696 chat[chat.length - 1]['send_date'] = chat[chat.length - 1]?.swipe_info[chat[chat.length - 1]['swipe_id']]?.send_date || chat[chat.length - 1]['send_date']; //update send date
9515 chat[chat.length - 1]['extra'] = JSON.parse(JSON.stringify(chat[chat.length - 1].swipe_info[chat[chat.length - 1]['swipe_id']]?.extra || chat[chat.length - 1].extra || []));8697 chat[chat.length - 1]['extra'] = structuredClone(chat[chat.length - 1].swipe_info[chat[chat.length - 1]['swipe_id']]?.extra || chat[chat.length - 1].extra || []);
9516 run_swipe_right = true; //then prepare to do normal right swipe to show next message8698 run_swipe_right = true; //then prepare to do normal right swipe to show next message
9517 }8699 }
95188700
@@ -9595,6 +8777,7 @@ export function swipe_right(_event = null, { source, repeated } = {}) {
9595 easing: animation_easing,8777 easing: animation_easing,
9596 queue: false,8778 queue: false,
9597 complete: async function () {8779 complete: async function () {
8780 appendMediaToMessage(chat[chat.length - 1], swipeMessage);
9598 await eventSource.emit(event_types.MESSAGE_SWIPED, (chat.length - 1));8781 await eventSource.emit(event_types.MESSAGE_SWIPED, (chat.length - 1));
9599 if (run_generate && !is_send_press && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) {8782 if (run_generate && !is_send_press && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) {
9600 console.debug('caught here 2');8783 console.debug('caught here 2');
@@ -9640,219 +8823,6 @@ export function swipe_right(_event = null, { source, repeated } = {}) {
9640}8823}
96418824
9642/**8825/**
9643 * @typedef {object} ConnectAPIMap
9644 * @property {string} selected - API name (e.g. "textgenerationwebui", "openai")
9645 * @property {string?} [button] - CSS selector for the API button
9646 * @property {string?} [type] - API type, mostly used by text completion. (e.g. "openrouter")
9647 * @property {string?} [source] - API source, mostly used by chat completion. (e.g. "openai")
9648 */
9649
9650/**
9651 * @type {Record<string, ConnectAPIMap>}
9652 */
9653export const CONNECT_API_MAP = {
9654 // Default APIs not contined inside text gen / chat gen
9655 'kobold': {
9656 selected: 'kobold',
9657 button: '#api_button',
9658 },
9659 'horde': {
9660 selected: 'koboldhorde',
9661 },
9662 'novel': {
9663 selected: 'novel',
9664 button: '#api_button_novel',
9665 },
9666 'koboldcpp': {
9667 selected: 'textgenerationwebui',
9668 button: '#api_button_textgenerationwebui',
9669 type: textgen_types.KOBOLDCPP,
9670 },
9671 // KoboldCpp alias
9672 'kcpp': {
9673 selected: 'textgenerationwebui',
9674 button: '#api_button_textgenerationwebui',
9675 type: textgen_types.KOBOLDCPP,
9676 },
9677 'openai': {
9678 selected: 'openai',
9679 button: '#api_button_openai',
9680 source: chat_completion_sources.OPENAI,
9681 },
9682 // OpenAI alias
9683 'oai': {
9684 selected: 'openai',
9685 button: '#api_button_openai',
9686 source: chat_completion_sources.OPENAI,
9687 },
9688 // Google alias
9689 'google': {
9690 selected: 'openai',
9691 button: '#api_button_openai',
9692 source: chat_completion_sources.MAKERSUITE,
9693 },
9694 // OpenRouter special naming, to differentiate between chat comp and text comp
9695 'openrouter': {
9696 selected: 'openai',
9697 button: '#api_button_openai',
9698 source: chat_completion_sources.OPENROUTER,
9699 },
9700 'openrouter-text': {
9701 selected: 'textgenerationwebui',
9702 button: '#api_button_textgenerationwebui',
9703 type: textgen_types.OPENROUTER,
9704 },
9705};
9706
9707// Collect all unique API names in an array
9708export const UNIQUE_APIS = [...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected))];
9709
9710// Fill connections map from textgen_types and chat_completion_sources
9711for (const textGenType of Object.values(textgen_types)) {
9712 if (CONNECT_API_MAP[textGenType]) continue;
9713 CONNECT_API_MAP[textGenType] = {
9714 selected: 'textgenerationwebui',
9715 button: '#api_button_textgenerationwebui',
9716 type: textGenType,
9717 };
9718}
9719for (const chatCompletionSource of Object.values(chat_completion_sources)) {
9720 if (CONNECT_API_MAP[chatCompletionSource]) continue;
9721 CONNECT_API_MAP[chatCompletionSource] = {
9722 selected: 'openai',
9723 button: '#api_button_openai',
9724 source: chatCompletionSource,
9725 };
9726}
9727
9728async function selectContextCallback(args, name) {
9729 if (!name) {
9730 return power_user.context.preset;
9731 }
9732
9733 const quiet = isTrueBoolean(args?.quiet);
9734 const contextNames = context_presets.map(preset => preset.name);
9735 const fuse = new Fuse(contextNames);
9736 const result = fuse.search(name);
9737
9738 if (result.length === 0) {
9739 !quiet && toastr.warning(t`Context template '${name}' not found`);
9740 return '';
9741 }
9742
9743 const foundName = result[0].item;
9744 selectContextPreset(foundName, { quiet: quiet });
9745 return foundName;
9746}
9747
9748async function selectInstructCallback(args, name) {
9749 if (!name) {
9750 return power_user.instruct.enabled || isTrueBoolean(args?.forceGet) ? power_user.instruct.preset : '';
9751 }
9752
9753 const quiet = isTrueBoolean(args?.quiet);
9754 const instructNames = instruct_presets.map(preset => preset.name);
9755 const fuse = new Fuse(instructNames);
9756 const result = fuse.search(name);
9757
9758 if (result.length === 0) {
9759 !quiet && toastr.warning(t`Instruct template '${name}' not found`);
9760 return '';
9761 }
9762
9763 const foundName = result[0].item;
9764 selectInstructPreset(foundName, { quiet: quiet });
9765 return foundName;
9766}
9767
9768async function enableInstructCallback() {
9769 $('#instruct_enabled').prop('checked', true).trigger('input').trigger('change');
9770 return '';
9771}
9772
9773async function disableInstructCallback() {
9774 $('#instruct_enabled').prop('checked', false).trigger('input').trigger('change');
9775 return '';
9776}
9777
9778/**
9779 * @param {string} text API name
9780 */
9781async function connectAPISlash(args, text) {
9782 if (!text.trim()) {
9783 for (const [key, config] of Object.entries(CONNECT_API_MAP)) {
9784 if (config.selected !== main_api) continue;
9785
9786 if (config.source) {
9787 if (oai_settings.chat_completion_source === config.source) {
9788 return key;
9789 } else {
9790 continue;
9791 }
9792 }
9793
9794 if (config.type) {
9795 if (textgen_settings.type === config.type) {
9796 return key;
9797 } else {
9798 continue;
9799 }
9800 }
9801
9802 return key;
9803 }
9804
9805 console.error('FIXME: The current API is not in the API map');
9806 return '';
9807 }
9808
9809 const apiConfig = CONNECT_API_MAP[text.toLowerCase()];
9810 if (!apiConfig) {
9811 toastr.error(t`Error: ${text} is not a valid API`);
9812 return '';
9813 }
9814
9815 let connectionRequired = false;
9816
9817 if (main_api !== apiConfig.selected) {
9818 $(`#main_api option[value='${apiConfig.selected || text}']`).prop('selected', true);
9819 $('#main_api').trigger('change');
9820 connectionRequired = true;
9821 }
9822
9823 if (apiConfig.source && oai_settings.chat_completion_source !== apiConfig.source) {
9824 $(`#chat_completion_source option[value='${apiConfig.source}']`).prop('selected', true);
9825 $('#chat_completion_source').trigger('change');
9826 connectionRequired = true;
9827 }
9828
9829 if (apiConfig.type && textgen_settings.type !== apiConfig.type) {
9830 $(`#textgen_type option[value='${apiConfig.type}']`).prop('selected', true);
9831 $('#textgen_type').trigger('change');
9832 connectionRequired = true;
9833 }
9834
9835 if (connectionRequired && apiConfig.button) {
9836 $(apiConfig.button).trigger('click');
9837 }
9838
9839 const quiet = isTrueBoolean(args?.quiet);
9840 const toast = quiet ? jQuery() : toastr.info(t`API set to ${text}, trying to connect..`);
9841
9842 try {
9843 if (connectionRequired) {
9844 await waitUntilCondition(() => online_status !== 'no_connection', 5000, 100);
9845 }
9846 console.log('Connection successful');
9847 } catch {
9848 console.log('Could not connect after 5 seconds, skipping.');
9849 }
9850
9851 toastr.clear(toast);
9852 return text;
9853}
9854
9855/**
9856 * Imports supported files dropped into the app window.8826 * Imports supported files dropped into the app window.
9857 * @param {File[]} files Array of files to process8827 * @param {File[]} files Array of files to process
9858 * @param {Map<File, string>} [data] Extra data to pass to the import function8828 * @param {Map<File, string>} [data] Extra data to pass to the import function
@@ -9870,6 +8840,7 @@ export async function processDroppedFiles(files, data = new Map()) {
98708840
9871 const allowedExtensions = [8841 const allowedExtensions = [
9872 'charx',8842 'charx',
8843 'byaf',
9873 ];8844 ];
98748845
9875 const avatarFileNames = [];8846 const avatarFileNames = [];
@@ -9933,7 +8904,7 @@ async function importCharacter(file, { preserveFileName = '', importTags = false
9933 }8904 }
99348905
9935 const ext = file.name.match(/\.(\w+)$/);8906 const ext = file.name.match(/\.(\w+)$/);
9936 if (!ext || !(['json', 'png', 'yaml', 'yml', 'charx'].includes(ext[1].toLowerCase()))) {8907 if (!ext || !(['json', 'png', 'yaml', 'yml', 'charx', 'byaf'].includes(ext[1].toLowerCase()))) {
9937 return;8908 return;
9938 }8909 }
99398910
@@ -9944,32 +8915,38 @@ async function importCharacter(file, { preserveFileName = '', importTags = false
9944 formData.append('file_type', format);8915 formData.append('file_type', format);
9945 if (preserveFileName) formData.append('preserved_name', preserveFileName);8916 if (preserveFileName) formData.append('preserved_name', preserveFileName);
99468917
9947 const data = await jQuery.ajax({8918 try {
9948 type: 'POST',8919 const result = await fetch('/api/characters/import', {
9949 url: '/api/characters/import',8920 method: 'POST',
9950 data: formData,8921 body: formData,
9951 async: true,8922 headers: getRequestHeaders({ omitContentType: true }),
9952 cache: false,8923 cache: 'no-cache',
9953 contentType: false,8924 });
9954 processData: false,
9955 });
99568925
9957 if (data.error) {8926 if (!result.ok) {
9958 toastr.error(t`The file is likely invalid or corrupted.`, t`Could not import character`);8927 throw new Error(`Failed to import character: ${result.statusText}`);
9959 return;8928 }
9960 }
99618929
9962 if (data.file_name !== undefined) {8930 const data = await result.json();
9963 $('#character_search_bar').val('').trigger('input');8931
8932 if (data.error) {
8933 throw new Error(`Server returned an error: ${data.error}`);
8934 }
99648935
9965 toastr.success(t`Character Created: ${String(data.file_name).replace('.png', '')}`);8936 if (data.file_name !== undefined) {
9966 let avatarFileName = `${data.file_name}.png`;8937 $('#character_search_bar').val('').trigger('input');
9967 if (importTags) {
9968 await importCharactersTags([avatarFileName]);
99698938
9970 selectImportedChar(data.file_name);8939 toastr.success(t`Character Created: ${String(data.file_name).replace('.png', '')}`);
8940 let avatarFileName = `${data.file_name}.png`;
8941 if (importTags) {
8942 await importCharactersTags([avatarFileName]);
8943 selectImportedChar(data.file_name);
8944 }
8945 return avatarFileName;
9971 }8946 }
9972 return avatarFileName;8947 } catch (error) {
8948 console.error('Error importing character', error);
8949 toastr.error(t`The file is likely invalid or corrupted.`, t`Could not import character`);
9973 }8950 }
9974}8951}
99758952
@@ -9995,32 +8972,6 @@ async function importFromURL(items, files) {
9995 }8972 }
9996}8973}
99978974
9998async function doImpersonate(args, prompt) {
9999 const options = prompt?.trim() ? { quiet_prompt: prompt.trim(), quietToLoud: true } : {};
10000 const shouldAwait = isTrueBoolean(args?.await);
10001 const outerPromise = new Promise((outerResolve) => setTimeout(async () => {
10002 try {
10003 await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
10004 } catch {
10005 console.warn('Timeout waiting for generation unlock');
10006 toastr.warning(t`Cannot run /impersonate command while the reply is being generated.`);
10007 return '';
10008 }
10009
10010 // Prevent generate recursion
10011 $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
10012
10013 outerResolve(new Promise(innerResolve => setTimeout(() => innerResolve(Generate('impersonate', options)), 1)));
10014 }, 1));
10015
10016 if (shouldAwait) {
10017 const innerPromise = await outerPromise;
10018 await innerPromise;
10019 }
10020
10021 return '';
10022}
10023
10024export async function doNewChat({ deleteCurrentChat = false } = {}) {8975export async function doNewChat({ deleteCurrentChat = false } = {}) {
10025 //Make a new chat for selected character8976 //Make a new chat for selected character
10026 if ((!selected_group && this_chid == undefined) || menu_type == 'create') {8977 if ((!selected_group && this_chid == undefined) || menu_type == 'create') {
@@ -10055,33 +9006,6 @@ export async function doNewChat({ deleteCurrentChat = false } = {}) {
100559006
10056}9007}
100579008
10058async function doDeleteChat() {
10059 await displayPastChats();
10060 let currentChatDeleteButton = $('.select_chat_block[highlight=\'true\']').parent().find('.PastChat_cross');
10061 $(currentChatDeleteButton).trigger('click');
10062 await delay(1);
10063 $('#dialogue_popup_ok').trigger('click', { fromSlashCommand: true });
10064 return '';
10065}
10066
10067async function doRenameChat(_, chatName) {
10068 if (!chatName) {
10069 toastr.warning(t`Name must be provided as an argument to rename this chat.`);
10070 return '';
10071 }
10072
10073 const currentChatName = getCurrentChatId();
10074 if (!currentChatName) {
10075 toastr.warning(t`No chat selected that can be renamed.`);
10076 return '';
10077 }
10078
10079 await renameChat(currentChatName, chatName);
10080
10081 toastr.success(t`Successfully renamed chat to: ${chatName}`);
10082 return '';
10083}
10084
10085/**9009/**
10086 * Renames a group or character chat.9010 * Renames a group or character chat.
10087 * @param {object} param Parameters for renaming chat9011 * @param {object} param Parameters for renaming chat
@@ -10195,12 +9119,6 @@ export async function updateRemoteChatName(characterId, newName) {
10195 }9119 }
10196}9120}
101979121
10198/**
10199 * /getchatname` slash command
10200 */
10201async function doGetChatName() {
10202 return getCurrentChatDetails().sessionName;
10203}
102049122
10205function doCharListDisplaySwitch() {9123function doCharListDisplaySwitch() {
10206 power_user.charListGrid = !power_user.charListGrid;9124 power_user.charListGrid = !power_user.charListGrid;
@@ -10208,11 +9126,6 @@ function doCharListDisplaySwitch() {
10208 saveSettingsDebounced();9126 saveSettingsDebounced();
10209}9127}
102109128
10211function doCloseChat() {
10212 $('#option_close_chat').trigger('click');
10213 return '';
10214}
10215
10216/**9129/**
10217 * Function to handle the deletion of a character, given a specific popup type and character ID.9130 * Function to handle the deletion of a character, given a specific popup type and character ID.
10218 * If popup type equals "del_ch", it will proceed with deletion otherwise it will exit the function.9131 * If popup type equals "del_ch", it will proceed with deletion otherwise it will exit the function.
@@ -10323,11 +9236,6 @@ export async function newAssistantChat({ temporary = false } = {}) {
10323 sendSystemMessage(system_message_types.ASSISTANT_NOTE);9236 sendSystemMessage(system_message_types.ASSISTANT_NOTE);
10324}9237}
103259238
10326function doTogglePanels() {
10327 $('#option_settings').trigger('click');
10328 return '';
10329}
10330
10331/**9239/**
10332 * Event handler to open a navbar drawer when a drawer open button is clicked.9240 * Event handler to open a navbar drawer when a drawer open button is clicked.
10333 * Handles click events on .drawer-opener elements.9241 * Handles click events on .drawer-opener elements.
@@ -10363,7 +9271,7 @@ export async function doNavbarIconClick() {
10363 for (const el of $openDrawers) {9271 for (const el of $openDrawers) {
10364 $(el).toggleClass('closedDrawer openDrawer');9272 $(el).toggleClass('closedDrawer openDrawer');
10365 }9273 }
10366 if ($openDrawers.length) {9274 if ($openDrawers.length && animation_duration) {
10367 await delay(animation_duration);9275 await delay(animation_duration);
10368 }9276 }
10369 icon.toggleClass('openIcon closedIcon');9277 icon.toggleClass('openIcon closedIcon');
@@ -10421,18 +9329,9 @@ function addDebugFunctions() {
10421 registerDebugFunction('generationTest', 'Send a generation request', 'Generates text using the currently selected API.', async () => {9329 registerDebugFunction('generationTest', 'Send a generation request', 'Generates text using the currently selected API.', async () => {
10422 const text = prompt('Input text:', 'Hello');9330 const text = prompt('Input text:', 'Hello');
10423 toastr.info('Working on it...');9331 toastr.info('Working on it...');
10424 const message = await generateRaw(text, null, false, false);9332 const message = await generateRaw({ prompt: text });
10425 alert(message);9333 alert(message);
10426 });9334 });
10427
10428 registerDebugFunction('clearPrompts', 'Delete itemized prompts', 'Deletes all itemized prompts from the local storage.', async () => {
10429 await clearItemizedPrompts();
10430 toastr.info('Itemized prompts deleted.');
10431 if (getCurrentChatId()) {
10432 await reloadCurrentChat();
10433 }
10434 });
10435
10436 registerDebugFunction('toggleEventTracing', 'Toggle event tracing', 'Useful to see what triggered a certain event.', () => {9335 registerDebugFunction('toggleEventTracing', 'Toggle event tracing', 'Useful to see what triggered a certain event.', () => {
10437 localStorage.setItem('eventTracing', localStorage.getItem('eventTracing') === 'true' ? 'false' : 'true');9336 localStorage.setItem('eventTracing', localStorage.getItem('eventTracing') === 'true' ? 'false' : 'true');
10438 toastr.info('Event tracing is now ' + (localStorage.getItem('eventTracing') === 'true' ? 'enabled' : 'disabled'));9337 toastr.info('Event tracing is now ' + (localStorage.getItem('eventTracing') === 'true' ? 'enabled' : 'disabled'));
@@ -10471,252 +9370,41 @@ API Settings: ${JSON.stringify(getSettingsContents[getSettingsContents.main_api
10471 });9370 });
10472}9371}
104739372
9373function initCharacterSearch() {
9374 const debouncedCharacterSearch = debounce((searchQuery) => {
9375 entitiesFilter.setFilterData(FILTER_TYPES.SEARCH, searchQuery);
9376 });
104749377
10475// MARK: DOM Handlers Start9378 const searchForm = $('#form_character_search_form');
10476jQuery(async function () {9379 const searchInput = $('#character_search_bar');
10477 async function doForceSave() {9380 const searchButton = $('#rm_button_search');
10478 await saveSettings();
10479 await saveChatConditional();
10480 toastr.success('Chat and settings saved.');
10481 return '';
10482 }
104839381
10484 SlashCommandParser.addCommandObject(SlashCommand.fromProps({9382 const storageKey = 'characterSearchFormVisible';
10485 name: 'dupe',
10486 callback: duplicateCharacter,
10487 helpString: 'Duplicates the currently selected character.',
10488 }));
10489 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
10490 name: 'api',
10491 callback: connectAPISlash,
10492 returns: 'the current API',
10493 namedArgumentList: [
10494 SlashCommandNamedArgument.fromProps({
10495 name: 'quiet',
10496 description: 'Suppress the toast message on connection',
10497 typeList: [ARGUMENT_TYPE.BOOLEAN],
10498 defaultValue: 'false',
10499 enumList: commonEnumProviders.boolean('trueFalse')(),
10500 }),
10501 ],
10502 unnamedArgumentList: [
10503 SlashCommandArgument.fromProps({
10504 description: 'API to connect to',
10505 typeList: [ARGUMENT_TYPE.STRING],
10506 enumList: Object.entries(CONNECT_API_MAP).map(([api, { selected }]) =>
10507 new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === selected)),
10508 selected[0].toUpperCase() ?? enumIcons.default)),
10509 }),
10510 ],
10511 helpString: `
10512 <div>
10513 Connect to an API. If no argument is provided, it will return the currently connected API.
10514 </div>
10515 <div>
10516 <strong>Available APIs:</strong>
10517 <pre><code>${Object.keys(CONNECT_API_MAP).join(', ')}</code></pre>
10518 </div>
10519 `,
10520 }));
10521 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
10522 name: 'impersonate',
10523 callback: doImpersonate,
10524 aliases: ['imp'],
10525 namedArgumentList: [
10526 new SlashCommandNamedArgument(
10527 'await',
10528 'Whether to await for the triggered generation before continuing',
10529 [ARGUMENT_TYPE.BOOLEAN],
10530 false,
10531 false,
10532 'false',
10533 ),
10534 ],
10535 unnamedArgumentList: [
10536 new SlashCommandArgument(
10537 'prompt', [ARGUMENT_TYPE.STRING], false,
10538 ),
10539 ],
10540 helpString: `
10541 <div>
10542 Calls an impersonation response, with an optional additional prompt.
10543 </div>
10544 <div>
10545 If <code>await=true</code> named argument is passed, the command will wait for the impersonation to end before continuing.
10546 </div>
10547 <div>
10548 <strong>Example:</strong>
10549 <ul>
10550 <li>
10551 <pre><code class="language-stscript">/impersonate What is the meaning of life?</code></pre>
10552 </li>
10553 </ul>
10554 </div>
10555 `,
10556 }));
10557 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
10558 name: 'delchat',
10559 callback: doDeleteChat,
10560 helpString: 'Deletes the current chat.',
10561 }));
10562 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
10563 name: 'renamechat',
10564 callback: doRenameChat,
10565 unnamedArgumentList: [
10566 new SlashCommandArgument(
10567 'new chat name', [ARGUMENT_TYPE.STRING], true,
10568 ),
10569 ],
10570 helpString: 'Renames the current chat.',
10571 }));
10572 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
10573 name: 'getchatname',
10574 callback: doGetChatName,
10575 returns: 'chat file name',
10576 helpString: 'Returns the name of the current chat file into the pipe.',
10577 }));
10578 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
10579 name: 'closechat',
10580 callback: doCloseChat,
10581 helpString: 'Closes the current chat.',
10582 }));
10583 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
10584 name: 'tempchat',
10585 callback: () => {
10586 return new Promise((resolve, reject) => {
10587 const eventCallback = async (chatId) => {
10588 if (chatId) {
10589 return reject('Not in a temporary chat');
10590 }
10591 await newAssistantChat({ temporary: true });
10592 return resolve('');
10593 };
10594 eventSource.once(event_types.CHAT_CHANGED, eventCallback);
10595 doCloseChat();
10596 setTimeout(() => {
10597 reject('Failed to open temporary chat');
10598 eventSource.removeListener(event_types.CHAT_CHANGED, eventCallback);
10599 }, debounce_timeout.relaxed);
10600 });
10601 },
10602 helpString: 'Opens a temporary chat with Assistant.',
10603 }));
10604 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
10605 name: 'panels',
10606 callback: doTogglePanels,
10607 aliases: ['togglepanels'],
10608 helpString: 'Toggle UI panels on/off',
10609 }));
10610 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
10611 name: 'forcesave',
10612 callback: doForceSave,
10613 helpString: 'Forces a save of the current chat and settings',
10614 }));
10615 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
10616 name: 'instruct',
10617 callback: selectInstructCallback,
10618 returns: 'current template',
10619 namedArgumentList: [
10620 SlashCommandNamedArgument.fromProps({
10621 name: 'quiet',
10622 description: 'Suppress the toast message on template change',
10623 typeList: [ARGUMENT_TYPE.BOOLEAN],
10624 defaultValue: 'false',
10625 enumList: commonEnumProviders.boolean('trueFalse')(),
10626 }),
10627 SlashCommandNamedArgument.fromProps({
10628 name: 'forceGet',
10629 description: 'Force getting a name even if instruct mode is disabled',
10630 typeList: [ARGUMENT_TYPE.BOOLEAN],
10631 defaultValue: 'false',
10632 enumList: commonEnumProviders.boolean('trueFalse')(),
10633 }),
10634 ],
10635 unnamedArgumentList: [
10636 SlashCommandArgument.fromProps({
10637 description: 'instruct template name',
10638 typeList: [ARGUMENT_TYPE.STRING],
10639 enumProvider: () => instruct_presets.map(preset => new SlashCommandEnumValue(preset.name, null, enumTypes.enum, enumIcons.preset)),
10640 }),
10641 ],
10642 helpString: `
10643 <div>
10644 Selects instruct mode template by name. Enables instruct mode if not already enabled.
10645 Gets the current instruct template if no name is provided and instruct mode is enabled or <code>forceGet=true</code> is passed.
10646 </div>
10647 <div>
10648 <strong>Example:</strong>
10649 <ul>
10650 <li>
10651 <pre><code class="language-stscript">/instruct creative</code></pre>
10652 </li>
10653 </ul>
10654 </div>
10655 `,
10656 }));
10657 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
10658 name: 'instruct-on',
10659 callback: enableInstructCallback,
10660 helpString: 'Enables instruct mode.',
10661 }));
10662 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
10663 name: 'instruct-off',
10664 callback: disableInstructCallback,
10665 helpString: 'Disables instruct mode',
10666 }));
10667 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
10668 name: 'instruct-state',
10669 aliases: ['instruct-toggle'],
10670 helpString: 'Gets the current instruct mode state. If an argument is provided, it will set the instruct mode state.',
10671 unnamedArgumentList: [
10672 SlashCommandArgument.fromProps({
10673 description: 'instruct mode state',
10674 typeList: [ARGUMENT_TYPE.BOOLEAN],
10675 enumList: commonEnumProviders.boolean('trueFalse')(),
10676 }),
10677 ],
10678 callback: async (_args, state) => {
10679 if (!state || typeof state !== 'string') {
10680 return String(power_user.instruct.enabled);
10681 }
106829383
10683 const newState = isTrueBoolean(state);9384 searchInput.on('input', function () {
10684 newState ? enableInstructCallback() : disableInstructCallback();9385 const searchQuery = String($(this).val());
10685 return String(power_user.instruct.enabled);9386 debouncedCharacterSearch(searchQuery);
10686 },9387 });
10687 }));
10688 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
10689 name: 'context',
10690 callback: selectContextCallback,
10691 returns: 'template name',
10692 namedArgumentList: [
10693 SlashCommandNamedArgument.fromProps({
10694 name: 'quiet',
10695 description: 'Suppress the toast message on template change',
10696 typeList: [ARGUMENT_TYPE.BOOLEAN],
10697 defaultValue: 'false',
10698 enumList: commonEnumProviders.boolean('trueFalse')(),
10699 }),
10700 ],
10701 unnamedArgumentList: [
10702 SlashCommandArgument.fromProps({
10703 description: 'context template name',
10704 typeList: [ARGUMENT_TYPE.STRING],
10705 enumProvider: () => context_presets.map(preset => new SlashCommandEnumValue(preset.name, null, enumTypes.enum, enumIcons.preset)),
10706 }),
10707 ],
10708 helpString: 'Selects context template by name. Gets the current template if no name is provided',
10709 }));
10710 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
10711 name: 'chat-manager',
10712 callback: () => {
10713 $('#option_select_chat').trigger('click');
10714 return '';
10715 },
10716 aliases: ['chat-history', 'manage-chats'],
10717 helpString: 'Opens the chat manager for the current character/group.',
10718 }));
107199388
9389 searchButton.on('click', function () {
9390 const newVisibility = !searchForm.is(':visible');
9391 searchForm.toggle(newVisibility);
9392 searchButton.toggleClass('active', newVisibility);
9393 accountStorage.setItem(storageKey, String(newVisibility));
9394 if (newVisibility) {
9395 searchInput.trigger('focus');
9396 }
9397 });
9398
9399 eventSource.on(event_types.APP_READY, () => {
9400 const isVisible = accountStorage.getItem(storageKey) === 'true';
9401 searchForm.toggle(isVisible);
9402 searchButton.toggleClass('active', isVisible);
9403 });
9404}
9405
9406// MARK: DOM Handlers Start
9407jQuery(async function () {
10720 setTimeout(function () {9408 setTimeout(function () {
10721 $('#groupControlsToggle').trigger('click');9409 $('#groupControlsToggle').trigger('click');
10722 $('#groupCurrentMemberListToggle .inline-drawer-icon').trigger('click');9410 $('#groupCurrentMemberListToggle .inline-drawer-icon').trigger('click');
@@ -10731,10 +9419,10 @@ jQuery(async function () {
10731 });9419 });
10732 $('#send_but, #option_regenerate, #option_continue, #mes_continue, #mes_impersonate').on('click', () => {9420 $('#send_but, #option_regenerate, #option_continue, #mes_continue, #mes_impersonate').on('click', () => {
10733 if (S_TAPreviouslyFocused) {9421 if (S_TAPreviouslyFocused) {
10734 $('#send_textarea').focus();9422 $('#send_textarea').trigger('focus');
10735 }9423 }
10736 });9424 });
10737 $(document).click(event => {9425 $(document).on('click', event => {
10738 if ($(':focus').attr('id') !== 'send_textarea') {9426 if ($(':focus').attr('id') !== 'send_textarea') {
10739 var validIDs = ['options_button', 'send_but', 'mes_impersonate', 'mes_continue', 'send_textarea', 'option_regenerate', 'option_continue'];9427 var validIDs = ['options_button', 'send_but', 'mes_impersonate', 'mes_continue', 'send_textarea', 'option_regenerate', 'option_continue'];
10740 if (!validIDs.includes($(event.target).attr('id'))) {9428 if (!validIDs.includes($(event.target).attr('id'))) {
@@ -10747,7 +9435,7 @@ jQuery(async function () {
107479435
10748 /////////////////9436 /////////////////
107499437
10750 $('#swipes-checkbox').change(function () {9438 $('#swipes-checkbox').on('change', function () {
10751 swipes = !!$('#swipes-checkbox').prop('checked');9439 swipes = !!$('#swipes-checkbox').prop('checked');
10752 if (swipes) {9440 if (swipes) {
10753 //console.log('toggle change calling showswipebtns');9441 //console.log('toggle change calling showswipebtns');
@@ -10764,13 +9452,7 @@ jQuery(async function () {
10764 $(document).on('click', '.last_mes .swipe_right', swipe_right);9452 $(document).on('click', '.last_mes .swipe_right', swipe_right);
10765 $(document).on('click', '.last_mes .swipe_left', swipe_left);9453 $(document).on('click', '.last_mes .swipe_left', swipe_left);
107669454
10767 const debouncedCharacterSearch = debounce((searchQuery) => {9455 initCharacterSearch();
10768 entitiesFilter.setFilterData(FILTER_TYPES.SEARCH, searchQuery);
10769 });
10770 $('#character_search_bar').on('input', function () {
10771 const searchQuery = String($(this).val());
10772 debouncedCharacterSearch(searchQuery);
10773 });
107749456
10775 $('#mes_impersonate').on('click', function () {9457 $('#mes_impersonate').on('click', function () {
10776 $('#option_impersonate').trigger('click');9458 $('#option_impersonate').trigger('click');
@@ -10890,11 +9572,51 @@ jQuery(async function () {
10890 }9572 }
10891 });9573 });
108929574
10893 $(document).on('click', '.PastChat_cross', function (e) {9575 /**
9576 * Handles the deletion of a chat file, including group chats.
9577 *
9578 * @param {string} chatFile - The name of the chat file to delete.
9579 * @param {object} group - The group object if the chat is part of a group.
9580 * @param {boolean} [fromSlashCommand=false] - Whether the deletion was triggered from a slash command.
9581 * @returns {Promise<void>}
9582 */
9583 async function handleDeleteChat(chatFile, group, fromSlashCommand = false) {
9584 // Close past chat popup.
9585 $('#select_chat_cross').trigger('click');
9586 showLoader();
9587 if (group) {
9588 await deleteGroupChat(group, chatFile);
9589 } else {
9590 await delChat(chatFile);
9591 }
9592
9593 if (fromSlashCommand) { // When called from `/delchat` command, don't re-open the history view.
9594 $('#options').hide(); // Hide option popup menu.
9595 hideLoader();
9596 } else { // Open the history view again after 2 seconds (delay to avoid edge cases for deleting last chat).
9597 setTimeout(function () {
9598 $('#option_select_chat').trigger('click');
9599 $('#options').hide(); // Hide option popup menu.
9600 hideLoader();
9601 }, 2000);
9602 }
9603 }
9604
9605 $(document).on('click', '.PastChat_cross', async function (e, { fromSlashCommand = false } = {}) {
10894 e.stopPropagation();9606 e.stopPropagation();
10895 chat_file_for_del = $(this).attr('file_name');9607 chat_file_for_del = $(this).attr('file_name');
10896 console.debug('detected cross click for' + chat_file_for_del);9608 console.debug('detected cross click for' + chat_file_for_del);
10897 callPopup('<h3>' + t`Delete the Chat File?` + '</h3>', 'del_chat');9609
9610 // Skip confirmation if called from a slash command.
9611 if (fromSlashCommand) {
9612 await handleDeleteChat(chat_file_for_del, selected_group, true);
9613 return;
9614 }
9615
9616 const result = await callGenericPopup('<h3>' + t`Delete the Chat File?` + '</h3>', POPUP_TYPE.CONFIRM);
9617 if (result === POPUP_RESULT.AFFIRMATIVE) {
9618 await handleDeleteChat(chat_file_for_del, selected_group, false);
9619 }
10898 });9620 });
108999621
10900 $('#advanced_div').on('click', function () {9622 $('#advanced_div').on('click', function () {
@@ -10943,25 +9665,7 @@ jQuery(async function () {
10943 }, animation_duration);9665 }, animation_duration);
109449666
10945 if (popup_type == 'del_chat') {9667 if (popup_type == 'del_chat') {
10946 //close past chat popup9668 await handleDeleteChat(chat_file_for_del, selected_group, fromSlashCommand);
10947 $('#select_chat_cross').trigger('click');
10948 showLoader();
10949 if (selected_group) {
10950 await deleteGroupChat(selected_group, chat_file_for_del);
10951 } else {
10952 await delChat(chat_file_for_del);
10953 }
10954
10955 if (fromSlashCommand) { // When called from `/delchat` command, don't re-open the history view.
10956 $('#options').hide(); // hide option popup menu
10957 hideLoader();
10958 } else { // Open the history view again after 2 seconds (delay to avoid edge cases for deleting last chat).
10959 setTimeout(function () {
10960 $('#option_select_chat').trigger('click');
10961 $('#options').hide(); // hide option popup menu
10962 hideLoader();
10963 }, 2000);
10964 }
10965 }9669 }
109669670
10967 if (dialogueResolve) {9671 if (dialogueResolve) {
@@ -11133,76 +9837,6 @@ jQuery(async function () {
11133 }9837 }
11134 });9838 });
111359839
11136 ///////////////////////////////////////////////////////////////////////////////////
11137
11138 $('#api_button').on('click', function (e) {
11139 if ($('#api_url_text').val() != '') {
11140 let value = formatKoboldUrl(String($('#api_url_text').val()).trim());
11141
11142 if (!value) {
11143 toastr.error('Please enter a valid URL.');
11144 return;
11145 }
11146
11147 $('#api_url_text').val(value);
11148 api_server = value;
11149 startStatusLoading();
11150
11151 main_api = 'kobold';
11152 saveSettingsDebounced();
11153 getStatusKobold();
11154 }
11155 });
11156
11157 $('#api_button_textgenerationwebui').on('click', async function (e) {
11158 const keys = [
11159 { id: 'api_key_mancer', secret: SECRET_KEYS.MANCER },
11160 { id: 'api_key_vllm', secret: SECRET_KEYS.VLLM },
11161 { id: 'api_key_aphrodite', secret: SECRET_KEYS.APHRODITE },
11162 { id: 'api_key_tabby', secret: SECRET_KEYS.TABBY },
11163 { id: 'api_key_togetherai', secret: SECRET_KEYS.TOGETHERAI },
11164 { id: 'api_key_ooba', secret: SECRET_KEYS.OOBA },
11165 { id: 'api_key_infermaticai', secret: SECRET_KEYS.INFERMATICAI },
11166 { id: 'api_key_dreamgen', secret: SECRET_KEYS.DREAMGEN },
11167 { id: 'api_key_openrouter-tg', secret: SECRET_KEYS.OPENROUTER },
11168 { id: 'api_key_koboldcpp', secret: SECRET_KEYS.KOBOLDCPP },
11169 { id: 'api_key_llamacpp', secret: SECRET_KEYS.LLAMACPP },
11170 { id: 'api_key_featherless', secret: SECRET_KEYS.FEATHERLESS },
11171 { id: 'api_key_huggingface', secret: SECRET_KEYS.HUGGINGFACE },
11172 { id: 'api_key_generic', secret: SECRET_KEYS.GENERIC },
11173 ];
11174
11175 for (const key of keys) {
11176 const keyValue = String($(`#${key.id}`).val()).trim();
11177 if (keyValue.length) {
11178 await writeSecret(key.secret, keyValue);
11179 }
11180 }
11181
11182 validateTextGenUrl();
11183 startStatusLoading();
11184 main_api = 'textgenerationwebui';
11185 saveSettingsDebounced();
11186 getStatusTextgen();
11187 });
11188
11189 $('#api_button_novel').on('click', async function (e) {
11190 e.stopPropagation();
11191 const api_key_novel = String($('#api_key_novel').val()).trim();
11192
11193 if (api_key_novel.length) {
11194 await writeSecret(SECRET_KEYS.NOVEL, api_key_novel);
11195 }
11196
11197 if (!secret_state[SECRET_KEYS.NOVEL]) {
11198 console.log('No secret key saved for NovelAI');
11199 return;
11200 }
11201
11202 startStatusLoading();
11203 // Check near immediately rather than waiting for up to 90s
11204 await getStatusNovel();
11205 });
112069840
11207 const button = $('#options_button');9841 const button = $('#options_button');
11208 const menu = $('#options');9842 const menu = $('#options');
@@ -11285,7 +9919,8 @@ jQuery(async function () {
11285 await doNewChat({ deleteCurrentChat: deleteCurrentChat });9919 await doNewChat({ deleteCurrentChat: deleteCurrentChat });
11286 }9920 }
11287 if (!selected_group && this_chid === undefined && !is_send_press) {9921 if (!selected_group && this_chid === undefined && !is_send_press) {
11288 await newAssistantChat({ temporary: true });9922 const alreadyInTempChat = this_chid === undefined && name2 === neutralCharacterName;
9923 await newAssistantChat({ temporary: alreadyInTempChat });
11289 }9924 }
11290 }9925 }
112919926
@@ -11424,48 +10059,11 @@ jQuery(async function () {
11424 is_delete_mode = false;10059 is_delete_mode = false;
11425 });10060 });
1142610061
11427 $('#settings_preset').change(function () {10062 $('#main_api').on('change', async function () {
11428 if ($('#settings_preset').find(':selected').val() != 'gui') {
11429 preset_settings = $('#settings_preset').find(':selected').text();
11430 const preset = koboldai_settings[koboldai_setting_names[preset_settings]];
11431 loadKoboldSettings(preset);
11432 setGenerationParamsFromPreset(preset);
11433 $('#kobold_api-settings').find('input').prop('disabled', false);
11434 $('#kobold_api-settings').css('opacity', 1.0);
11435 $('#kobold_order')
11436 .css('opacity', 1)
11437 .sortable('enable');
11438 } else {
11439 //$('.button').disableSelection();
11440 preset_settings = 'gui';
11441
11442 $('#kobold_api-settings').find('input').prop('disabled', true);
11443 $('#kobold_api-settings').css('opacity', 0.5);
11444
11445 $('#kobold_order')
11446 .css('opacity', 0.5)
11447 .sortable('disable');
11448 }
11449 saveSettingsDebounced();
11450 });
11451
11452 $('#settings_preset_novel').change(function () {
11453 nai_settings.preset_settings_novel = $('#settings_preset_novel')
11454 .find(':selected')
11455 .text();
11456
11457 const preset = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]];
11458 loadNovelPreset(preset);
11459 amount_gen = Number($('#amount_gen').val());
11460 max_context = Number($('#max_context').val());
11461
11462 saveSettingsDebounced();
11463 });
11464
11465 $('#main_api').change(function () {
11466 cancelStatusCheck('Canceled because main api changed');10063 cancelStatusCheck('Canceled because main api changed');
11467 changeMainAPI();10064 changeMainAPI();
11468 saveSettingsDebounced();10065 saveSettingsDebounced();
10066 await eventSource.emit(event_types.MAIN_API_CHANGED, { apiId: main_api });
11469 });10067 });
1147010068
11471 ////////////////// OPTIMIZED RANGE SLIDER LISTENERS////////////////10069 ////////////////// OPTIMIZED RANGE SLIDER LISTENERS////////////////
@@ -11542,17 +10140,12 @@ jQuery(async function () {
11542 }10140 }
11543 });10141 });
1154410142
11545 $(document).on('pointerup', '.mes_prompt', async function () {
11546 let mesIdForItemization = $(this).closest('.mes').attr('mesId');
11547 console.log(`looking for mesID: ${mesIdForItemization}`);
11548 if (itemizedPrompts.length !== undefined && itemizedPrompts.length !== 0) {
11549 await promptItemize(itemizedPrompts, mesIdForItemization);
11550 }
11551 });
11552
11553 //********************10143 //********************
11554 //***Message Editor***10144 //***Message Editor***
11555 $(document).on('click', '.mes_edit', async function () {10145 $(document).on('click', '.mes_edit', async function () {
10146 if (is_delete_mode) {
10147 return;
10148 }
11556 if (this_chid !== undefined || selected_group || name2 === neutralCharacterName) {10149 if (this_chid !== undefined || selected_group || name2 === neutralCharacterName) {
11557 // Previously system messages we're allowed to be edited10150 // Previously system messages we're allowed to be edited
11558 /*const message = $(this).closest(".mes");10151 /*const message = $(this).closest(".mes");
@@ -11896,6 +10489,9 @@ jQuery(async function () {
11896 await importCharactersTags(avatarFileNames);10489 await importCharactersTags(avatarFileNames);
11897 selectImportedChar(avatarFileNames[avatarFileNames.length - 1]);10490 selectImportedChar(avatarFileNames[avatarFileNames.length - 1]);
11898 }10491 }
10492
10493 // Clear the file input value to allow re-uploading the same file
10494 e.target.value = '';
11899 });10495 });
1190010496
11901 $('#export_button').on('click', function () {10497 $('#export_button').on('click', function () {
@@ -11990,7 +10586,7 @@ jQuery(async function () {
11990 select_rm_characters();10586 select_rm_characters();
11991 });10587 });
1199210588
11993 $('#dupe_button').click(async function () {10589 $('#dupe_button').on('click', async function () {
11994 await duplicateCharacter();10590 await duplicateCharacter();
11995 });10591 });
1199610592
@@ -12126,9 +10722,17 @@ jQuery(async function () {
12126 $('body').append(newElement);10722 $('body').append(newElement);
12127 newElement.fadeIn(animation_duration);10723 newElement.fadeIn(animation_duration);
12128 const zoomedAvatarImgElement = $(`.zoomed_avatar[forChar="${charname}"] img`);10724 const zoomedAvatarImgElement = $(`.zoomed_avatar[forChar="${charname}"] img`);
12129 if (messageElement.attr('is_user') == 'true' || (messageElement.attr('is_system') == 'true' && !isValidCharacter)) { //handle user and system avatars10725 if (messageElement.attr('is_user') == 'true' || (messageElement.attr('is_system') == 'true' && !isValidCharacter)) {
12130 zoomedAvatarImgElement.attr('src', thumbURL);10726 //handle user and system avatars
12131 zoomedAvatarImgElement.attr('data-izoomify-url', thumbURL);10727 const isValidPersona = decodeURIComponent(targetAvatarImg) in power_user.personas;
10728 if (isValidPersona) {
10729 const personaSrc = getUserAvatar(targetAvatarImg);
10730 zoomedAvatarImgElement.attr('src', personaSrc);
10731 zoomedAvatarImgElement.attr('data-izoomify-url', personaSrc);
10732 } else {
10733 zoomedAvatarImgElement.attr('src', thumbURL);
10734 zoomedAvatarImgElement.attr('data-izoomify-url', thumbURL);
10735 }
12132 } else if (messageElement.attr('is_user') == 'false') { //handle char avatars10736 } else if (messageElement.attr('is_user') == 'false') { //handle char avatars
12133 zoomedAvatarImgElement.attr('src', avatarSrc);10737 zoomedAvatarImgElement.attr('src', avatarSrc);
12134 zoomedAvatarImgElement.attr('data-izoomify-url', avatarSrc);10738 zoomedAvatarImgElement.attr('data-izoomify-url', avatarSrc);
@@ -12182,18 +10786,18 @@ jQuery(async function () {
12182 }10786 }
12183 });10787 });
1218410788
12185 $(document).keyup(function (e) {10789 $(document).on('keyup', function (e) {
12186 if (e.key === 'Escape') {10790 if (e.key === 'Escape') {
12187 const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length > 0;10791 const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length > 0;
12188 if (isEditVisible && power_user.auto_save_msg_edits === false) {10792 if (isEditVisible && power_user.auto_save_msg_edits === false) {
12189 closeMessageEditor('all');10793 closeMessageEditor('all');
12190 $('#send_textarea').focus();10794 $('#send_textarea').trigger('focus');
12191 return;10795 return;
12192 }10796 }
12193 if (isEditVisible && power_user.auto_save_msg_edits === true) {10797 if (isEditVisible && power_user.auto_save_msg_edits === true) {
12194 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).trigger('click');10798 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).trigger('click');
12195 closeMessageEditor('reasoning');10799 closeMessageEditor('reasoning');
12196 $('#send_textarea').focus();10800 $('#send_textarea').trigger('focus');
12197 return;10801 return;
12198 }10802 }
12199 if (!this_edit_mes_id && $('#mes_stop').is(':visible')) {10803 if (!this_edit_mes_id && $('#mes_stop').is(':visible')) {
@@ -12210,13 +10814,13 @@ jQuery(async function () {
12210 const target = $(targetElement.selectedOptions).attr('id');10814 const target = $(targetElement.selectedOptions).attr('id');
12211 switch (target) {10815 switch (target) {
12212 case 'set_character_world':10816 case 'set_character_world':
12213 openCharacterWorldPopup();10817 await openCharacterWorldPopup();
12214 break;10818 break;
12215 case 'set_chat_scenario':10819 case 'set_chat_scenario':
12216 await setScenarioOverride();10820 await setScenarioOverride();
12217 break;10821 break;
12218 case 'renameCharButton':10822 case 'renameCharButton':
12219 renameCharacter();10823 await renameCharacter();
12220 break;10824 break;
12221 case 'import_character_info':10825 case 'import_character_info':
12222 await importEmbeddedWorldInfo();10826 await importEmbeddedWorldInfo();
@@ -12294,6 +10898,9 @@ jQuery(async function () {
12294 });10898 });
1229510899
12296 $(document).on('change', '.range-block-counter input, .neo-range-input', function (e) {10900 $(document).on('change', '.range-block-counter input, .neo-range-input', function (e) {
10901 if (!(e.target instanceof HTMLElement)) {
10902 return;
10903 }
12297 e.target.focus();10904 e.target.focus();
12298 e.target.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true }));10905 e.target.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true }));
12299 });10906 });
@@ -12314,7 +10921,6 @@ jQuery(async function () {
12314 } else {10921 } else {
12315 //if value not ok, warn and reset to last known valid value10922 //if value not ok, warn and reset to last known valid value
12316 toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);10923 toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);
12317 console.log(valueBeforeManualInput);
12318 //newSlider.val(valueBeforeManualInput)10924 //newSlider.val(valueBeforeManualInput)
12319 $(this).val(valueBeforeManualInput);10925 $(this).val(valueBeforeManualInput);
12320 }10926 }
@@ -12324,7 +10930,6 @@ jQuery(async function () {
1232410930
12325 $(document).on('keyup', '.range-block-counter input, .neo-range-input', function () {10931 $(document).on('keyup', '.range-block-counter input, .neo-range-input', function () {
12326 valueBeforeManualInput = $(this).val();10932 valueBeforeManualInput = $(this).val();
12327 console.log(valueBeforeManualInput);
12328 isManualInput = true;10933 isManualInput = true;
12329 });10934 });
1233010935
@@ -12342,7 +10947,6 @@ jQuery(async function () {
12342 } else {10947 } else {
12343 //if value not ok, warn and reset to last known valid value10948 //if value not ok, warn and reset to last known valid value
12344 toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);10949 toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);
12345 console.log(valueBeforeManualInput);
12346 $(this).val(valueBeforeManualInput);10950 $(this).val(valueBeforeManualInput);
12347 }10951 }
12348 }10952 }
@@ -12437,13 +11041,6 @@ jQuery(async function () {
12437 // Added here to prevent execution before script.js is loaded and get rid of quirky timeouts11041 // Added here to prevent execution before script.js is loaded and get rid of quirky timeouts
12438 await firstLoadInit();11042 await firstLoadInit();
1243911043
12440 eventSource.on(event_types.CHAT_DELETED, async (name) => {
12441 await deleteItemizedPrompts(name);
12442 });
12443 eventSource.on(event_types.GROUP_CHAT_DELETED, async (name) => {
12444 await deleteItemizedPrompts(name);
12445 });
12446
12447 window.addEventListener('beforeunload', (e) => {11044 window.addEventListener('beforeunload', (e) => {
12448 if (isChatSaving) {11045 if (isChatSaving) {
12449 e.preventDefault();11046 e.preventDefault();
public/scripts/PromptManager.js+259 -96
@@ -6,7 +6,7 @@ import { event_types, eventSource, is_send_press, main_api, substituteParams } f
6import { is_group_generating } from './group-chats.js';6import { is_group_generating } from './group-chats.js';
7import { Message, MessageCollection, TokenHandler } from './openai.js';7import { Message, MessageCollection, TokenHandler } from './openai.js';
8import { power_user } from './power-user.js';8import { power_user } from './power-user.js';
9import { debounce, waitUntilCondition, escapeHtml } from './utils.js';9import { debounce, waitUntilCondition, escapeHtml, uuidv4 } from './utils.js';
10import { debounce_timeout } from './constants.js';10import { debounce_timeout } from './constants.js';
11import { renderTemplateAsync } from './templates.js';11import { renderTemplateAsync } from './templates.js';
12import { Popup } from './popup.js';12import { Popup } from './popup.js';
@@ -29,6 +29,7 @@ function debouncePromise(func, delay) {
29}29}
3030
31const DEFAULT_DEPTH = 4;31const DEFAULT_DEPTH = 4;
32const DEFAULT_ORDER = 100;
3233
33/**34/**
34 * @enum {number}35 * @enum {number}
@@ -77,25 +78,108 @@ const registerPromptManagerMigration = () => {
77 * Represents a prompt.78 * Represents a prompt.
78 */79 */
79class Prompt {80class Prompt {
80 identifier; role; content; name; system_prompt; position; injection_position; injection_depth; injection_order; forbid_overrides; extension;81 /**
82 * Indicates if the prompt is enabled.
83 * @type {boolean}
84 */
85 enabled;
86
87 /**
88 * Unique identifier for the prompt.
89 * @type {string}
90 */
91 identifier;
92
93 /**
94 * Role of the prompt, e.g., 'system', 'user', etc.
95 * @type {string}
96 */
97 role;
98
99 /**
100 * Content of the prompt.
101 * @type {string}
102 */
103 content;
104
105 /**
106 * Display name of the prompt.
107 * @type {string}
108 */
109 name;
110
111 /**
112 * Indicates if the prompt is a system prompt.
113 * @type {boolean}
114 */
115 system_prompt;
116
117 /**
118 * Position of the prompt in the prompt list.
119 * @type {string|number}
120 */
121 position;
122
123 /**
124 * Inject position of the prompt (relative = 0 or in-chat = 1)
125 * @type {number}
126 */
127 injection_position;
128
129 /**
130 * Depth of the prompt in the chat.
131 * @type {number}
132 */
133 injection_depth;
134
135 /**
136 * Order of the prompt in the chat.
137 * @type {number}
138 */
139 injection_order;
140
141 /**
142 * Indicates if the prompt should not be overridden.
143 * @type {boolean}
144 */
145 forbid_overrides;
146
147 /**
148 * Prompt is added by an extension.
149 * @type {boolean}
150 */
151 extension;
152
153 /**
154 * A list of generation type triggers for the prompt injection.
155 * @type {string[]}
156 */
157 injection_trigger;
158
159 /**
160 * Indicates if the prompt is a marker prompt.
161 * @type {boolean}
162 */
163 marker;
81164
82 /**165 /**
83 * Create a new Prompt instance.166 * Create a new Prompt instance.
84 *167 *
85 * @param {Object} param0 - Object containing the properties of the prompt.168 * @param {Object} [param0] - Object containing the properties of the prompt.
86 * @param {string} param0.identifier - The unique identifier of the prompt.169 * @param {string} [param0.identifier] - The unique identifier of the prompt.
87 * @param {string} param0.role - The role associated with the prompt.170 * @param {string} [param0.role] - The role associated with the prompt.
88 * @param {string} param0.content - The content of the prompt.171 * @param {string} [param0.content] - The content of the prompt.
89 * @param {string} [param0.name] - The name of the prompt.172 * @param {string} [param0.name] - The name of the prompt.
90 * @param {boolean} [param0.system_prompt] - Indicates if the prompt is a system prompt.173 * @param {boolean} [param0.system_prompt] - Indicates if the prompt is a system prompt.
91 * @param {string} [param0.position] - The position of the prompt in the prompt list.174 * @param {string|number} [param0.position] - The position of the prompt in the prompt list.
92 * @param {number} [param0.injection_position] - The insert position of the prompt.175 * @param {number} [param0.injection_position] - The insert position of the prompt.
93 * @param {number} [param0.injection_depth] - The depth of the prompt in the chat.176 * @param {number} [param0.injection_depth] - The depth of the prompt in the chat.
94 * @param {number} [param0.injection_order] - The order of the prompt in the chat.177 * @param {number} [param0.injection_order] - The order of the prompt in the chat.
178 * @param {string[]} [param0.injection_trigger] - The generation type trigger for the prompt injection.
95 * @param {boolean} [param0.forbid_overrides] - Indicates if the prompt should not be overridden.179 * @param {boolean} [param0.forbid_overrides] - Indicates if the prompt should not be overridden.
96 * @param {boolean} [param0.extension] - Prompt is added by an extension.180 * @param {boolean} [param0.extension] - Prompt is added by an extension.
97 */181 */
98 constructor({ identifier, role, content, name, system_prompt, position, injection_depth, injection_position, forbid_overrides, extension, injection_order } = {}) {182 constructor({ identifier, role, content, name, system_prompt, position, injection_depth, injection_position, forbid_overrides, extension, injection_order, injection_trigger } = {}) {
99 this.identifier = identifier;183 this.identifier = identifier;
100 this.role = role;184 this.role = role;
101 this.content = content;185 this.content = content;
@@ -106,7 +190,8 @@ class Prompt {
106 this.injection_position = injection_position;190 this.injection_position = injection_position;
107 this.forbid_overrides = forbid_overrides;191 this.forbid_overrides = forbid_overrides;
108 this.extension = extension ?? false;192 this.extension = extension ?? false;
109 this.injection_order = injection_order ?? 100;193 this.injection_order = injection_order ?? DEFAULT_ORDER;
194 this.injection_trigger = injection_trigger ?? [];
110 }195 }
111}196}
112197
@@ -114,7 +199,16 @@ class Prompt {
114 * Representing a collection of prompts.199 * Representing a collection of prompts.
115 */200 */
116export class PromptCollection {201export class PromptCollection {
202 /**
203 * List of Prompts in the collection.
204 * @type {Prompt[]}
205 */
117 collection = [];206 collection = [];
207
208 /**
209 * List of identifiers of prompts that have been overridden.
210 * @type {string[]}
211 */
118 overriddenPrompts = [];212 overriddenPrompts = [];
119213
120 /**214 /**
@@ -129,7 +223,7 @@ export class PromptCollection {
129 /**223 /**
130 * Checks if the provided instances are of the Prompt class.224 * Checks if the provided instances are of the Prompt class.
131 *225 *
132 * @param {...any} prompts - Instances to check.226 * @param {...Prompt} prompts - Instances to check.
133 * @throws Will throw an error if one or more instances are not of the Prompt class.227 * @throws Will throw an error if one or more instances are not of the Prompt class.
134 */228 */
135 checkPromptInstance(...prompts) {229 checkPromptInstance(...prompts) {
@@ -191,6 +285,12 @@ export class PromptCollection {
191 return this.index(identifier) !== -1;285 return this.index(identifier) !== -1;
192 }286 }
193287
288 /**
289 * Overrides a prompt at a specific position in the collection.
290 *
291 * @param {Prompt} prompt - The Prompt instance to override.
292 * @param {number} position - The position in the collection to override the Prompt instance.
293 */
194 override(prompt, position) {294 override(prompt, position) {
195 this.set(prompt, position);295 this.set(prompt, position);
196 this.overriddenPrompts.push(prompt.identifier);296 this.overriddenPrompts.push(prompt.identifier);
@@ -274,7 +374,7 @@ class PromptManager {
274 this.tryGenerate = async () => { };374 this.tryGenerate = async () => { };
275375
276 /** Called to persist the configuration, must return a promise */376 /** Called to persist the configuration, must return a promise */
277 this.saveServiceSettings = () => { };377 this.saveServiceSettings = () => { return Promise.resolve(); };
278378
279 /** Toggle prompt button click */379 /** Toggle prompt button click */
280 this.handleToggle = () => { };380 this.handleToggle = () => { };
@@ -444,32 +544,51 @@ class PromptManager {
444 break;544 break;
445 }545 }
446546
447 document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name').value = prompt.name;547 const nameField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name'));
448 document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role').value = 'system';548 const roleField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role'));
449 document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt').value = prompt.content ?? '';549 const promptField = /** @type {HTMLTextAreaElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt'));
450 document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position').value = prompt.injection_position ?? 0;550 const injectionPositionField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position'));
451 document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth').value = prompt.injection_depth ?? DEFAULT_DEPTH;551 const injectionDepthField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth'));
452 document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_order').value = prompt.injection_order ?? 100;552 const injectionOrderField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_order'));
453 document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block').style.visibility = prompt.injection_position === INJECTION_POSITION.ABSOLUTE ? 'visible' : 'hidden';553 const injectionTriggerField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_trigger'));
454 document.getElementById(this.configuration.prefix + 'prompt_manager_order_block').style.visibility = prompt.injection_position === INJECTION_POSITION.ABSOLUTE ? 'visible' : 'hidden';554 const depthBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block'));
455 document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_forbid_overrides').checked = prompt.forbid_overrides ?? false;555 const orderBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_order_block'));
456 document.getElementById(this.configuration.prefix + 'prompt_manager_forbid_overrides_block').style.visibility = this.overridablePrompts.includes(prompt.identifier) ? 'visible' : 'hidden';556 const forbidOverridesField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_forbid_overrides'));
457 document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt').disabled = prompt.marker ?? false;557 const forbidOverridesBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_forbid_overrides_block'));
458 document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_source_block').style.display = isPulledPrompt ? '' : 'none';558 const entrySourceBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_source_block'));
559 const entrySource = /** @type {HTMLSpanElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_source'));
560
561 nameField.value = prompt.name;
562 roleField.value = 'system';
563 promptField.value = prompt.content ?? '';
564 injectionPositionField.value = (prompt.injection_position ?? 0).toString();
565 injectionDepthField.value = (prompt.injection_depth ?? DEFAULT_DEPTH).toString();
566 injectionOrderField.value = (prompt.injection_order ?? DEFAULT_ORDER).toString();
567 Array.from(injectionTriggerField.options).forEach(option => {
568 option.selected = false;
569 });
570 injectionTriggerField.dispatchEvent(new Event('change', { bubbles: true }));
571 depthBlock.style.visibility = prompt.injection_position === INJECTION_POSITION.ABSOLUTE ? 'visible' : 'hidden';
572 orderBlock.style.visibility = prompt.injection_position === INJECTION_POSITION.ABSOLUTE ? 'visible' : 'hidden';
573 forbidOverridesField.checked = prompt.forbid_overrides ?? false;
574 forbidOverridesBlock.style.visibility = this.overridablePrompts.includes(prompt.identifier) ? 'visible' : 'hidden';
575 promptField.disabled = prompt.marker ?? false;
576 entrySourceBlock.style.display = isPulledPrompt ? '' : 'none';
459577
460 if (isPulledPrompt) {578 if (isPulledPrompt) {
461 const sourceName = this.promptSources[promptId];579 const sourceName = this.promptSources[promptId];
462 document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_source').textContent = sourceName;580 entrySource.textContent = sourceName;
463 }581 }
464582
465 if (!this.systemPrompts.includes(promptId)) {583 if (!this.systemPrompts.includes(promptId)) {
466 document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position').removeAttribute('disabled');584 injectionPositionField.removeAttribute('disabled');
467 }585 }
468 };586 };
469587
470 // Append prompt to selected character588 // Append prompt to selected character
471 this.handleAppendPrompt = (event) => {589 this.handleAppendPrompt = (event) => {
472 const promptID = document.getElementById(this.configuration.prefix + 'prompt_manager_footer_append_prompt').value;590 const appendPromptFooter = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_footer_append_prompt'));
591 const promptID = appendPromptFooter.value;
473 const prompt = this.getPromptById(promptID);592 const prompt = this.getPromptById(promptID);
474593
475 if (prompt) {594 if (prompt) {
@@ -483,7 +602,8 @@ class PromptManager {
483 this.handleDeletePrompt = async (event) => {602 this.handleDeletePrompt = async (event) => {
484 Popup.show.confirm(t`Are you sure you want to delete this prompt?`, null).then((userChoice) => {603 Popup.show.confirm(t`Are you sure you want to delete this prompt?`, null).then((userChoice) => {
485 if (!userChoice) return;604 if (!userChoice) return;
486 const promptID = document.getElementById(this.configuration.prefix + 'prompt_manager_footer_append_prompt').value;605 const appendPromptFooter = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_footer_append_prompt'));
606 const promptID = appendPromptFooter.value;
487 const prompt = this.getPromptById(promptID);607 const prompt = this.getPromptById(promptID);
488608
489 if (prompt && true === this.isPromptDeletionAllowed(prompt)) {609 if (prompt && true === this.isPromptDeletionAllowed(prompt)) {
@@ -566,6 +686,7 @@ class PromptManager {
566 fileOpener.accept = '.json';686 fileOpener.accept = '.json';
567687
568 fileOpener.addEventListener('change', (event) => {688 fileOpener.addEventListener('change', (event) => {
689 if (!(event.target instanceof HTMLInputElement)) return;
569 const file = event.target.files[0];690 const file = event.target.files[0];
570 if (!file) return;691 if (!file) return;
571692
@@ -575,7 +696,7 @@ class PromptManager {
575 const fileContent = event.target.result;696 const fileContent = event.target.result;
576697
577 try {698 try {
578 const data = JSON.parse(fileContent);699 const data = JSON.parse(fileContent.toString());
579 this.import(data);700 this.import(data);
580 } catch (err) {701 } catch (err) {
581 toastr.error(t`An error occurred while importing prompts. More info available in console.`);702 toastr.error(t`An error occurred while importing prompts. More info available in console.`);
@@ -615,7 +736,7 @@ class PromptManager {
615736
616 // Update edit form if present737 // Update edit form if present
617 // @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent738 // @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
618 const popupEditFormPrompt = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt');739 const popupEditFormPrompt = /** @type {HTMLTextAreaElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt'));
619 if (popupEditFormPrompt.offsetParent) {740 if (popupEditFormPrompt.offsetParent) {
620 popupEditFormPrompt.value = prompt.content;741 popupEditFormPrompt.value = prompt.content;
621 }742 }
@@ -673,6 +794,7 @@ class PromptManager {
673794
674 // Trigger re-render when token settings are changed795 // Trigger re-render when token settings are changed
675 document.getElementById('openai_max_context').addEventListener('change', (event) => {796 document.getElementById('openai_max_context').addEventListener('change', (event) => {
797 if (!(event.target instanceof HTMLInputElement)) return;
676 this.serviceSettings.openai_max_context = event.target.value;798 this.serviceSettings.openai_max_context = event.target.value;
677 if (this.activeCharacter) this.renderDebounced();799 if (this.activeCharacter) this.renderDebounced();
678 });800 });
@@ -778,23 +900,33 @@ class PromptManager {
778900
779 /**901 /**
780 * Update a prompt with the values from the HTML form.902 * Update a prompt with the values from the HTML form.
781 * @param {object} prompt - The prompt to be updated.903 * @param {Partial<Prompt>} prompt - The prompt to be updated.
782 * @returns {void}904 * @returns {void}
783 */905 */
784 updatePromptWithPromptEditForm(prompt) {906 updatePromptWithPromptEditForm(prompt) {
785 prompt.name = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name').value;907 const nameField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name'));
786 prompt.role = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role').value;908 const roleField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role'));
787 prompt.content = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt').value;909 const promptField = /** @type {HTMLTextAreaElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt'));
788 prompt.injection_position = Number(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position').value);910 const injectionPositionField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position'));
789 prompt.injection_depth = Number(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth').value);911 const injectionDepthField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth'));
790 prompt.injection_order = Number(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_order').value);912 const injectionOrderField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_order'));
791 prompt.forbid_overrides = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_forbid_overrides').checked;913 const injectionTriggerField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_trigger'));
914 const forbidOverridesField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_forbid_overrides'));
915
916 prompt.name = nameField.value;
917 prompt.role = roleField.value;
918 prompt.content = promptField.value;
919 prompt.injection_position = Number(injectionPositionField.value);
920 prompt.injection_depth = Number(injectionDepthField.value);
921 prompt.injection_order = Number(injectionOrderField.value);
922 prompt.injection_trigger = Array.from(injectionTriggerField.selectedOptions).map(option => option.value);
923 prompt.forbid_overrides = forbidOverridesField.checked;
792 }924 }
793925
794 /**926 /**
795 * Find a prompt by its identifier and update it with the provided object.927 * Find a prompt by its identifier and update it with the provided object.
796 * @param {string} identifier - The identifier of the prompt.928 * @param {string} identifier - The identifier of the prompt.
797 * @param {object} updatePrompt - An object with properties to be updated in the prompt.929 * @param {Prompt} updatePrompt - An object with properties to be updated in the prompt.
798 * @returns {void}930 * @returns {void}
799 */931 */
800 updatePromptByIdentifier(identifier, updatePrompt) {932 updatePromptByIdentifier(identifier, updatePrompt) {
@@ -804,7 +936,7 @@ class PromptManager {
804936
805 /**937 /**
806 * Iterate over an array of prompts, find each one by its identifier, and update them with the provided data.938 * Iterate over an array of prompts, find each one by its identifier, and update them with the provided data.
807 * @param {object[]} prompts - An array of prompt updates.939 * @param {Prompt[]} prompts - An array of prompt updates.
808 * @returns {void}940 * @returns {void}
809 */941 */
810 updatePrompts(prompts) {942 updatePrompts(prompts) {
@@ -826,7 +958,7 @@ class PromptManager {
826958
827 /**959 /**
828 * Add a prompt to the current character's prompt list.960 * Add a prompt to the current character's prompt list.
829 * @param {object} prompt - The prompt to be added.961 * @param {Prompt} prompt - The prompt to be added.
830 * @param {object} character - The character whose prompt list will be updated.962 * @param {object} character - The character whose prompt list will be updated.
831 * @returns {void}963 * @returns {void}
832 */964 */
@@ -839,7 +971,7 @@ class PromptManager {
839971
840 /**972 /**
841 * Remove a prompt from the current character's prompt list.973 * Remove a prompt from the current character's prompt list.
842 * @param {object} prompt - The prompt to be removed.974 * @param {Prompt} prompt - The prompt to be removed.
843 * @param {object} character - The character whose prompt list will be updated.975 * @param {object} character - The character whose prompt list will be updated.
844 * @returns {void}976 * @returns {void}
845 */977 */
@@ -853,7 +985,7 @@ class PromptManager {
853985
854 /**986 /**
855 * Create a new prompt and add it to the list of prompts.987 * Create a new prompt and add it to the list of prompts.
856 * @param {object} prompt - The prompt to be added.988 * @param {Partial<Prompt>} prompt - The prompt to be added.
857 * @param {string} identifier - The identifier for the new prompt.989 * @param {string} identifier - The identifier for the new prompt.
858 * @returns {void}990 * @returns {void}
859 */991 */
@@ -931,7 +1063,7 @@ class PromptManager {
9311063
932 /**1064 /**
933 * Check whether a prompt can be inspected.1065 * Check whether a prompt can be inspected.
934 * @param {object} prompt - The prompt to check.1066 * @param {Prompt} prompt - The prompt to check.
935 * @returns {boolean} True if the prompt is a marker, false otherwise.1067 * @returns {boolean} True if the prompt is a marker, false otherwise.
936 */1068 */
937 isPromptInspectionAllowed(prompt) {1069 isPromptInspectionAllowed(prompt) {
@@ -940,7 +1072,7 @@ class PromptManager {
9401072
941 /**1073 /**
942 * Check whether a prompt can be deleted. System prompts cannot be deleted.1074 * Check whether a prompt can be deleted. System prompts cannot be deleted.
943 * @param {object} prompt - The prompt to check.1075 * @param {Prompt} prompt - The prompt to check.
944 * @returns {boolean} True if the prompt can be deleted, false otherwise.1076 * @returns {boolean} True if the prompt can be deleted, false otherwise.
945 */1077 */
946 isPromptDeletionAllowed(prompt) {1078 isPromptDeletionAllowed(prompt) {
@@ -949,7 +1081,7 @@ class PromptManager {
9491081
950 /**1082 /**
951 * Check whether a prompt can be edited.1083 * Check whether a prompt can be edited.
952 * @param {object} prompt - The prompt to check.1084 * @param {Prompt} prompt - The prompt to check.
953 * @returns {boolean} True if the prompt can be edited, false otherwise.1085 * @returns {boolean} True if the prompt can be edited, false otherwise.
954 */1086 */
955 isPromptEditAllowed(prompt) {1087 isPromptEditAllowed(prompt) {
@@ -966,7 +1098,7 @@ class PromptManager {
9661098
967 /**1099 /**
968 * Check whether a prompt can be toggled on or off.1100 * Check whether a prompt can be toggled on or off.
969 * @param {object} prompt - The prompt to check.1101 * @param {Prompt} prompt - The prompt to check.
970 * @returns {boolean} True if the prompt can be deleted, false otherwise.1102 * @returns {boolean} True if the prompt can be deleted, false otherwise.
971 */1103 */
972 isPromptToggleAllowed(prompt) {1104 isPromptToggleAllowed(prompt) {
@@ -1062,7 +1194,7 @@ class PromptManager {
10621194
1063 /**1195 /**
1064 * Get the prompts for a specific character. Can be filtered to only include enabled prompts.1196 * Get the prompts for a specific character. Can be filtered to only include enabled prompts.
1065 * @returns {object[]} The prompts for the character.1197 * @returns {Prompt[]} The prompts for the character.
1066 * @param character1198 * @param character
1067 * @param onlyEnabled1199 * @param onlyEnabled
1068 */1200 */
@@ -1075,7 +1207,7 @@ class PromptManager {
1075 /**1207 /**
1076 * Get the order of prompts for a specific character. If no character is specified or the character doesn't have a prompt list, an empty array is returned.1208 * Get the order of prompts for a specific character. If no character is specified or the character doesn't have a prompt list, an empty array is returned.
1077 * @param {object|null} character - The character to get the prompt list for.1209 * @param {object|null} character - The character to get the prompt list for.
1078 * @returns {object[]} The prompt list for the character, or an empty array.1210 * @returns {Partial<Prompt>[]} The prompt list for the character, or an empty array.
1079 */1211 */
1080 getPromptOrderForCharacter(character) {1212 getPromptOrderForCharacter(character) {
1081 return !character ? [] : (this.serviceSettings.prompt_order.find(list => String(list.character_id) === String(character.id))?.order ?? []);1213 return !character ? [] : (this.serviceSettings.prompt_order.find(list => String(list.character_id) === String(character.id))?.order ?? []);
@@ -1083,7 +1215,7 @@ class PromptManager {
10831215
1084 /**1216 /**
1085 * Set the prompts for the manager.1217 * Set the prompts for the manager.
1086 * @param {object[]} prompts - The prompts to be set.1218 * @param {Partial<Prompt>[]} prompts - The prompts to be set.
1087 * @returns {void}1219 * @returns {void}
1088 */1220 */
1089 setPrompts(prompts) {1221 setPrompts(prompts) {
@@ -1125,7 +1257,7 @@ class PromptManager {
1125 /**1257 /**
1126 * Finds and returns a prompt by its identifier.1258 * Finds and returns a prompt by its identifier.
1127 * @param {string} identifier - Identifier of the prompt1259 * @param {string} identifier - Identifier of the prompt
1128 * @returns {Object|null} The prompt object, or null if not found1260 * @returns {Prompt|null} The prompt object, or null if not found
1129 */1261 */
1130 getPromptById(identifier) {1262 getPromptById(identifier) {
1131 return this.serviceSettings.prompts.find(item => item && item.identifier === identifier) ?? null;1263 return this.serviceSettings.prompts.find(item => item && item.identifier === identifier) ?? null;
@@ -1143,9 +1275,9 @@ class PromptManager {
1143 /**1275 /**
1144 * Enriches a generic object, creating a new prompt object in the process1276 * Enriches a generic object, creating a new prompt object in the process
1145 *1277 *
1146 * @param {Object} prompt - Prompt object1278 * @param {Partial<Prompt>} prompt - Prompt object
1147 * @param original1279 * @param original
1148 * @returns {Object} An object with "role" and "content" properties1280 * @returns {Prompt} An object with "role" and "content" properties
1149 */1281 */
1150 preparePrompt(prompt, original = null) {1282 preparePrompt(prompt, original = null) {
1151 const groupMembers = this.getActiveGroupCharacters();1283 const groupMembers = this.getActiveGroupCharacters();
@@ -1184,7 +1316,7 @@ class PromptManager {
11841316
1185 const debouncedSaveServiceSettings = debouncePromise(() => this.saveServiceSettings(), 300);1317 const debouncedSaveServiceSettings = debouncePromise(() => this.saveServiceSettings(), 300);
11861318
1187 const textarea = document.getElementById(textareaIdentifier);1319 const textarea = /** @type {HTMLTextAreaElement} */(document.getElementById(textareaIdentifier));
1188 textarea.addEventListener('blur', () => {1320 textarea.addEventListener('blur', () => {
1189 prompt.content = textarea.value;1321 prompt.content = textarea.value;
1190 this.updatePromptByIdentifier(identifier, prompt);1322 this.updatePromptByIdentifier(identifier, prompt);
@@ -1193,9 +1325,15 @@ class PromptManager {
11931325
1194 }1326 }
11951327
1328 /**
1329 * Updates the quick edit textarea for a specific prompt.
1330 * @param {string} identifier - The identifier of the prompt.
1331 * @param {Prompt} prompt - The updated prompt object.
1332 * @returns {string} The ID of the updated textarea element.
1333 */
1196 updateQuickEdit(identifier, prompt) {1334 updateQuickEdit(identifier, prompt) {
1197 const elementId = `${identifier}_prompt_quick_edit_textarea`;1335 const elementId = `${identifier}_prompt_quick_edit_textarea`;
1198 const textarea = document.getElementById(elementId);1336 const textarea = /** @type {HTMLTextAreaElement} */(document.getElementById(elementId));
1199 textarea.value = prompt.content;1337 textarea.value = prompt.content;
12001338
1201 return elementId;1339 return elementId;
@@ -1220,30 +1358,35 @@ class PromptManager {
12201358
1221 /**1359 /**
1222 * Loads a given prompt into the edit form fields.1360 * Loads a given prompt into the edit form fields.
1223 * @param {Object} prompt - Prompt object with properties 'name', 'role', 'content', and 'system_prompt'1361 * @param {Partial<Prompt>} prompt - Prompt object with properties 'name', 'role', 'content', and 'system_prompt'
1224 */1362 */
1225 loadPromptIntoEditForm(prompt) {1363 loadPromptIntoEditForm(prompt) {
1226 const nameField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name');1364 const nameField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name'));
1227 const roleField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role');1365 const roleField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role'));
1228 const promptField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt');1366 const promptField = /** @type {HTMLTextAreaElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt'));
1229 const injectionPositionField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position');1367 const injectionPositionField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position'));
1230 const injectionDepthField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth');1368 const injectionDepthField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth'));
1231 const injectionOrderField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_order');1369 const injectionOrderField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_order'));
1232 const injectionDepthBlock = document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block');1370 const injectionTriggerField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_trigger'));
1233 const injectionOrderBlock = document.getElementById(this.configuration.prefix + 'prompt_manager_order_block');1371 const injectionDepthBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block'));
1234 const forbidOverridesField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_forbid_overrides');1372 const injectionOrderBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_order_block'));
1235 const forbidOverridesBlock = document.getElementById(this.configuration.prefix + 'prompt_manager_forbid_overrides_block');1373 const forbidOverridesField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_forbid_overrides'));
1236 const entrySourceBlock = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_source_block');1374 const forbidOverridesBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_forbid_overrides_block'));
1237 const entrySource = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_source');1375 const entrySourceBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_source_block'));
1376 const entrySource = /** @type {HTMLSpanElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_source'));
1238 const isPulledPrompt = Object.keys(this.promptSources).includes(prompt.identifier);1377 const isPulledPrompt = Object.keys(this.promptSources).includes(prompt.identifier);
12391378
1240 nameField.value = prompt.name ?? '';1379 nameField.value = prompt.name ?? '';
1241 roleField.value = prompt.role || 'system';1380 roleField.value = prompt.role || 'system';
1242 promptField.value = prompt.content ?? '';1381 promptField.value = prompt.content ?? '';
1243 promptField.disabled = prompt.marker ?? false;1382 promptField.disabled = prompt.marker ?? false;
1244 injectionPositionField.value = prompt.injection_position ?? INJECTION_POSITION.RELATIVE;1383 injectionPositionField.value = (prompt.injection_position ?? INJECTION_POSITION.RELATIVE).toString();
1245 injectionDepthField.value = prompt.injection_depth ?? DEFAULT_DEPTH;1384 injectionDepthField.value = (prompt.injection_depth ?? DEFAULT_DEPTH).toString();
1246 injectionOrderField.value = prompt.injection_order ?? 100;1385 injectionOrderField.value = (prompt.injection_order ?? DEFAULT_ORDER).toString();
1386 Array.from(injectionTriggerField.options).forEach(option => {
1387 option.selected = Array.isArray(prompt.injection_trigger) && prompt.injection_trigger.includes(option.value);
1388 });
1389 injectionTriggerField.dispatchEvent(new Event('change', { bubbles: true }));
1247 injectionDepthBlock.style.visibility = prompt.injection_position === INJECTION_POSITION.ABSOLUTE ? 'visible' : 'hidden';1390 injectionDepthBlock.style.visibility = prompt.injection_position === INJECTION_POSITION.ABSOLUTE ? 'visible' : 'hidden';
1248 injectionOrderBlock.style.visibility = prompt.injection_position === INJECTION_POSITION.ABSOLUTE ? 'visible' : 'hidden';1391 injectionOrderBlock.style.visibility = prompt.injection_position === INJECTION_POSITION.ABSOLUTE ? 'visible' : 'hidden';
1249 injectionPositionField.removeAttribute('disabled');1392 injectionPositionField.removeAttribute('disabled');
@@ -1335,17 +1478,19 @@ class PromptManager {
1335 const editArea = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_edit');1478 const editArea = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_edit');
1336 editArea.style.display = 'none';1479 editArea.style.display = 'none';
13371480
1338 const nameField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name');1481 const nameField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name'));
1339 const roleField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role');1482 const roleField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role'));
1340 const promptField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt');1483 const promptField = /** @type {HTMLTextAreaElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt'));
1341 const injectionPositionField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position');1484 const injectionPositionField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position'));
1342 const injectionDepthField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth');1485 const injectionDepthField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth'));
1343 const injectionDepthBlock = document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block');1486 const injectionDepthBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block'));
1344 const injectionOrderBlock = document.getElementById(this.configuration.prefix + 'prompt_manager_order_block');1487 const injectionOrderBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_order_block'));
1345 const forbidOverridesField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_forbid_overrides');1488 const injectionOrderField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_order'));
1346 const forbidOverridesBlock = document.getElementById(this.configuration.prefix + 'prompt_manager_forbid_overrides_block');1489 const injectionTriggerField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_trigger'));
1347 const entrySourceBlock = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_source_block');1490 const forbidOverridesField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_forbid_overrides'));
1348 const entrySource = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_source');1491 const forbidOverridesBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_forbid_overrides_block'));
1492 const entrySourceBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_source_block'));
1493 const entrySource = /** @type {HTMLSpanElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_source'));
13491494
1350 nameField.value = '';1495 nameField.value = '';
1351 roleField.selectedIndex = 0;1496 roleField.selectedIndex = 0;
@@ -1353,7 +1498,9 @@ class PromptManager {
1353 promptField.disabled = false;1498 promptField.disabled = false;
1354 injectionPositionField.selectedIndex = 0;1499 injectionPositionField.selectedIndex = 0;
1355 injectionPositionField.removeAttribute('disabled');1500 injectionPositionField.removeAttribute('disabled');
1356 injectionDepthField.value = DEFAULT_DEPTH;1501 injectionDepthField.value = DEFAULT_DEPTH.toString();
1502 injectionOrderField.value = DEFAULT_ORDER.toString();
1503 injectionTriggerField.value = '';
1357 injectionDepthBlock.style.visibility = 'unset';1504 injectionDepthBlock.style.visibility = 'unset';
1358 injectionOrderBlock.style.visibility = 'unset';1505 injectionOrderBlock.style.visibility = 'unset';
1359 forbidOverridesBlock.style.visibility = 'unset';1506 forbidOverridesBlock.style.visibility = 'unset';
@@ -1373,22 +1520,30 @@ class PromptManager {
13731520
1374 /**1521 /**
1375 * Returns a full list of prompts whose content markers have been substituted.1522 * Returns a full list of prompts whose content markers have been substituted.
1523 * @param {string} generationType - The type of generation, e.g., 'continue' or 'quiet'.
1376 * @returns {PromptCollection} A PromptCollection object1524 * @returns {PromptCollection} A PromptCollection object
1377 */1525 */
1378 getPromptCollection() {1526 getPromptCollection(generationType) {
1527 generationType = String(generationType || 'normal').toLowerCase().trim();
1528 const promptCollection = new PromptCollection();
1379 const promptOrder = this.getPromptOrderForCharacter(this.activeCharacter);1529 const promptOrder = this.getPromptOrderForCharacter(this.activeCharacter);
13801530
1381 const promptCollection = new PromptCollection();
1382 promptOrder.forEach(entry => {1531 promptOrder.forEach(entry => {
1383 if (true === entry.enabled) {1532 const prompt = this.getPromptById(entry.identifier);
1384 const prompt = this.getPromptById(entry.identifier);1533 const allowedTrigger = entry.enabled && this.shouldTrigger(prompt, generationType);
1385 if (prompt) promptCollection.add(this.preparePrompt(prompt));1534
1386 } else if (!entry.enabled && entry.identifier === 'main') {1535 if (!prompt) {
1536 return;
1537 }
1538
1539 if (allowedTrigger) {
1540 promptCollection.add(this.preparePrompt(prompt));
1541 } else if (entry.identifier === 'main') {
1387 // Some extensions require main prompt to be present for relative inserts.1542 // Some extensions require main prompt to be present for relative inserts.
1388 // So we make a GMO-free vegan replacement.1543 // So we make a GMO-free vegan replacement.
1389 const prompt = structuredClone(this.getPromptById(entry.identifier));1544 const replacementPrompt = structuredClone(prompt);
1390 prompt.content = '';1545 replacementPrompt.content = '';
1391 if (prompt) promptCollection.add(this.preparePrompt(prompt));1546 promptCollection.add(this.preparePrompt(replacementPrompt));
1392 }1547 }
1393 });1548 });
13941549
@@ -1396,6 +1551,18 @@ class PromptManager {
1396 }1551 }
13971552
1398 /**1553 /**
1554 * Checks if a prompt should be triggered based on its injection triggers.
1555 * @param {Prompt} prompt - The prompt to check.
1556 * @param {string} generationType - The type of generation to check against.
1557 * @returns {boolean} True if the prompt should be triggered, false otherwise.
1558 */
1559 shouldTrigger(prompt, generationType) {
1560 if (!Array.isArray(prompt?.injection_trigger)) return true;
1561 if (!prompt.injection_trigger.length) return true;
1562 return prompt.injection_trigger.includes(generationType);
1563 }
1564
1565 /**
1399 * Setter for messages property1566 * Setter for messages property
1400 *1567 *
1401 * @param {import('./openai.js').MessageCollection} messages1568 * @param {import('./openai.js').MessageCollection} messages
@@ -1807,11 +1974,7 @@ class PromptManager {
1807 * @returns {string} A string representation of an uuid41974 * @returns {string} A string representation of an uuid4
1808 */1975 */
1809 getUuidv4() {1976 getUuidv4() {
1810 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {1977 return uuidv4();
1811 let r = Math.random() * 16 | 0,
1812 v = c === 'x' ? r : (r & 0x3 | 0x8);
1813 return v.toString(16);
1814 });
1815 }1978 }
18161979
1817 /**1980 /**
public/scripts/RossAscends-mods.js+13 -15
@@ -4,7 +4,6 @@ import {
4 characters,4 characters,
5 online_status,5 online_status,
6 main_api,6 main_api,
7 api_server,
8 is_send_press,7 is_send_press,
9 max_context,8 max_context,
10 saveSettingsDebounced,9 saveSettingsDebounced,
@@ -42,6 +41,7 @@ import { debounce_timeout } from './constants.js';
42import { Popup } from './popup.js';41import { Popup } from './popup.js';
43import { accountStorage } from './util/AccountStorage.js';42import { accountStorage } from './util/AccountStorage.js';
44import { getCurrentUserHandle } from './user.js';43import { getCurrentUserHandle } from './user.js';
44import { kai_settings } from './kai-settings.js';
4545
46var RPanelPin = document.getElementById('rm_button_panel_pin');46var RPanelPin = document.getElementById('rm_button_panel_pin');
47var LPanelPin = document.getElementById('lm_button_panel_pin');47var LPanelPin = document.getElementById('lm_button_panel_pin');
@@ -371,7 +371,7 @@ function RA_autoconnect(PrevApi) {
371 if (online_status === 'no_connection' && power_user.auto_connect) {371 if (online_status === 'no_connection' && power_user.auto_connect) {
372 switch (main_api) {372 switch (main_api) {
373 case 'kobold':373 case 'kobold':
374 if (api_server && isValidUrl(api_server)) {374 if (kai_settings.api_server && isValidUrl(kai_settings.api_server)) {
375 $('#api_button').trigger('click');375 $('#api_button').trigger('click');
376 }376 }
377 break;377 break;
@@ -397,8 +397,6 @@ function RA_autoconnect(PrevApi) {
397 case 'openai':397 case 'openai':
398 if (((secret_state[SECRET_KEYS.OPENAI] || oai_settings.reverse_proxy) && oai_settings.chat_completion_source == chat_completion_sources.OPENAI)398 if (((secret_state[SECRET_KEYS.OPENAI] || oai_settings.reverse_proxy) && oai_settings.chat_completion_source == chat_completion_sources.OPENAI)
399 || ((secret_state[SECRET_KEYS.CLAUDE] || oai_settings.reverse_proxy) && oai_settings.chat_completion_source == chat_completion_sources.CLAUDE)399 || ((secret_state[SECRET_KEYS.CLAUDE] || oai_settings.reverse_proxy) && oai_settings.chat_completion_source == chat_completion_sources.CLAUDE)
400 || ((secret_state[SECRET_KEYS.SCALE] || secret_state[SECRET_KEYS.SCALE_COOKIE]) && oai_settings.chat_completion_source == chat_completion_sources.SCALE)
401 || (oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI)
402 || (secret_state[SECRET_KEYS.OPENROUTER] && oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER)400 || (secret_state[SECRET_KEYS.OPENROUTER] && oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER)
403 || (secret_state[SECRET_KEYS.AI21] && oai_settings.chat_completion_source == chat_completion_sources.AI21)401 || (secret_state[SECRET_KEYS.AI21] && oai_settings.chat_completion_source == chat_completion_sources.AI21)
404 || (secret_state[SECRET_KEYS.MAKERSUITE] && oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE)402 || (secret_state[SECRET_KEYS.MAKERSUITE] && oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE)
@@ -434,19 +432,19 @@ function OpenNavPanels() {
434 //auto-open R nav if locked and previously open432 //auto-open R nav if locked and previously open
435 if (accountStorage.getItem('NavLockOn') == 'true' && accountStorage.getItem('NavOpened') == 'true') {433 if (accountStorage.getItem('NavLockOn') == 'true' && accountStorage.getItem('NavOpened') == 'true') {
436 //console.log("RA -- clicking right nav to open");434 //console.log("RA -- clicking right nav to open");
437 $('#rightNavDrawerIcon').click();435 $('#rightNavDrawerIcon').trigger('click');
438 }436 }
439437
440 //auto-open L nav if locked and previously open438 //auto-open L nav if locked and previously open
441 if (accountStorage.getItem('LNavLockOn') == 'true' && accountStorage.getItem('LNavOpened') == 'true') {439 if (accountStorage.getItem('LNavLockOn') == 'true' && accountStorage.getItem('LNavOpened') == 'true') {
442 console.debug('RA -- clicking left nav to open');440 console.debug('RA -- clicking left nav to open');
443 $('#leftNavDrawerIcon').click();441 $('#leftNavDrawerIcon').trigger('click');
444 }442 }
445443
446 //auto-open WI if locked and previously open444 //auto-open WI if locked and previously open
447 if (accountStorage.getItem('WINavLockOn') == 'true' && accountStorage.getItem('WINavOpened') == 'true') {445 if (accountStorage.getItem('WINavLockOn') == 'true' && accountStorage.getItem('WINavOpened') == 'true') {
448 console.debug('RA -- clicking WI to open');446 console.debug('RA -- clicking WI to open');
449 $('#WIDrawerIcon').click();447 $('#WIDrawerIcon').trigger('click');
450 }448 }
451 }449 }
452}450}
@@ -711,7 +709,7 @@ export function initRossMods() {
711 RA_autoconnect();709 RA_autoconnect();
712 }710 }
713711
714 $('#main_api').change(function () {712 $('#main_api').on('change', function () {
715 var PrevAPI = main_api;713 var PrevAPI = main_api;
716 setTimeout(() => RA_autoconnect(PrevAPI), 100);714 setTimeout(() => RA_autoconnect(PrevAPI), 100);
717 });715 });
@@ -834,11 +832,11 @@ export function initRossMods() {
834 });832 });
835833
836 var chatbarInFocus = false;834 var chatbarInFocus = false;
837 $('#send_textarea').focus(function () {835 $('#send_textarea').on('focus', function () {
838 chatbarInFocus = true;836 chatbarInFocus = true;
839 });837 });
840838
841 $('#send_textarea').blur(function () {839 $('#send_textarea').on('blur', function () {
842 chatbarInFocus = false;840 chatbarInFocus = false;
843 });841 });
844842
@@ -846,8 +844,8 @@ export function initRossMods() {
846 OpenNavPanels();844 OpenNavPanels();
847 }, 300);845 }, 300);
848846
849 $(SelectedCharacterTab).click(function () { accountStorage.setItem('SelectedNavTab', 'rm_button_selected_ch'); });847 $(SelectedCharacterTab).on('click', function () { accountStorage.setItem('SelectedNavTab', 'rm_button_selected_ch'); });
850 $('#rm_button_characters').click(function () { accountStorage.setItem('SelectedNavTab', 'rm_button_characters'); });848 $('#rm_button_characters').on('click', function () { accountStorage.setItem('SelectedNavTab', 'rm_button_characters'); });
851849
852 // when a char is selected from the list, save them as the auto-load character for next page load850 // when a char is selected from the list, save them as the auto-load character for next page load
853851
@@ -932,7 +930,7 @@ export function initRossMods() {
932 var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');930 var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');
933 if (SwipeTargetMesClassParent !== null) {931 if (SwipeTargetMesClassParent !== null) {
934 if (SwipeButR.css('display') === 'flex') {932 if (SwipeButR.css('display') === 'flex') {
935 SwipeButR.click();933 SwipeButR.trigger('click');
936 }934 }
937 }935 }
938 });936 });
@@ -956,7 +954,7 @@ export function initRossMods() {
956 var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');954 var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');
957 if (SwipeTargetMesClassParent !== null) {955 if (SwipeTargetMesClassParent !== null) {
958 if (SwipeButL.css('display') === 'flex') {956 if (SwipeButL.css('display') === 'flex') {
959 SwipeButL.click();957 SwipeButL.trigger('click');
960 }958 }
961 }959 }
962 });960 });
@@ -1159,7 +1157,7 @@ export function initRossMods() {
1159 const lastMes = document.querySelector('.last_mes');1157 const lastMes = document.querySelector('.last_mes');
1160 const editMes = lastMes.querySelector('.mes_block .mes_edit');1158 const editMes = lastMes.querySelector('.mes_block .mes_edit');
1161 if (editMes !== null) {1159 if (editMes !== null) {
1162 $(editMes).click();1160 $(editMes).trigger('click');
1163 return;1161 return;
1164 }1162 }
1165 }1163 }
public/scripts/backgrounds.js+3 -6
@@ -421,7 +421,7 @@ async function autoBackgroundCommand() {
421421
422 const list = options.map(option => `- ${option.text}`).join('\n');422 const list = options.map(option => `- ${option.text}`).join('\n');
423 const prompt = stringFormat(autoBgPrompt, list);423 const prompt = stringFormat(autoBgPrompt, list);
424 const reply = await generateQuietPrompt(prompt, false, false);424 const reply = await generateQuietPrompt({ quietPrompt: prompt });
425 const fuse = new Fuse(options, { keys: ['text'] });425 const fuse = new Fuse(options, { keys: ['text'] });
426 const bestMatch = fuse.search(reply, { limit: 1 });426 const bestMatch = fuse.search(reply, { limit: 1 });
427427
@@ -524,7 +524,7 @@ async function resolveImageUrl(bg, isCustom) {
524 ? bg524 ? bg
525 : getThumbnailUrl('bg', bg);525 : getThumbnailUrl('bg', bg);
526526
527 return `url('${thumbnailUrl}')`;527 return `url("${thumbnailUrl}")`;
528}528}
529529
530/**530/**
@@ -636,12 +636,9 @@ async function uploadBackground(formData) {
636 return;636 return;
637 }637 }
638638
639 const headers = getRequestHeaders();
640 delete headers['Content-Type'];
641
642 const response = await fetch('/api/backgrounds/upload', {639 const response = await fetch('/api/backgrounds/upload', {
643 method: 'POST',640 method: 'POST',
644 headers: headers,641 headers: getRequestHeaders({ omitContentType: true }),
645 body: formData,642 body: formData,
646 cache: 'no-cache',643 cache: 'no-cache',
647 });644 });
public/scripts/bulk-edit.js+6 -2
@@ -1,6 +1,6 @@
1import { characterGroupOverlay } from '../script.js';1import { characterGroupOverlay } from '../script.js';
2import { BulkEditOverlay, BulkEditOverlayState } from './BulkEditOverlay.js';2import { BulkEditOverlay, BulkEditOverlayState, CharacterContextMenu } from './BulkEditOverlay.js';
33import { event_types, eventSource } from './events.js';
44
5let is_bulk_edit = false;5let is_bulk_edit = false;
66
@@ -121,4 +121,8 @@ export function initBulkEdit() {
121 $('#bulkEditButton').on('click', onEditButtonClick);121 $('#bulkEditButton').on('click', onEditButtonClick);
122 $('#bulkSelectAllButton').on('click', onSelectAllButtonClick);122 $('#bulkSelectAllButton').on('click', onSelectAllButtonClick);
123 $('#bulkDeleteButton').on('click', onDeleteButtonClick);123 $('#bulkDeleteButton').on('click', onDeleteButtonClick);
124
125 const characterContextMenu = new CharacterContextMenu(characterGroupOverlay);
126 eventSource.on(event_types.CHARACTER_PAGE_LOADED, characterGroupOverlay.onPageLoad);
127 console.debug('Character context menu initialized', characterContextMenu);
124}128}
public/scripts/cfg-scale.js+1 -1
@@ -135,7 +135,7 @@ function onCfgMenuItemClick() {
135 .siblings('.inline-drawer-content')135 .siblings('.inline-drawer-content')
136 .css('display') !== 'block') {136 .css('display') !== 'block') {
137 $('#floatingPrompt').addClass('resizing');137 $('#floatingPrompt').addClass('resizing');
138 $('#CFGBlockToggle').click();138 $('#CFGBlockToggle').trigger('click');
139 }139 }
140 } else {140 } else {
141 //hide AN if it's already displayed141 //hide AN if it's already displayed
public/scripts/chat-templates.js+78 -7
@@ -1,3 +1,5 @@
1import { t } from './i18n.js';
2
1// the hash can be obtained from command line e.g. via: MODEL=path_to_model; python -c "import json, hashlib, sys; print(hashlib.sha256(json.load(open('"$MODEL"/tokenizer_config.json'))['chat_template'].encode()).hexdigest())"3// the hash can be obtained from command line e.g. via: MODEL=path_to_model; python -c "import json, hashlib, sys; print(hashlib.sha256(json.load(open('"$MODEL"/tokenizer_config.json'))['chat_template'].encode()).hexdigest())"
2// note that chat templates must be trimmed to match the llama.cpp metadata value4// note that chat templates must be trimmed to match the llama.cpp metadata value
3const hash_derivations = {5const hash_derivations = {
@@ -83,21 +85,31 @@ const hash_derivations = {
83 '854b703e44ca06bdb196cc471c728d15dbab61e744fe6cdce980086b61646ed1':85 '854b703e44ca06bdb196cc471c728d15dbab61e744fe6cdce980086b61646ed1':
84 'GLM-4'86 'GLM-4'
85 ,87 ,
86};
8788
88const substr_derivations = {89 // Kimi K2, ...
89 '<|im_start|>': 'ChatML', // qwen2.5, ...90 'aab20feb9bc6881f941ea649356130ffbc4943b3c2577c0991e1fba90de5a0fc':
91 'Moonshot AI'
92 ,
90};93};
9194
95const substr_derivations = [
96 ['Moonshot AI', ['<|im_user|>user<|im_middle|>', '<|im_assistant|>assistant<|im_middle|>', '<|im_end|>']],
97
98 // Generic cases
99 ['ChatML', ['<|im_start|>user', '<|im_start|>assistant', '<|im_end|>']],
100];
101
92const parse_derivation = derivation => (typeof derivation === 'string') ? {102const parse_derivation = derivation => (typeof derivation === 'string') ? {
93 'context': derivation,103 'context': derivation,
94 'instruct': derivation,104 'instruct': derivation,
95} : derivation;105} : derivation;
96106
107const not_found = { context: null, instruct: null };
108
97export async function deriveTemplatesFromChatTemplate(chat_template, hash) {109export async function deriveTemplatesFromChatTemplate(chat_template, hash) {
98 if (chat_template.trim() === '') {110 if (chat_template.trim() === '') {
99 console.log('Missing chat template.');111 console.log('Missing chat template.');
100 return null;112 return not_found;
101 }113 }
102114
103 if (hash in hash_derivations) {115 if (hash in hash_derivations) {
@@ -105,12 +117,71 @@ export async function deriveTemplatesFromChatTemplate(chat_template, hash) {
105 }117 }
106118
107 // heuristics119 // heuristics
108 for (const [substr, derivation] of Object.entries(substr_derivations) ) {120 for (const [derivation, substr] of substr_derivations) {
109 if (chat_template.includes(substr)) {121 if ([substr].flat().every(str => chat_template.includes(str))) {
110 return parse_derivation(derivation);122 return parse_derivation(derivation);
111 }123 }
112 }124 }
113125
114 console.warn(`Unknown chat template hash: ${hash} for [${chat_template}]`);126 console.warn(`Unknown chat template hash: ${hash} for [${chat_template}]`);
115 return null;127 return not_found;
128}
129
130export async function bindModelTemplates(power_user, online_status) {
131 if (online_status === 'no_connection') {
132 return false;
133 }
134
135 const chatTemplateHash = power_user.chat_template_hash;
136 const bindModelTemplates = power_user.model_templates_mappings[online_status]
137 ?? power_user.model_templates_mappings[chatTemplateHash]
138 ?? {};
139 const bindingsMatch = bindModelTemplates
140 && power_user.context.preset == bindModelTemplates['context']
141 && (!power_user.instruct.enabled || power_user.instruct.preset === bindModelTemplates['instruct']);
142
143 const bound = [];
144
145 if (bindingsMatch) {
146 // unmap current preset
147 delete power_user.model_templates_mappings[chatTemplateHash];
148 delete power_user.model_templates_mappings[online_status];
149 toastr.info(t`Context preset for ${online_status} will use defaults when loaded the next time.`);
150 } else {
151 if (power_user.context_derived) {
152 if (power_user.context.preset !== bindModelTemplates['context']) {
153 bound.push(`${power_user.context.preset} context preset`);
154 // toastr.info(`Bound ${power_user.context.preset} preset to currently loaded model and all models that share its chat template.`);
155
156 // map current preset to current chat template hash
157 bindModelTemplates['context'] = power_user.context.preset;
158 }
159 } else {
160 toastr.warning(t`Note: Context derivation is disabled. Not including context preset.`);
161 }
162 if (power_user.instruct.enabled) {
163 if (power_user.instruct_derived) {
164 if (power_user.instruct.preset !== bindModelTemplates['instruct']) {
165 bound.push(`${power_user.instruct.preset} instruct preset`);
166 bindModelTemplates['instruct'] = power_user.instruct.preset;
167 }
168 } else {
169 toastr.warning(t`Note: Instruct derivation is disabled. Not including instruct preset.`);
170 }
171 }
172 if (bound.length == 0) {
173 toastr.warning(t`No applicable presets available.`);
174 return false;
175 }
176
177 toastr.info(t`Bound ${online_status} to ${bound.join(', ')}.`);
178 if (!online_status.startsWith('koboldcpp/ggml-model-')) {
179 power_user.model_templates_mappings[online_status] = bindModelTemplates;
180 }
181 if (chatTemplateHash !== '') {
182 power_user.model_templates_mappings[chatTemplateHash] = bindModelTemplates;
183 }
184 }
185
186 return true;
116}187}
public/scripts/chats.js+191 -17
@@ -43,6 +43,8 @@ import {
43 extractTextFromOffice,43 extractTextFromOffice,
44 download,44 download,
45 getFileText,45 getFileText,
46 getFileExtension,
47 convertTextToBase64,
46} from './utils.js';48} from './utils.js';
47import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';49import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';
48import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';50import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
@@ -68,7 +70,7 @@ import { accountStorage } from './util/AccountStorage.js';
68 * @returns {Promise<string>} Converted file text70 * @returns {Promise<string>} Converted file text
69 */71 */
7072
71const fileSizeLimit = 1024 * 1024 * 100; // 100 MB73const fileSizeLimit = 1024 * 1024 * 350; // 350 MB
72const ATTACHMENT_SOURCE = {74const ATTACHMENT_SOURCE = {
73 GLOBAL: 'global',75 GLOBAL: 'global',
74 CHARACTER: 'character',76 CHARACTER: 'character',
@@ -204,17 +206,16 @@ export async function populateFileAttachment(message, inputId = 'file_form_input
204 const fileNamePrefix = `${Date.now()}_${slug}`;206 const fileNamePrefix = `${Date.now()}_${slug}`;
205 const fileBase64 = await getBase64Async(file);207 const fileBase64 = await getBase64Async(file);
206 let base64Data = fileBase64.split(',')[1];208 let base64Data = fileBase64.split(',')[1];
209 const extension = getFileExtension(file);
207210
208 // If file is image211 // If file is image
209 if (file.type.startsWith('image/')) {212 if (file.type.startsWith('image/')) {
210 const extension = file.type.split('/')[1];
211 const imageUrl = await saveBase64AsFile(base64Data, name2, fileNamePrefix, extension);213 const imageUrl = await saveBase64AsFile(base64Data, name2, fileNamePrefix, extension);
212 message.extra.image = imageUrl;214 message.extra.image = imageUrl;
213 message.extra.inline_image = true;215 message.extra.inline_image = true;
214 }216 }
215 // If file is video217 // If file is video
216 else if (file.type.startsWith('video/')) {218 else if (file.type.startsWith('video/')) {
217 const extension = file.type.split('/')[1];
218 const videoUrl = await saveBase64AsFile(base64Data, name2, fileNamePrefix, extension);219 const videoUrl = await saveBase64AsFile(base64Data, name2, fileNamePrefix, extension);
219 message.extra.video = videoUrl;220 message.extra.video = videoUrl;
220 } else {221 } else {
@@ -224,7 +225,7 @@ export async function populateFileAttachment(message, inputId = 'file_form_input
224 try {225 try {
225 const converter = getConverter(file.type);226 const converter = getConverter(file.type);
226 const fileText = await converter(file);227 const fileText = await converter(file);
227 base64Data = window.btoa(unescape(encodeURIComponent(fileText)));228 base64Data = convertTextToBase64(fileText);
228 } catch (error) {229 } catch (error) {
229 toastr.error(String(error), t`Could not convert file`);230 toastr.error(String(error), t`Could not convert file`);
230 console.error('Could not convert file', error);231 console.error('Could not convert file', error);
@@ -247,6 +248,7 @@ export async function populateFileAttachment(message, inputId = 'file_form_input
247248
248 } catch (error) {249 } catch (error) {
249 console.error('Could not upload file', error);250 console.error('Could not upload file', error);
251 toastr.error(t`Either the file is corrupted or its format is not supported.`, t`Could not upload the file`);
250 } finally {252 } finally {
251 $('#file_form').trigger('reset');253 $('#file_form').trigger('reset');
252 }254 }
@@ -476,7 +478,7 @@ export async function appendFileContent(message, messageText) {
476export function encodeStyleTags(text) {478export function encodeStyleTags(text) {
477 const styleRegex = /<style>(.+?)<\/style>/gims;479 const styleRegex = /<style>(.+?)<\/style>/gims;
478 return text.replaceAll(styleRegex, (_, match) => {480 return text.replaceAll(styleRegex, (_, match) => {
479 return `<custom-style>${escape(match)}</custom-style>`;481 return `<custom-style>${encodeURIComponent(match)}</custom-style>`;
480 });482 });
481}483}
482484
@@ -497,14 +499,7 @@ export function decodeStyleTags(text, { prefix } = { prefix: '.mes_text ' }) {
497 for (let i = 0; i < rule.selectors.length; i++) {499 for (let i = 0; i < rule.selectors.length; i++) {
498 const selector = rule.selectors[i];500 const selector = rule.selectors[i];
499 if (selector) {501 if (selector) {
500 const selectors = (selector.split(' ') ?? []).map((v) => {502 rule.selectors[i] = prefix + sanitizeSelector(selector);
501 if (v.startsWith('.')) {
502 return '.custom-' + v.substring(1);
503 }
504 return v;
505 }).join(' ');
506
507 rule.selectors[i] = prefix + selectors;
508 }503 }
509 }504 }
510 }505 }
@@ -513,6 +508,36 @@ export function decodeStyleTags(text, { prefix } = { prefix: '.mes_text ' }) {
513 }508 }
514 }509 }
515510
511 function sanitizeSelector(selector) {
512 // Handle pseudo-classes that can contain nested selectors
513 const pseudoClasses = ['has', 'not', 'where', 'is', 'matches', 'any'];
514 const pseudoRegex = new RegExp(`:(${pseudoClasses.join('|')})\\(([^)]+)\\)`, 'g');
515
516 // First, sanitize any nested selectors within pseudo-classes
517 selector = selector.replace(pseudoRegex, (match, pseudoClass, content) => {
518 // Recursively sanitize the content within the pseudo-class
519 const sanitizedContent = sanitizeSimpleSelector(content);
520 return `:${pseudoClass}(${sanitizedContent})`;
521 });
522
523 // Then sanitize the main selector parts
524 return sanitizeSimpleSelector(selector);
525 }
526
527 function sanitizeSimpleSelector(selector) {
528 // Split by spaces but preserve complex selectors
529 return selector.split(/\s+/).map((part) => {
530 // Handle class selectors, but preserve pseudo-classes and other complex parts
531 return part.replace(/\.([\w-]+)/g, (match, className) => {
532 // Don't modify if it's already prefixed with 'custom-'
533 if (className.startsWith('custom-')) {
534 return match;
535 }
536 return `.custom-${className}`;
537 });
538 }).join(' ');
539 }
540
516 function sanitizeRuleSet(ruleSet) {541 function sanitizeRuleSet(ruleSet) {
517 if (Array.isArray(ruleSet.selectors) || Array.isArray(ruleSet.declarations)) {542 if (Array.isArray(ruleSet.selectors) || Array.isArray(ruleSet.declarations)) {
518 sanitizeRule(ruleSet);543 sanitizeRule(ruleSet);
@@ -529,7 +554,7 @@ export function decodeStyleTags(text, { prefix } = { prefix: '.mes_text ' }) {
529554
530 return text.replaceAll(styleDecodeRegex, (_, style) => {555 return text.replaceAll(styleDecodeRegex, (_, style) => {
531 try {556 try {
532 let styleCleaned = unescape(style).replaceAll(/<br\/>/g, '');557 let styleCleaned = decodeURIComponent(style).replaceAll(/<br\/>/g, '');
533 const ast = css.parse(styleCleaned);558 const ast = css.parse(styleCleaned);
534 const sheet = ast?.stylesheet;559 const sheet = ast?.stylesheet;
535 if (sheet) {560 if (sheet) {
@@ -893,6 +918,27 @@ async function deleteMessageImage() {
893 await saveChatConditional();918 await saveChatConditional();
894}919}
895920
921async function deleteMessageVideo() {
922 const confirm = await Popup.show.confirm(t`Delete video from message?`, t`This action can't be undone.`);
923 if (!confirm) {
924 return;
925 }
926
927 const mesBlock = $(this).closest('.mes');
928 const mesId = mesBlock.attr('mesid');
929 const message = chat[mesId];
930
931 if (!message?.extra?.video) {
932 console.warn('Message has no video or it is empty');
933 return;
934 }
935
936 delete message.extra.video;
937 mesBlock.find('.mes_video_container').remove();
938
939 await saveChatConditional();
940}
941
896/**942/**
897 * Deletes file from the server.943 * Deletes file from the server.
898 * @param {string} url Path to the file on the server944 * @param {string} url Path to the file on the server
@@ -1475,14 +1521,14 @@ export async function uploadFileAttachmentToServer(file, target) {
1475 try {1521 try {
1476 const converter = getConverter(file.type);1522 const converter = getConverter(file.type);
1477 const fileText = await converter(file);1523 const fileText = await converter(file);
1478 base64Data = window.btoa(unescape(encodeURIComponent(fileText)));1524 base64Data = convertTextToBase64(fileText);
1479 } catch (error) {1525 } catch (error) {
1480 toastr.error(String(error), t`Could not convert file`);1526 toastr.error(String(error), t`Could not convert file`);
1481 console.error('Could not convert file', error);1527 console.error('Could not convert file', error);
1482 }1528 }
1483 } else {1529 } else {
1484 const fileText = await file.text();1530 const fileText = await file.text();
1485 base64Data = window.btoa(unescape(encodeURIComponent(fileText)));1531 base64Data = convertTextToBase64(fileText);
1486 }1532 }
14871533
1488 const fileUrl = await uploadFileAttachment(uniqueFileName, base64Data);1534 const fileUrl = await uploadFileAttachment(uniqueFileName, base64Data);
@@ -1670,6 +1716,132 @@ export function registerFileConverter(mimeType, converter) {
1670 converters[mimeType] = converter;1716 converters[mimeType] = converter;
1671}1717}
16721718
1719export function addDOMPurifyHooks() {
1720 // Allow target="_blank" in links
1721 DOMPurify.addHook('afterSanitizeAttributes', function (node) {
1722 if ('target' in node) {
1723 node.setAttribute('target', '_blank');
1724 node.setAttribute('rel', 'noopener');
1725 }
1726 });
1727
1728 DOMPurify.addHook('uponSanitizeAttribute', (node, data, config) => {
1729 if (!config['MESSAGE_SANITIZE']) {
1730 return;
1731 }
1732
1733 /* Retain the classes on UI elements of messages that interact with the main UI */
1734 const permittedNodeTypes = ['BUTTON', 'DIV'];
1735 if (config['MESSAGE_ALLOW_SYSTEM_UI'] && node.classList.contains('menu_button') && permittedNodeTypes.includes(node.nodeName)) {
1736 return;
1737 }
1738
1739 switch (data.attrName) {
1740 case 'class': {
1741 if (data.attrValue) {
1742 data.attrValue = data.attrValue.split(' ').map((v) => {
1743 if (v.startsWith('fa-') || v.startsWith('note-') || v === 'monospace') {
1744 return v;
1745 }
1746
1747 return 'custom-' + v;
1748 }).join(' ');
1749 }
1750 break;
1751 }
1752 }
1753 });
1754
1755 DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
1756 if (!config['MESSAGE_SANITIZE']) {
1757 return;
1758 }
1759
1760 // Replace line breaks with <br> in unknown elements
1761 if (node instanceof HTMLUnknownElement) {
1762 node.innerHTML = node.innerHTML.trim().replaceAll('\n', '<br>');
1763 }
1764
1765 const isMediaAllowed = isExternalMediaAllowed();
1766 if (isMediaAllowed) {
1767 return;
1768 }
1769
1770 if (!(node instanceof Element)) {
1771 return;
1772 }
1773
1774 let mediaBlocked = false;
1775
1776 switch (node.tagName) {
1777 case 'AUDIO':
1778 case 'VIDEO':
1779 case 'SOURCE':
1780 case 'TRACK':
1781 case 'EMBED':
1782 case 'OBJECT':
1783 case 'IMG': {
1784 const isExternalUrl = (url) => (url.indexOf('://') > 0 || url.indexOf('//') === 0) && !url.startsWith(window.location.origin);
1785 const src = node.getAttribute('src');
1786 const data = node.getAttribute('data');
1787 const srcset = node.getAttribute('srcset');
1788
1789 if (srcset) {
1790 const srcsetUrls = srcset.split(',');
1791
1792 for (const srcsetUrl of srcsetUrls) {
1793 const [url] = srcsetUrl.trim().split(' ');
1794
1795 if (isExternalUrl(url)) {
1796 console.warn('External media blocked', url);
1797 node.remove();
1798 mediaBlocked = true;
1799 break;
1800 }
1801 }
1802 }
1803
1804 if (src && isExternalUrl(src)) {
1805 console.warn('External media blocked', src);
1806 mediaBlocked = true;
1807 node.remove();
1808 }
1809
1810 if (data && isExternalUrl(data)) {
1811 console.warn('External media blocked', data);
1812 mediaBlocked = true;
1813 node.remove();
1814 }
1815
1816 if (mediaBlocked && (node instanceof HTMLMediaElement)) {
1817 node.autoplay = false;
1818 node.pause();
1819 }
1820 }
1821 break;
1822 }
1823
1824 if (mediaBlocked) {
1825 const entityId = getCurrentEntityId();
1826 const warningShownKey = `mediaWarningShown:${entityId}`;
1827
1828 if (accountStorage.getItem(warningShownKey) === null) {
1829 const warningToast = toastr.warning(
1830 t`Use the 'Ext. Media' button to allow it. Click on this message to dismiss.`,
1831 t`External media has been blocked`,
1832 {
1833 timeOut: 0,
1834 preventDuplicates: true,
1835 onclick: () => toastr.clear(warningToast),
1836 },
1837 );
1838
1839 accountStorage.setItem(warningShownKey, 'true');
1840 }
1841 }
1842 });
1843}
1844
1673export function initChatUtilities() {1845export function initChatUtilities() {
1674 $(document).on('click', '.mes_hide', async function () {1846 $(document).on('click', '.mes_hide', async function () {
1675 const messageBlock = $(this).closest('.mes');1847 const messageBlock = $(this).closest('.mes');
@@ -1849,13 +2021,15 @@ export function initChatUtilities() {
1849 reloadCurrentChat();2021 reloadCurrentChat();
1850 });2022 });
18512023
1852 $('#creators_note_styles_button').on('click', function () {2024 $('#creators_note_styles_button').on('click', function (e) {
2025 e.stopPropagation();
1853 openGlobalStylesPreferenceDialog();2026 openGlobalStylesPreferenceDialog();
1854 });2027 });
18552028
1856 $(document).on('click', '.mes_img', expandMessageImage);2029 $(document).on('click', '.mes_img', expandMessageImage);
1857 $(document).on('click', '.mes_img_enlarge', expandAndZoomMessageImage);2030 $(document).on('click', '.mes_img_enlarge', expandAndZoomMessageImage);
1858 $(document).on('click', '.mes_img_delete', deleteMessageImage);2031 $(document).on('click', '.mes_img_delete', deleteMessageImage);
2032 $(document).on('click', '.mes_video_delete', deleteMessageVideo);
18592033
1860 $('#file_form_input').on('change', async () => {2034 $('#file_form_input').on('change', async () => {
1861 const fileInput = document.getElementById('file_form_input');2035 const fileInput = document.getElementById('file_form_input');
public/scripts/constants.js+18 -0
@@ -22,3 +22,21 @@ export const debounce_timeout = {
22 * which is needed to preserve world info timed effects.22 * which is needed to preserve world info timed effects.
23 */23 */
24export const IGNORE_SYMBOL = Symbol.for('ignore');24export const IGNORE_SYMBOL = Symbol.for('ignore');
25
26/**
27 * Common video file extensions. Should be the same as supported by Gemini.
28 * https://ai.google.dev/gemini-api/docs/video-understanding#supported-formats
29 */
30export const VIDEO_EXTENSIONS = ['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', '3gp', 'mkv', 'mpg'];
31
32/**
33 * Known generation triggers that can be passed to Generate function.
34 */
35export const GENERATION_TYPE_TRIGGERS = [
36 'normal',
37 'continue',
38 'impersonate',
39 'swipe',
40 'regenerate',
41 'quiet',
42];
public/scripts/custom-request.js+7 -2
@@ -1,5 +1,5 @@
1import { getPresetManager } from './preset-manager.js';1import { getPresetManager } from './preset-manager.js';
2import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js';2import { extractJsonFromData, extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js';
3import { getTextGenServer } from './textgen-settings.js';3import { getTextGenServer } from './textgen-settings.js';
4import { extractReasoningFromData } from './reasoning.js';4import { extractReasoningFromData } from './reasoning.js';
5import { formatInstructModeChat, formatInstructModePrompt, getInstructStoppingSequences, names_behavior_types } from './instruct-mode.js';5import { formatInstructModeChat, formatInstructModePrompt, getInstructStoppingSequences, names_behavior_types } from './instruct-mode.js';
@@ -467,7 +467,7 @@ export class ChatCompletionService {
467 return json;467 return json;
468 }468 }
469469
470 return {470 const result = {
471 content: extractMessageFromData(json, this.TYPE),471 content: extractMessageFromData(json, this.TYPE),
472 reasoning: extractReasoningFromData(json, {472 reasoning: extractReasoningFromData(json, {
473 mainApi: this.TYPE,473 mainApi: this.TYPE,
@@ -475,6 +475,11 @@ export class ChatCompletionService {
475 ignoreShowThoughts: true,475 ignoreShowThoughts: true,
476 }),476 }),
477 };477 };
478 // Try parse JSON
479 if (data.json_schema) {
480 result.content = JSON.parse(extractJsonFromData(json, { mainApi: this.TYPE, chatCompletionSource: data.chat_completion_source }));
481 }
482 return result;
478 }483 }
479484
480 if (!response.ok) {485 if (!response.ok) {
public/scripts/data-maid.js+21 -9
@@ -1,4 +1,5 @@
1import { getRequestHeaders } from '../script.js';1import { getRequestHeaders } from '../script.js';
2import { VIDEO_EXTENSIONS } from './constants.js';
2import { t } from './i18n.js';3import { t } from './i18n.js';
3import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';4import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';
4import { renderTemplateAsync } from './templates.js';5import { renderTemplateAsync } from './templates.js';
@@ -44,6 +45,10 @@ class DataMaidDialog {
44 name: t`Background Thumbnails`,45 name: t`Background Thumbnails`,
45 description: t`Thumbnails for missing or deleted backgrounds.`,46 description: t`Thumbnails for missing or deleted backgrounds.`,
46 },47 },
48 personaThumbnails: {
49 name: t`Persona Thumbnails`,
50 description: t`Thumbnails for missing or deleted personas.`,
51 },
47 chatBackups: {52 chatBackups: {
48 name: t`Chat Backups`,53 name: t`Chat Backups`,
49 description: t`Automatically generated chat backups.`,54 description: t`Automatically generated chat backups.`,
@@ -217,8 +222,9 @@ class DataMaidDialog {
217 button.addEventListener('click', async () => {222 button.addEventListener('click', async () => {
218 const item = button.closest('.dataMaidItem');223 const item = button.closest('.dataMaidItem');
219 const hash = item?.getAttribute('data-hash');224 const hash = item?.getAttribute('data-hash');
225 const itemName = items.find(i => i.hash === hash)?.name;
220 if (hash) {226 if (hash) {
221 await this.view(prop, hash);227 await this.view(prop, hash, itemName);
222 }228 }
223 });229 });
224 });230 });
@@ -304,13 +310,14 @@ class DataMaidDialog {
304 * Opens the item view for a specific hash.310 * Opens the item view for a specific hash.
305 * @param {string} prop Property name for the category311 * @param {string} prop Property name for the category
306 * @param {string} hash Item hash to view312 * @param {string} hash Item hash to view
313 * @param {string} name Name of the item to view
307 * @private314 * @private
308 */315 */
309 async view(prop, hash) {316 async view(prop, hash, name) {
310 const url = this.getViewUrl(hash);317 const url = this.getViewUrl(hash);
311 const isImage = ['images', 'avatarThumbnails', 'backgroundThumbnails'].includes(prop);318 const isImage = ['images', 'avatarThumbnails', 'backgroundThumbnails'].includes(prop);
312 const element = isImage319 const element = isImage
313 ? await this.getViewElement(url)320 ? await this.getViewElement(url, name)
314 : await this.getTextViewElement(url);321 : await this.getTextViewElement(url);
315 await callGenericPopup(element, POPUP_TYPE.DISPLAY, '', { large: true, wide: true });322 await callGenericPopup(element, POPUP_TYPE.DISPLAY, '', { large: true, wide: true });
316 }323 }
@@ -341,16 +348,21 @@ class DataMaidDialog {
341 }348 }
342349
343 /**350 /**
344 * Gets an image element for viewing images.351 * Gets a media element for viewing images or videos.
345 * @param {string} url View URL352 * @param {string} url View URL
353 * @param {string} name Name of the file
346 * @returns {Promise<HTMLElement>} Image element354 * @returns {Promise<HTMLElement>} Image element
347 * @private355 * @private
348 */356 */
349 async getViewElement(url) {357 async getViewElement(url, name) {
350 const img = document.createElement('img');358 const isVideo = VIDEO_EXTENSIONS.includes(name.split('.').pop());
351 img.src = url;359 const mediaElement = document.createElement(isVideo ? 'video' : 'img');
352 img.classList.add('dataMaidImageView');360 if (mediaElement instanceof HTMLVideoElement) {
353 return img;361 mediaElement.controls = true;
362 }
363 mediaElement.src = url;
364 mediaElement.classList.add('dataMaidImageView');
365 return mediaElement;
354 }366 }
355367
356 /**368 /**
public/scripts/events.js+92 -0
@@ -0,0 +1,92 @@
1import { EventEmitter } from '../lib/eventemitter.js';
2
3export const event_types = {
4 APP_READY: 'app_ready',
5 EXTRAS_CONNECTED: 'extras_connected',
6 MESSAGE_SWIPED: 'message_swiped',
7 MESSAGE_SENT: 'message_sent',
8 MESSAGE_RECEIVED: 'message_received',
9 MESSAGE_EDITED: 'message_edited',
10 MESSAGE_DELETED: 'message_deleted',
11 MESSAGE_UPDATED: 'message_updated',
12 MESSAGE_FILE_EMBEDDED: 'message_file_embedded',
13 MESSAGE_REASONING_EDITED: 'message_reasoning_edited',
14 MESSAGE_REASONING_DELETED: 'message_reasoning_deleted',
15 MORE_MESSAGES_LOADED: 'more_messages_loaded',
16 IMPERSONATE_READY: 'impersonate_ready',
17 CHAT_CHANGED: 'chat_id_changed',
18 GENERATION_AFTER_COMMANDS: 'GENERATION_AFTER_COMMANDS',
19 GENERATION_STARTED: 'generation_started',
20 GENERATION_STOPPED: 'generation_stopped',
21 GENERATION_ENDED: 'generation_ended',
22 SD_PROMPT_PROCESSING: 'sd_prompt_processing',
23 EXTENSIONS_FIRST_LOAD: 'extensions_first_load',
24 EXTENSION_SETTINGS_LOADED: 'extension_settings_loaded',
25 SETTINGS_LOADED: 'settings_loaded',
26 SETTINGS_UPDATED: 'settings_updated',
27 GROUP_UPDATED: 'group_updated',
28 MOVABLE_PANELS_RESET: 'movable_panels_reset',
29 SETTINGS_LOADED_BEFORE: 'settings_loaded_before',
30 SETTINGS_LOADED_AFTER: 'settings_loaded_after',
31 CHATCOMPLETION_SOURCE_CHANGED: 'chatcompletion_source_changed',
32 CHATCOMPLETION_MODEL_CHANGED: 'chatcompletion_model_changed',
33 OAI_PRESET_CHANGED_BEFORE: 'oai_preset_changed_before',
34 OAI_PRESET_CHANGED_AFTER: 'oai_preset_changed_after',
35 OAI_PRESET_EXPORT_READY: 'oai_preset_export_ready',
36 OAI_PRESET_IMPORT_READY: 'oai_preset_import_ready',
37 WORLDINFO_SETTINGS_UPDATED: 'worldinfo_settings_updated',
38 WORLDINFO_UPDATED: 'worldinfo_updated',
39 CHARACTER_EDITED: 'character_edited',
40 CHARACTER_PAGE_LOADED: 'character_page_loaded',
41 CHARACTER_GROUP_OVERLAY_STATE_CHANGE_BEFORE: 'character_group_overlay_state_change_before',
42 CHARACTER_GROUP_OVERLAY_STATE_CHANGE_AFTER: 'character_group_overlay_state_change_after',
43 USER_MESSAGE_RENDERED: 'user_message_rendered',
44 CHARACTER_MESSAGE_RENDERED: 'character_message_rendered',
45 FORCE_SET_BACKGROUND: 'force_set_background',
46 CHAT_DELETED: 'chat_deleted',
47 CHAT_CREATED: 'chat_created',
48 GROUP_CHAT_DELETED: 'group_chat_deleted',
49 GROUP_CHAT_CREATED: 'group_chat_created',
50 GENERATE_BEFORE_COMBINE_PROMPTS: 'generate_before_combine_prompts',
51 GENERATE_AFTER_COMBINE_PROMPTS: 'generate_after_combine_prompts',
52 GENERATE_AFTER_DATA: 'generate_after_data',
53 GROUP_MEMBER_DRAFTED: 'group_member_drafted',
54 GROUP_WRAPPER_STARTED: 'group_wrapper_started',
55 GROUP_WRAPPER_FINISHED: 'group_wrapper_finished',
56 WORLD_INFO_ACTIVATED: 'world_info_activated',
57 TEXT_COMPLETION_SETTINGS_READY: 'text_completion_settings_ready',
58 CHAT_COMPLETION_SETTINGS_READY: 'chat_completion_settings_ready',
59 CHAT_COMPLETION_PROMPT_READY: 'chat_completion_prompt_ready',
60 CHARACTER_FIRST_MESSAGE_SELECTED: 'character_first_message_selected',
61 // TODO: Naming convention is inconsistent with other events
62 CHARACTER_DELETED: 'characterDeleted',
63 CHARACTER_DUPLICATED: 'character_duplicated',
64 CHARACTER_RENAMED: 'character_renamed',
65 CHARACTER_RENAMED_IN_PAST_CHAT: 'character_renamed_in_past_chat',
66 /** @deprecated The event is aliased to STREAM_TOKEN_RECEIVED. */
67 SMOOTH_STREAM_TOKEN_RECEIVED: 'stream_token_received',
68 STREAM_TOKEN_RECEIVED: 'stream_token_received',
69 STREAM_REASONING_DONE: 'stream_reasoning_done',
70 FILE_ATTACHMENT_DELETED: 'file_attachment_deleted',
71 WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate',
72 OPEN_CHARACTER_LIBRARY: 'open_character_library',
73 ONLINE_STATUS_CHANGED: 'online_status_changed',
74 IMAGE_SWIPED: 'image_swiped',
75 CONNECTION_PROFILE_LOADED: 'connection_profile_loaded',
76 CONNECTION_PROFILE_CREATED: 'connection_profile_created',
77 CONNECTION_PROFILE_DELETED: 'connection_profile_deleted',
78 CONNECTION_PROFILE_UPDATED: 'connection_profile_updated',
79 TOOL_CALLS_PERFORMED: 'tool_calls_performed',
80 TOOL_CALLS_RENDERED: 'tool_calls_rendered',
81 CHARACTER_MANAGEMENT_DROPDOWN: 'charManagementDropdown',
82 SECRET_WRITTEN: 'secret_written',
83 SECRET_DELETED: 'secret_deleted',
84 SECRET_ROTATED: 'secret_rotated',
85 SECRET_EDITED: 'secret_edited',
86 PRESET_CHANGED: 'preset_changed',
87 PRESET_DELETED: 'preset_deleted',
88 MAIN_API_CHANGED: 'main_api_changed',
89 WORLDINFO_ENTRIES_LOADED: 'worldinfo_entries_loaded',
90};
91
92export const eventSource = new EventEmitter([event_types.APP_READY]);
public/scripts/extensions/assets/index.js+2 -2
@@ -7,7 +7,7 @@ import { DOMPurify } from '../../../lib.js';
7import { getRequestHeaders, processDroppedFiles, eventSource, event_types } from '../../../script.js';7import { getRequestHeaders, processDroppedFiles, eventSource, event_types } from '../../../script.js';
8import { deleteExtension, extensionNames, getContext, installExtension, renderExtensionTemplateAsync } from '../../extensions.js';8import { deleteExtension, extensionNames, getContext, installExtension, renderExtensionTemplateAsync } from '../../extensions.js';
9import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';9import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
10import { executeSlashCommands } from '../../slash-commands.js';10import { executeSlashCommandsWithOptions } from '../../slash-commands.js';
11import { accountStorage } from '../../util/AccountStorage.js';11import { accountStorage } from '../../util/AccountStorage.js';
12import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js';12import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js';
13import { t } from '../../i18n.js';13import { t } from '../../i18n.js';
@@ -142,7 +142,7 @@ async function downloadAssetsList(url) {
142 const assetDelete = async function () {142 const assetDelete = async function () {
143 if (assetType === 'character') {143 if (assetType === 'character') {
144 toastr.error('Go to the characters menu to delete a character.', 'Character deletion not supported');144 toastr.error('Go to the characters menu to delete a character.', 'Character deletion not supported');
145 await executeSlashCommands(`/go ${asset['id']}`);145 await executeSlashCommandsWithOptions(`/go ${asset['id']}`);
146 return;146 return;
147 }147 }
148 element.off('click');148 element.off('click');
public/scripts/extensions/caption/index.js+12 -3
@@ -1,4 +1,4 @@
1import { ensureImageFormatSupported, getBase64Async, isTrueBoolean, saveBase64AsFile } from '../../utils.js';1import { ensureImageFormatSupported, getBase64Async, getFileExtension, isTrueBoolean, saveBase64AsFile } from '../../utils.js';
2import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js';2import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js';
3import { appendMediaToMessage, eventSource, event_types, getRequestHeaders, saveChatConditional, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js';3import { appendMediaToMessage, eventSource, event_types, getRequestHeaders, saveChatConditional, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js';
4import { getMessageTimeStamp } from '../../RossAscends-mods.js';4import { getMessageTimeStamp } from '../../RossAscends-mods.js';
@@ -53,6 +53,10 @@ function migrateSettings() {
53 if (!extension_settings.caption.template) {53 if (!extension_settings.caption.template) {
54 extension_settings.caption.template = TEMPLATE_DEFAULT;54 extension_settings.caption.template = TEMPLATE_DEFAULT;
55 }55 }
56
57 if (!extension_settings.caption.show_in_chat) {
58 extension_settings.caption.show_in_chat = false;
59 }
56}60}
5761
58/**62/**
@@ -167,6 +171,7 @@ async function sendCaptionedMessage(caption, image) {
167 extra: {171 extra: {
168 image: image,172 image: image,
169 title: messageText,173 title: messageText,
174 inline_image: !!extension_settings.caption.show_in_chat,
170 },175 },
171 };176 };
172 context.chat.push(message);177 context.chat.push(message);
@@ -327,11 +332,11 @@ async function getCaptionForFile(file, prompt, quiet) {
327 setSpinnerIcon();332 setSpinnerIcon();
328 const context = getContext();333 const context = getContext();
329 const fileData = await getBase64Async(await ensureImageFormatSupported(file));334 const fileData = await getBase64Async(await ensureImageFormatSupported(file));
330 const base64Format = fileData.split(',')[0].split(';')[0].split('/')[1];335 const extension = getFileExtension(file);
331 const base64Data = fileData.split(',')[1];336 const base64Data = fileData.split(',')[1];
332 const { caption } = await doCaptionRequest(base64Data, fileData, prompt);337 const { caption } = await doCaptionRequest(base64Data, fileData, prompt);
333 if (!quiet) {338 if (!quiet) {
334 const imagePath = await saveBase64AsFile(base64Data, context.name2, '', base64Format);339 const imagePath = await saveBase64AsFile(base64Data, context.name2, '', extension);
335 await sendCaptionedMessage(caption, imagePath);340 await sendCaptionedMessage(caption, imagePath);
336 }341 }
337 return caption;342 return caption;
@@ -596,6 +601,10 @@ jQuery(async function () {
596 extension_settings.caption.alt_endpoint_enabled = !!$('#caption_altEndpoint_enabled').prop('checked');601 extension_settings.caption.alt_endpoint_enabled = !!$('#caption_altEndpoint_enabled').prop('checked');
597 saveSettingsDebounced();602 saveSettingsDebounced();
598 });603 });
604 $('#caption_show_in_chat').prop('checked', !!(extension_settings.caption.show_in_chat)).on('input', () => {
605 extension_settings.caption.show_in_chat = !!$('#caption_show_in_chat').prop('checked');
606 saveSettingsDebounced();
607 });
599608
600 const onMessageEvent = async (index) => {609 const onMessageEvent = async (index) => {
601 if (!extension_settings.caption.auto_mode) {610 if (!extension_settings.caption.auto_mode) {
public/scripts/extensions/caption/settings.html+10 -4
@@ -48,6 +48,7 @@
48 <option data-type="mistral" value="pixtral-large-2411">pixtral-large-2411</option>48 <option data-type="mistral" value="pixtral-large-2411">pixtral-large-2411</option>
49 <option data-type="mistral" value="mistral-large-pixtral-2411">mistral-large-pixtral-2411</option>49 <option data-type="mistral" value="mistral-large-pixtral-2411">mistral-large-pixtral-2411</option>
50 <option data-type="mistral" value="mistral-small-2503">mistral-small-2503</option>50 <option data-type="mistral" value="mistral-small-2503">mistral-small-2503</option>
51 <option data-type="mistral" value="mistral-small-2506">mistral-small-2506</option>
51 <option data-type="mistral" value="mistral-small-latest">mistral-small-latest</option>52 <option data-type="mistral" value="mistral-small-latest">mistral-small-latest</option>
52 <option data-type="mistral" value="mistral-medium-latest">mistral-medium-latest</option>53 <option data-type="mistral" value="mistral-medium-latest">mistral-medium-latest</option>
53 <option data-type="mistral" value="mistral-medium-2505">mistral-medium-2505</option>54 <option data-type="mistral" value="mistral-medium-2505">mistral-medium-2505</option>
@@ -84,7 +85,6 @@
84 <option data-type="anthropic" value="claude-3-5-haiku-latest">claude-3-5-haiku-latest</option>85 <option data-type="anthropic" value="claude-3-5-haiku-latest">claude-3-5-haiku-latest</option>
85 <option data-type="anthropic" value="claude-3-5-haiku-20241022">claude-3-5-haiku-20241022</option>86 <option data-type="anthropic" value="claude-3-5-haiku-20241022">claude-3-5-haiku-20241022</option>
86 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>87 <option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
87 <option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
88 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>88 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
89 <option data-type="google" value="gemini-2.5-pro">gemini-2.5-pro</option>89 <option data-type="google" value="gemini-2.5-pro">gemini-2.5-pro</option>
90 <option data-type="google" value="gemini-2.5-pro-preview-06-05">gemini-2.5-pro-preview-06-05</option>90 <option data-type="google" value="gemini-2.5-pro-preview-06-05">gemini-2.5-pro-preview-06-05</option>
@@ -189,6 +189,7 @@
189 <option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option>189 <option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option>
190 <option data-type="vllm" value="vllm_current" data-i18n="currently_selected">[Currently selected]</option>190 <option data-type="vllm" value="vllm_current" data-i18n="currently_selected">[Currently selected]</option>
191 <option data-type="custom" value="custom_current" data-i18n="currently_selected">[Currently selected]</option>191 <option data-type="custom" value="custom_current" data-i18n="currently_selected">[Currently selected]</option>
192 <option data-type="xai" value="grok-4-0709">grok-4-0709</option>
192 <option data-type="xai" value="grok-2-vision-1212">grok-2-vision-1212</option>193 <option data-type="xai" value="grok-2-vision-1212">grok-2-vision-1212</option>
193 <option data-type="xai" value="grok-vision-beta">grok-vision-beta</option>194 <option data-type="xai" value="grok-vision-beta">grok-vision-beta</option>
194 <option data-type="pollinations" value="openai">openai</option>195 <option data-type="pollinations" value="openai">openai</option>
@@ -228,23 +229,28 @@
228 </div>229 </div>
229 <div id="caption_prompt_block">230 <div id="caption_prompt_block">
230 <label for="caption_prompt" data-i18n="Caption Prompt">Caption Prompt</label>231 <label for="caption_prompt" data-i18n="Caption Prompt">Caption Prompt</label>
231 <textarea id="caption_prompt" class="text_pole" rows="1" placeholder="&lt; Use default &gt;">{{PROMPT_DEFAULT}}</textarea>232 <textarea id="caption_prompt" class="text_pole textarea_compact autoSetHeight" rows="1" placeholder="&lt; Use default &gt;">{{PROMPT_DEFAULT}}</textarea>
232 <label class="checkbox_label margin-bot-10px" for="caption_prompt_ask" title="Ask for a custom prompt every time an image is captioned.">233 <label class="checkbox_label margin-bot-10px" for="caption_prompt_ask" title="Ask for a custom prompt every time an image is captioned.">
233 <input id="caption_prompt_ask" type="checkbox" class="checkbox">234 <input id="caption_prompt_ask" type="checkbox" class="checkbox">
234 <span data-i18n="Ask every time">Ask every time</span>235 <span data-i18n="Ask every time">Ask every time</span>
235 </label>236 </label>
236 </div>237 </div>
237 <label for="caption_template"><span data-i18n="Message Template">Message Template</span> <small><span data-i18n="(use _space">(use </span> <code>&lcub;&lcub;caption&rcub;&rcub;</code> <span data-i18n="macro)">macro)</span></small></label>238 <label for="caption_template"><span data-i18n="Message Template">Message Template</span> <small><span data-i18n="(use _space">(use </span> <code>&lcub;&lcub;caption&rcub;&rcub;</code> <span data-i18n="macro)">macro)</span></small></label>
238 <textarea id="caption_template" class="text_pole" rows="2" placeholder="&lt; Use default &gt;">{{TEMPLATE_DEFAULT}}</textarea>239 <textarea id="caption_template" class="text_pole textarea_compact autoSetHeight" rows="2" placeholder="&lt; Use default &gt;">{{TEMPLATE_DEFAULT}}</textarea>
239 <label class="checkbox_label" for="caption_auto_mode">240 <label class="checkbox_label" for="caption_auto_mode">
240 <input id="caption_auto_mode" type="checkbox" class="checkbox">241 <input id="caption_auto_mode" type="checkbox" class="checkbox">
241 <span data-i18n="Automatically caption images">Automatically caption images</span>242 <span data-i18n="Automatically caption images">Automatically caption images</span>
242 <i class="fa-solid fa-info-circle" title="Automatically caption images when they are pasted into the chat or attached to messages."></i>243 <i class="fa-solid fa-info-circle" title="Automatically caption images when they are pasted into the chat or attached to messages."></i>
243 </label>244 </label>
244 <label class="checkbox_label margin-bot-10px" for="caption_refine_mode">245 <label class="checkbox_label" for="caption_refine_mode">
245 <input id="caption_refine_mode" type="checkbox" class="checkbox">246 <input id="caption_refine_mode" type="checkbox" class="checkbox">
246 <span data-i18n="Edit captions before saving">Edit captions before saving</span>247 <span data-i18n="Edit captions before saving">Edit captions before saving</span>
247 </label>248 </label>
249 <label class="checkbox_label" for="caption_show_in_chat">
250 <input id="caption_show_in_chat" type="checkbox" class="checkbox">
251 <span data-i18n="Show captions in chat">Show captions in chat</span>
252 </label>
253 <div class="margin-bot-10px"></div>
248 </div>254 </div>
249 </div>255 </div>
250</div>256</div>
public/scripts/extensions/expressions/index.js+31 -16
@@ -1,6 +1,6 @@
1import { Fuse } from '../../../lib.js';1import { Fuse } from '../../../lib.js';
22
3import { characters, eventSource, event_types, generateQuietPrompt, generateRaw, getRequestHeaders, main_api, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types, this_chid } from '../../../script.js';3import { characters, eventSource, event_types, generateQuietPrompt, generateRaw, getRequestHeaders, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types, this_chid } from '../../../script.js';
4import { dragElement, isMobile } from '../../RossAscends-mods.js';4import { dragElement, isMobile } from '../../RossAscends-mods.js';
5import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';5import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
6import { loadMovingUIState, performFuzzySearch, power_user } from '../../power-user.js';6import { loadMovingUIState, performFuzzySearch, power_user } from '../../power-user.js';
@@ -1056,10 +1056,10 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin
1056 inApiCall = true;1056 inApiCall = true;
1057 switch (extension_settings.expressions.promptType) {1057 switch (extension_settings.expressions.promptType) {
1058 case PROMPT_TYPE.raw:1058 case PROMPT_TYPE.raw:
1059 emotionResponse = await generateRaw(text, main_api, false, false, prompt);1059 emotionResponse = await generateRaw({ prompt: text, systemPrompt: prompt });
1060 break;1060 break;
1061 case PROMPT_TYPE.full:1061 case PROMPT_TYPE.full:
1062 emotionResponse = await generateQuietPrompt(prompt, false, false);1062 emotionResponse = await generateQuietPrompt({ quietPrompt: prompt });
1063 break;1063 break;
1064 }1064 }
1065 } finally {1065 } finally {
@@ -1331,7 +1331,7 @@ async function renderFallbackExpressionPicker() {
1331 defaultPicker.empty();1331 defaultPicker.empty();
13321332
13331333
1334 addOption(OPTION_NO_FALLBACK, '[ No fallback ]', !extension_settings.expressions.fallback_expression);1334 addOption(OPTION_NO_FALLBACK, '[ No fallback ]', !extension_settings.expressions.fallback_expression && !extension_settings.expressions.showDefault);
1335 addOption(OPTION_EMOJI_FALLBACK, '[ Default emojis ]', !!extension_settings.expressions.showDefault);1335 addOption(OPTION_EMOJI_FALLBACK, '[ Default emojis ]', !!extension_settings.expressions.showDefault);
13361336
1337 for (const expression of expressions) {1337 for (const expression of expressions) {
@@ -1758,27 +1758,38 @@ async function onExpressionFallbackChanged() {
1758 saveSettingsDebounced();1758 saveSettingsDebounced();
1759}1759}
17601760
1761/**
1762 * Handles the file upload process for a sprite image.
1763 * @param {string} url URL to upload the file to
1764 * @param {FormData} formData FormData object containing the file and other data to upload
1765 * @returns {Promise<any>} - The response data from the server
1766 */
1761async function handleFileUpload(url, formData) {1767async function handleFileUpload(url, formData) {
1762 try {1768 try {
1763 const data = await jQuery.ajax({1769 const result = await fetch(url, {
1764 type: 'POST',1770 method: 'POST',
1765 url: url,1771 headers: getRequestHeaders({ omitContentType: true }),
1766 data: formData,1772 body: formData,
1767 beforeSend: function () { },1773 cache: 'no-cache',
1768 cache: false,
1769 contentType: false,
1770 processData: false,
1771 });1774 });
17721775
1776 if (!result.ok) {
1777 throw new Error(`Upload failed with status ${result.status}`);
1778 }
1779
1780 const data = await result.json();
1781
1773 // Refresh sprites list1782 // Refresh sprites list
1774 const name = formData.get('name');1783 const name = formData.get('name').toString();
1775 delete spriteCache[name];1784 delete spriteCache[name];
1776 await fetchImagesNoCache();1785 await fetchImagesNoCache();
1777 await validateImages(name);1786 await validateImages(name);
17781787
1779 return data;1788 return data ?? {};
1780 } catch (error) {1789 } catch (error) {
1790 console.error('Error uploading image:', error);
1781 toastr.error('Failed to upload image');1791 toastr.error('Failed to upload image');
1792 return {};
1782 }1793 }
1783}1794}
17841795
@@ -1996,7 +2007,11 @@ async function onClickExpressionUploadPackButton() {
1996 const uploadToast = toastr.info('Please wait...', 'Upload is processing', { timeOut: 0, extendedTimeOut: 0 });2007 const uploadToast = toastr.info('Please wait...', 'Upload is processing', { timeOut: 0, extendedTimeOut: 0 });
1997 const { count } = await handleFileUpload('/api/sprites/upload-zip', formData);2008 const { count } = await handleFileUpload('/api/sprites/upload-zip', formData);
1998 toastr.clear(uploadToast);2009 toastr.clear(uploadToast);
1999 toastr.success(`Uploaded ${count} image(s) for ${name}`);2010
2011 // Only show success message if at least one image was uploaded
2012 if (count) {
2013 toastr.success(`Uploaded ${count} image(s) for ${name}`);
2014 }
20002015
2001 // Reset the input2016 // Reset the input
2002 e.target.form.reset();2017 e.target.form.reset();
@@ -2116,7 +2131,7 @@ function migrateSettings() {
2116 saveSettingsDebounced();2131 saveSettingsDebounced();
2117 }2132 }
21182133
2119 if (extension_settings.expressions.showDefault && extension_settings.expressions.fallback_expression !== undefined) {2134 if (extension_settings.expressions.showDefault && extension_settings.expressions.fallback_expression) {
2120 extension_settings.expressions.showDefault = false;2135 extension_settings.expressions.showDefault = false;
2121 saveSettingsDebounced();2136 saveSettingsDebounced();
2122 }2137 }
public/scripts/extensions/gallery/index.js+9 -23
@@ -8,7 +8,7 @@ import {
8 animation_easing,8 animation_easing,
9} from '../../../script.js';9} from '../../../script.js';
10import { groups, selected_group } from '../../group-chats.js';10import { groups, selected_group } from '../../group-chats.js';
11import { loadFileToDocument, delay, getBase64Async, getSanitizedFilename } from '../../utils.js';11import { loadFileToDocument, delay, getBase64Async, getSanitizedFilename, saveBase64AsFile, getFileExtension } from '../../utils.js';
12import { loadMovingUIState } from '../../power-user.js';12import { loadMovingUIState } from '../../power-user.js';
13import { dragElement } from '../../RossAscends-mods.js';13import { dragElement } from '../../RossAscends-mods.js';
14import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';14import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
@@ -324,7 +324,6 @@ async function showCharGallery(deleteModeState = false) {
324 await delay(100);324 await delay(100);
325 await initGallery(items, url);325 await initGallery(items, url);
326 } catch (err) {326 } catch (err) {
327 console.trace();
328 console.error(err);327 console.error(err);
329 }328 }
330}329}
@@ -341,27 +340,12 @@ async function showCharGallery(deleteModeState = false) {
341async function uploadFile(file, url) {340async function uploadFile(file, url) {
342 try {341 try {
343 // Convert the file to a base64 string342 // Convert the file to a base64 string
344 const base64Data = await getBase64Async(file);343 const fileBase64 = await getBase64Async(file);
344 const base64Data = fileBase64.split(',')[1];
345 const extension = getFileExtension(file);
346 const path = await saveBase64AsFile(base64Data, url, '', extension);
345347
346 // Create the payload348 toastr.success(t`File uploaded successfully. Saved at: ${path}`);
347 const payload = {
348 image: base64Data,
349 ch_name: url,
350 };
351
352 const response = await fetch('/api/images/upload', {
353 method: 'POST',
354 headers: getRequestHeaders(),
355 body: JSON.stringify(payload),
356 });
357
358 if (!response.ok) {
359 throw new Error(`HTTP error! Status: ${response.status}`);
360 }
361
362 const result = await response.json();
363
364 toastr.success(t`File uploaded successfully. Saved at: ${result.path}`);
365 } catch (error) {349 } catch (error) {
366 console.error('There was an issue uploading the file:', error);350 console.error('There was an issue uploading the file:', error);
367351
@@ -620,6 +604,9 @@ function makeDragImg(id, url) {
620 }604 }
621 draggableElem.id = uniqueId;605 draggableElem.id = uniqueId;
622606
607 // Add the galleryImageDraggable to have unique class
608 draggableElem.classList.add('galleryImageDraggable');
609
623 // Ensure that the newly added element is displayed as block610 // Ensure that the newly added element is displayed as block
624 draggableElem.style.display = 'block';611 draggableElem.style.display = 'block';
625 //and has no padding unlike other non-zoomed-avatar draggables612 //and has no padding unlike other non-zoomed-avatar draggables
@@ -749,7 +736,6 @@ async function listGalleryCommand(args) {
749 return JSON.stringify(items.map(it => it.src));736 return JSON.stringify(items.map(it => it.src));
750737
751 } catch (err) {738 } catch (err) {
752 console.trace();
753 console.error(err);739 console.error(err);
754 }740 }
755 return JSON.stringify([]);741 return JSON.stringify([]);
public/scripts/extensions/memory/index.js+15 -3
@@ -506,7 +506,7 @@ async function summarizeCallback(args, text) {
506 case summary_sources.extras:506 case summary_sources.extras:
507 return await callExtrasSummarizeAPI(text);507 return await callExtrasSummarizeAPI(text);
508 case summary_sources.main:508 case summary_sources.main:
509 return removeReasoningFromString(await generateRaw(text, '', false, false, prompt, extension_settings.memory.overrideResponseLength));509 return removeReasoningFromString(await generateRaw({ prompt: text, systemPrompt: prompt, responseLength: extension_settings.memory.overrideResponseLength }));
510 case summary_sources.webllm: {510 case summary_sources.webllm: {
511 const messages = [{ role: 'system', content: prompt }, { role: 'user', content: text }].filter(m => m.content);511 const messages = [{ role: 'system', content: prompt }, { role: 'user', content: text }].filter(m => m.content);
512 const params = extension_settings.memory.overrideResponseLength > 0 ? { max_tokens: extension_settings.memory.overrideResponseLength } : {};512 const params = extension_settings.memory.overrideResponseLength > 0 ? { max_tokens: extension_settings.memory.overrideResponseLength } : {};
@@ -677,7 +677,13 @@ async function summarizeChatMain(context, force, skipWIAN) {
677 if (prompt_builders.DEFAULT === extension_settings.memory.prompt_builder) {677 if (prompt_builders.DEFAULT === extension_settings.memory.prompt_builder) {
678 try {678 try {
679 inApiCall = true;679 inApiCall = true;
680 summary = await generateQuietPrompt(prompt, false, skipWIAN, '', '', extension_settings.memory.overrideResponseLength);680 /** @type {import('../../../script.js').GenerateQuietPromptParams} */
681 const params = {
682 quietPrompt: prompt,
683 skipWIAN: skipWIAN,
684 responseLength: extension_settings.memory.overrideResponseLength,
685 };
686 summary = await generateQuietPrompt(params);
681 } finally {687 } finally {
682 inApiCall = false;688 inApiCall = false;
683 }689 }
@@ -701,7 +707,13 @@ async function summarizeChatMain(context, force, skipWIAN) {
701 return null;707 return null;
702 }708 }
703709
704 const rawSummary = await generateRaw(rawPrompt, '', false, false, prompt, extension_settings.memory.overrideResponseLength);710 /** @type {import('../../../script.js').GenerateRawParams} */
711 const params = {
712 prompt: rawPrompt,
713 systemPrompt: prompt,
714 responseLength: extension_settings.memory.overrideResponseLength,
715 };
716 const rawSummary = await generateRaw(params);
705 summary = removeReasoningFromString(rawSummary);717 summary = removeReasoningFromString(rawSummary);
706 index = lastUsedIndex;718 index = lastUsedIndex;
707 } finally {719 } finally {
public/scripts/extensions/quick-reply/src/QuickReplySettings.js+3 -3
@@ -1,5 +1,5 @@
1import { chat_metadata, saveChatDebounced, saveSettingsDebounced } from '../../../../script.js';1import { chat_metadata, saveSettingsDebounced } from '../../../../script.js';
2import { extension_settings } from '../../../extensions.js';2import { extension_settings, saveMetadataDebounced } from '../../../extensions.js';
3import { QuickReplyConfig } from './QuickReplyConfig.js';3import { QuickReplyConfig } from './QuickReplyConfig.js';
44
5export class QuickReplySettings {5export class QuickReplySettings {
@@ -62,7 +62,7 @@ export class QuickReplySettings {
62 saveSettingsDebounced();62 saveSettingsDebounced();
63 if (this.chatConfig) {63 if (this.chatConfig) {
64 chat_metadata.quickReply = this.chatConfig.toJSON();64 chat_metadata.quickReply = this.chatConfig.toJSON();
65 saveChatDebounced();65 saveMetadataDebounced();
66 }66 }
67 if (this.onSave) {67 if (this.onSave) {
68 this.onSave();68 this.onSave();
public/scripts/extensions/regex/index.js+5 -0
@@ -375,6 +375,11 @@ function migrateSettings() {
375 performSave = true;375 performSave = true;
376 }376 }
377377
378 if (!Array.isArray(script.placement)) {
379 script.placement = [];
380 performSave = true;
381 }
382
378 if (script.placement.includes(regex_placement.MD_DISPLAY)) {383 if (script.placement.includes(regex_placement.MD_DISPLAY)) {
379 script.placement = script.placement.length === 1 ?384 script.placement = script.placement.length === 1 ?
380 Object.values(regex_placement).filter((e) => e !== regex_placement.MD_DISPLAY) :385 Object.values(regex_placement).filter((e) => e !== regex_placement.MD_DISPLAY) :
public/scripts/extensions/shared.js+7 -3
@@ -38,10 +38,14 @@ export async function getMultimodalCaption(base64Img, prompt) {
38 const isVllm = extension_settings.caption.multimodal_api === 'vllm';38 const isVllm = extension_settings.caption.multimodal_api === 'vllm';
39 const base64Bytes = base64Img.length * 0.75;39 const base64Bytes = base64Img.length * 0.75;
40 const compressionLimit = 2 * 1024 * 1024;40 const compressionLimit = 2 * 1024 * 1024;
41 const safeMimeTypes = ['image/jpeg', 'image/png', 'image/webp'];
42 const mimeType = base64Img?.split(';')?.[0]?.split(':')?.[1];
41 const thumbnailNeeded = ['google', 'openrouter', 'mistral', 'groq', 'vertexai'].includes(extension_settings.caption.multimodal_api);43 const thumbnailNeeded = ['google', 'openrouter', 'mistral', 'groq', 'vertexai'].includes(extension_settings.caption.multimodal_api);
42 if ((thumbnailNeeded && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) {44 if ((thumbnailNeeded && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) {
43 const maxSide = 1024;45 const maxSide = 2048;
44 base64Img = await createThumbnail(base64Img, maxSide, maxSide, 'image/jpeg');46 base64Img = await createThumbnail(base64Img, maxSide, maxSide);
47 } else if (!safeMimeTypes.includes(mimeType)) {
48 base64Img = await createThumbnail(base64Img, null, null);
45 }49 }
4650
47 const proxyUrl = useReverseProxy ? oai_settings.reverse_proxy : '';51 const proxyUrl = useReverseProxy ? oai_settings.reverse_proxy : '';
@@ -467,7 +471,7 @@ export class ConnectionManagerRequestService {
467471
468 /**472 /**
469 * @param {import('./connection-manager/index.js').ConnectionProfile?} [profile]473 * @param {import('./connection-manager/index.js').ConnectionProfile?} [profile]
470 * @return {import('../../script.js').ConnectAPIMap}474 * @return {import('../slash-commands.js').ConnectAPIMap}
471 * @throws {Error}475 * @throws {Error}
472 */476 */
473 static validateProfile(profile) {477 static validateProfile(profile) {
public/scripts/extensions/stable-diffusion/index.js+31 -15
@@ -51,7 +51,7 @@ import {
51 SlashCommandArgument,51 SlashCommandArgument,
52 SlashCommandNamedArgument,52 SlashCommandNamedArgument,
53} from '../../slash-commands/SlashCommandArgument.js';53} from '../../slash-commands/SlashCommandArgument.js';
54import { debounce_timeout } from '../../constants.js';54import { debounce_timeout, VIDEO_EXTENSIONS } from '../../constants.js';
55import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';55import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
56import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js';56import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js';
57import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';57import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
@@ -339,6 +339,7 @@ const defaultSettings = {
339};339};
340340
341const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);341const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);
342const isVideo = (/** @type {string} */ format) => VIDEO_EXTENSIONS.includes(String(format || '').trim().toLowerCase());
342343
343/**344/**
344 * Generate interceptor for interactive mode triggers.345 * Generate interceptor for interactive mode triggers.
@@ -2476,14 +2477,14 @@ async function generatePicture(initiator, args, trigger, message, callback) {
24762477
2477 if (generationType === generationMode.BACKGROUND) {2478 if (generationType === generationMode.BACKGROUND) {
2478 const callbackOriginal = callback;2479 const callbackOriginal = callback;
2479 callback = async function (prompt, imagePath, generationType, _negativePromptPrefix, _initiator, prefixedPrompt) {2480 callback = async function (prompt, imagePath, generationType, _negativePromptPrefix, _initiator, prefixedPrompt, format) {
2480 const imgUrl = `url("${encodeURI(imagePath)}")`;2481 const imgUrl = `url("${encodeURI(imagePath)}")`;
2481 await eventSource.emit(event_types.FORCE_SET_BACKGROUND, { url: imgUrl, path: imagePath });2482 await eventSource.emit(event_types.FORCE_SET_BACKGROUND, { url: imgUrl, path: imagePath });
24822483
2483 if (typeof callbackOriginal === 'function') {2484 if (typeof callbackOriginal === 'function') {
2484 await callbackOriginal(prompt, imagePath, generationType, negativePromptPrefix, initiator, prefixedPrompt);2485 await callbackOriginal(prompt, imagePath, generationType, negativePromptPrefix, initiator, prefixedPrompt, format);
2485 } else {2486 } else {
2486 await sendMessage(prompt, imagePath, generationType, negativePromptPrefix, initiator, prefixedPrompt);2487 await sendMessage(prompt, imagePath, generationType, negativePromptPrefix, initiator, prefixedPrompt, format);
2487 }2488 }
2488 };2489 };
2489 }2490 }
@@ -2725,7 +2726,7 @@ function getUserAvatarUrl() {
2725 * @returns {Promise<string>} - A promise that resolves when the prompt generation completes.2726 * @returns {Promise<string>} - A promise that resolves when the prompt generation completes.
2726 */2727 */
2727async function generatePrompt(quietPrompt) {2728async function generatePrompt(quietPrompt) {
2728 const reply = await generateQuietPrompt(quietPrompt, false, false);2729 const reply = await generateQuietPrompt({ quietPrompt });
2729 const processedReply = processReply(reply);2730 const processedReply = processReply(reply);
27302731
2731 if (!processedReply) {2732 if (!processedReply) {
@@ -2838,8 +2839,8 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
2838 const filename = `${characterName}_${humanizedDateTime()}`;2839 const filename = `${characterName}_${humanizedDateTime()}`;
2839 const base64Image = await saveBase64AsFile(result.data, characterName, filename, result.format);2840 const base64Image = await saveBase64AsFile(result.data, characterName, filename, result.format);
2840 callback2841 callback
2841 ? await callback(prompt, base64Image, generationType, additionalNegativePrefix, initiator, prefixedPrompt)2842 ? await callback(prompt, base64Image, generationType, additionalNegativePrefix, initiator, prefixedPrompt, result.format)
2842 : await sendMessage(prompt, base64Image, generationType, additionalNegativePrefix, initiator, prefixedPrompt);2843 : await sendMessage(prompt, base64Image, generationType, additionalNegativePrefix, initiator, prefixedPrompt, result.format);
2843 return base64Image;2844 return base64Image;
2844}2845}
28452846
@@ -3524,7 +3525,8 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
3524 const text = await promptResult.text();3525 const text = await promptResult.text();
3525 throw new Error(text);3526 throw new Error(text);
3526 }3527 }
3527 return { format: 'png', data: await promptResult.text() };3528 const { format, data } = await promptResult.json();
3529 return { format, data };
3528}3530}
35293531
35303532
@@ -3864,8 +3866,9 @@ async function onComfyDeleteWorkflowClick() {
3864 * @param {string} additionalNegativePrefix Additional negative prompt used for the image generation3866 * @param {string} additionalNegativePrefix Additional negative prompt used for the image generation
3865 * @param {string} initiator The initiator of the image generation3867 * @param {string} initiator The initiator of the image generation
3866 * @param {string} prefixedPrompt Prompt with an attached specific prefix3868 * @param {string} prefixedPrompt Prompt with an attached specific prefix
3869 * @param {string} format Format of the image (e.g., 'png', 'jpg')
3867 */3870 */
3868async function sendMessage(prompt, image, generationType, additionalNegativePrefix, initiator, prefixedPrompt) {3871async function sendMessage(prompt, image, generationType, additionalNegativePrefix, initiator, prefixedPrompt, format) {
3869 const context = getContext();3872 const context = getContext();
3870 const name = context.groupId ? systemUserName : context.name2;3873 const name = context.groupId ? systemUserName : context.name2;
3871 const template = extension_settings.sd.prompts[generationMode.MESSAGE] || '{{prompt}}';3874 const template = extension_settings.sd.prompts[generationMode.MESSAGE] || '{{prompt}}';
@@ -3885,6 +3888,12 @@ async function sendMessage(prompt, image, generationType, additionalNegativePref
3885 image_swipes: [image],3888 image_swipes: [image],
3886 },3889 },
3887 };3890 };
3891 if (isVideo(format)) {
3892 message.extra.video = image;
3893 delete message.extra.image;
3894 delete message.extra.image_swipes;
3895 delete message.extra.inline_image;
3896 }
3888 context.chat.push(message);3897 context.chat.push(message);
3889 const messageId = context.chat.length - 1;3898 const messageId = context.chat.length - 1;
3890 await eventSource.emit(event_types.MESSAGE_RECEIVED, messageId, 'extension');3899 await eventSource.emit(event_types.MESSAGE_RECEIVED, messageId, 'extension');
@@ -4068,7 +4077,7 @@ async function sdMessageButton(e) {
4068 }4077 }
4069 }4078 }
40704079
4071 function saveGeneratedImage(prompt, image, generationType, negative) {4080 function saveGeneratedImage(prompt, image, generationType, negative, _initiator, _prefixedPrompt, format) {
4072 // Some message sources may not create the extra object4081 // Some message sources may not create the extra object
4073 if (typeof message.extra !== 'object' || message.extra === null) {4082 if (typeof message.extra !== 'object' || message.extra === null) {
4074 message.extra = {};4083 message.extra = {};
@@ -4085,17 +4094,24 @@ async function sdMessageButton(e) {
4085 swipes.push(message.extra.image);4094 swipes.push(message.extra.image);
4086 }4095 }
40874096
4088 swipes.push(image);4097 const isVideoFormat = isVideo(format);
4098
4099 if (isVideoFormat) {
4100 message.extra.video = image;
4101 } else {
4102 swipes.push(image);
4103
4104 // If already contains an image and it's not inline - leave it as is
4105 message.extra.inline_image = !(message.extra.image && !message.extra.inline_image);
4106 message.extra.image = image;
4107 }
40894108
4090 // If already contains an image and it's not inline - leave it as is
4091 message.extra.inline_image = !(message.extra.image && !message.extra.inline_image);
4092 message.extra.image = image;
4093 message.extra.title = prompt;4109 message.extra.title = prompt;
4094 message.extra.generationType = generationType;4110 message.extra.generationType = generationType;
4095 message.extra.negative = negative;4111 message.extra.negative = negative;
4096 appendMediaToMessage(message, $mes);4112 appendMediaToMessage(message, $mes);
40974113
4098 context.saveChat();4114 return context.saveChat();
4099 }4115 }
4100}4116}
41014117
public/scripts/extensions/token-counter/index.js+0 -13
@@ -9,19 +9,6 @@ import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
9import { renderExtensionTemplateAsync } from '../../extensions.js';9import { renderExtensionTemplateAsync } from '../../extensions.js';
10import { t } from '../../i18n.js';10import { t } from '../../i18n.js';
1111
12function rgb2hex(rgb) {
13 rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
14 return (rgb && rgb.length === 4) ? '#' +
15 ('0' + parseInt(rgb[1], 10).toString(16)).slice(-2) +
16 ('0' + parseInt(rgb[2], 10).toString(16)).slice(-2) +
17 ('0' + parseInt(rgb[3], 10).toString(16)).slice(-2) : '';
18}
19
20$('button').click(function () {
21 var hex = rgb2hex($('input').val());
22 $('.result').html(hex);
23});
24
25async function doTokenCounter() {12async function doTokenCounter() {
26 const { tokenizerName, tokenizerId } = getFriendlyTokenizerName(main_api);13 const { tokenizerName, tokenizerId } = getFriendlyTokenizerName(main_api);
27 const html = await renderExtensionTemplateAsync('token-counter', 'window', { tokenizerName });14 const html = await renderExtensionTemplateAsync('token-counter', 'window', { tokenizerName });
public/scripts/extensions/translate/index.html+8 -2
@@ -28,8 +28,14 @@
28 <option value="free">Free</option>28 <option value="free">Free</option>
29 <option value="pro">Pro</option>29 <option value="pro">Pro</option>
30 </select>30 </select>
31 <div id="translate_key_button" class="menu_button fa-solid fa-key margin0"></div>31 <div id="translate_key_button" class="menu_button menu_button_icon margin0 manage-api-keys">
32 <div id="translate_url_button" class="menu_button fa-solid fa-link margin0"></div>32 <i class="fa-solid fa-key"></i>
33 <span>Key</span>
34 </div>
35 <div id="translate_url_button" class="menu_button menu_button_icon margin0 manage-api-keys">
36 <i class="fa-solid fa-link"></i>
37 <span>URL</span>
38 </div>
33 </div>39 </div>
34 <label data-i18n="ext_translate_target_lang" for="translation_target_language">Target Language</label>40 <label data-i18n="ext_translate_target_lang" for="translation_target_language">Target Language</label>
35 <select id="translation_target_language" name="target_language"></select>41 <select id="translation_target_language" name="target_language"></select>
public/scripts/extensions/translate/index.js+12 -59
@@ -10,9 +10,9 @@ import {
10 updateMessageBlock,10 updateMessageBlock,
11} from '../../../script.js';11} from '../../../script.js';
12import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js';12import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js';
13import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from '../../popup.js';13import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
14import { updateReasoningUI } from '../../reasoning.js';14import { updateReasoningUI } from '../../reasoning.js';
15import { findSecret, secret_state, writeSecret } from '../../secrets.js';15import { secret_state } from '../../secrets.js';
16import { SlashCommand } from '../../slash-commands/SlashCommand.js';16import { SlashCommand } from '../../slash-commands/SlashCommand.js';
17import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';17import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
18import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';18import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
@@ -152,9 +152,9 @@ const LOCAL_URL = ['libre', 'oneringtranslator', 'deeplx', 'lingva'];
152function showKeysButton() {152function showKeysButton() {
153 const providerRequiresKey = KEY_REQUIRED.includes(extension_settings.translate.provider);153 const providerRequiresKey = KEY_REQUIRED.includes(extension_settings.translate.provider);
154 const providerOptionalUrl = LOCAL_URL.includes(extension_settings.translate.provider);154 const providerOptionalUrl = LOCAL_URL.includes(extension_settings.translate.provider);
155 $('#translate_key_button').toggle(providerRequiresKey);155 $('#translate_key_button').toggle(providerRequiresKey).data('key', extension_settings.translate.provider);
156 $('#translate_key_button').toggleClass('success', Boolean(secret_state[extension_settings.translate.provider]));156 $('#translate_key_button').toggleClass('success', Boolean(secret_state[extension_settings.translate.provider]));
157 $('#translate_url_button').toggle(providerOptionalUrl);157 $('#translate_url_button').toggle(providerOptionalUrl).data('key', extension_settings.translate.provider + '_url');
158 $('#translate_url_button').toggleClass('success', Boolean(secret_state[extension_settings.translate.provider + '_url']));158 $('#translate_url_button').toggleClass('success', Boolean(secret_state[extension_settings.translate.provider + '_url']));
159 $('#deepl_api_endpoint').toggle(extension_settings.translate.provider === 'deepl');159 $('#deepl_api_endpoint').toggle(extension_settings.translate.provider === 'deepl');
160}160}
@@ -749,63 +749,16 @@ jQuery(async () => {
749 saveSettingsDebounced();749 saveSettingsDebounced();
750 });750 });
751 $(document).on('click', '.mes_translate', onMessageTranslateClick);751 $(document).on('click', '.mes_translate', onMessageTranslateClick);
752 $('#translate_key_button').on('click', async () => {
753 const optionText = $('#translation_provider option:selected').text();
754 const key = await callGenericPopup(`<h3>${optionText} API Key</h3>`, POPUP_TYPE.INPUT, '', {
755 customButtons: [{
756 text: 'Remove Key',
757 appendAtEnd: true,
758 result: POPUP_RESULT.NEGATIVE,
759 action: async () => {
760 await writeSecret(extension_settings.translate.provider, '');
761 toastr.success('API Key removed');
762 $('#translate_key_button').toggleClass('success', !!secret_state[extension_settings.translate.provider]);
763 },
764 }],
765 });
766752
767 if (!key) {753 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach((eventType) => {
768 return;754 eventSource.on(eventType, (/** @type {string} */ key) => {
769 }755 if (key === extension_settings.translate.provider) {
770756 $('#translate_key_button').toggleClass('success', !!secret_state[extension_settings.translate.provider]);
771 await writeSecret(extension_settings.translate.provider, key);757 }
772 toastr.success('API Key saved');758 if (key === `${extension_settings.translate.provider}_url`) {
773 $('#translate_key_button').addClass('success');759 $('#translate_url_button').toggleClass('success', !!secret_state[`${extension_settings.translate.provider}_url`]);
774 });760 }
775 $('#translate_url_button').on('click', async () => {
776 const optionText = $('#translation_provider option:selected').text();
777 const exampleURLs = {
778 'libre': 'http://127.0.0.1:5000/translate',
779 'lingva': 'https://lingva.ml/api/v1',
780 'oneringtranslator': 'http://127.0.0.1:4990/translate',
781 'deeplx': 'http://127.0.0.1:1188/translate',
782 };
783 const popupText = `<h3>${optionText} API URL</h3><i>Example: <tt>${String(exampleURLs[extension_settings.translate.provider])}</tt></i>`;
784
785 const secretKey = extension_settings.translate.provider + '_url';
786 const savedUrl = secret_state[secretKey] ? await findSecret(secretKey) : '';
787
788 const url = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedUrl, {
789 customButtons: [{
790 text: 'Remove URL',
791 appendAtEnd: true,
792 result: POPUP_RESULT.NEGATIVE,
793 action: async () => {
794 await writeSecret(secretKey, '');
795 toastr.success('API URL removed');
796 $('#translate_url_button').toggleClass('success', !!secret_state[secretKey]);
797 },
798 }],
799 });761 });
800
801 if (!url) {
802 return;
803 }
804
805 await writeSecret(secretKey, url);
806
807 toastr.success('API URL saved');
808 $('#translate_url_button').addClass('success');
809 });762 });
810763
811 loadSettings();764 loadSettings();
public/scripts/extensions/tts/alltalk.js+9 -9
@@ -296,20 +296,20 @@ class AllTalkTtsProvider {
296 const ttsNarrateDialoguesCheckbox = document.getElementById('tts_narrate_dialogues');296 const ttsNarrateDialoguesCheckbox = document.getElementById('tts_narrate_dialogues');
297 if (this.settings.narrator_enabled) {297 if (this.settings.narrator_enabled) {
298 ttsPassAsterisksCheckbox.checked = false;298 ttsPassAsterisksCheckbox.checked = false;
299 $('#tts_pass_asterisks').click();299 $('#tts_pass_asterisks').trigger('click');
300 $('#tts_pass_asterisks').trigger('change');300 $('#tts_pass_asterisks').trigger('change');
301 }301 }
302 if (!this.settings.narrator_enabled) {302 if (!this.settings.narrator_enabled) {
303 ttsPassAsterisksCheckbox.checked = true;303 ttsPassAsterisksCheckbox.checked = true;
304 $('#tts_pass_asterisks').click();304 $('#tts_pass_asterisks').trigger('click');
305 $('#tts_pass_asterisks').trigger('change');305 $('#tts_pass_asterisks').trigger('change');
306 }306 }
307 if (this.settings.narrator_enabled) {307 if (this.settings.narrator_enabled) {
308 ttsNarrateQuotedCheckbox.checked = true;308 ttsNarrateQuotedCheckbox.checked = true;
309 ttsNarrateDialoguesCheckbox.checked = true;309 ttsNarrateDialoguesCheckbox.checked = true;
310 $('#tts_narrate_quoted').click();310 $('#tts_narrate_quoted').trigger('click');
311 $('#tts_narrate_quoted').trigger('change');311 $('#tts_narrate_quoted').trigger('change');
312 $('#tts_narrate_dialogues').click();312 $('#tts_narrate_dialogues').trigger('click');
313 $('#tts_narrate_dialogues').trigger('change');313 $('#tts_narrate_dialogues').trigger('change');
314 }314 }
315 atNarratorSelect.value = this.settings.narrator_enabled.toString();315 atNarratorSelect.value = this.settings.narrator_enabled.toString();
@@ -783,21 +783,21 @@ class AllTalkTtsProvider {
783783
784 if (narratorOption === 'true') {784 if (narratorOption === 'true') {
785 ttsPassAsterisksCheckbox.checked = false;785 ttsPassAsterisksCheckbox.checked = false;
786 $('#tts_pass_asterisks').click();786 $('#tts_pass_asterisks').trigger('click');
787 $('#tts_pass_asterisks').trigger('change');787 $('#tts_pass_asterisks').trigger('change');
788 ttsNarrateQuotedCheckbox.checked = true;788 ttsNarrateQuotedCheckbox.checked = true;
789 ttsNarrateDialoguesCheckbox.checked = true;789 ttsNarrateDialoguesCheckbox.checked = true;
790 $('#tts_narrate_quoted').click();790 $('#tts_narrate_quoted').trigger('click');
791 $('#tts_narrate_quoted').trigger('change');791 $('#tts_narrate_quoted').trigger('change');
792 $('#tts_narrate_dialogues').click();792 $('#tts_narrate_dialogues').trigger('click');
793 $('#tts_narrate_dialogues').trigger('change');793 $('#tts_narrate_dialogues').trigger('change');
794 } else if (narratorOption === 'silent') {794 } else if (narratorOption === 'silent') {
795 ttsPassAsterisksCheckbox.checked = false;795 ttsPassAsterisksCheckbox.checked = false;
796 $('#tts_pass_asterisks').click();796 $('#tts_pass_asterisks').trigger('click');
797 $('#tts_pass_asterisks').trigger('change');797 $('#tts_pass_asterisks').trigger('change');
798 } else {798 } else {
799 ttsPassAsterisksCheckbox.checked = true;799 ttsPassAsterisksCheckbox.checked = true;
800 $('#tts_pass_asterisks').click();800 $('#tts_pass_asterisks').trigger('click');
801 $('#tts_pass_asterisks').trigger('change');801 $('#tts_pass_asterisks').trigger('change');
802 }802 }
803803
public/scripts/extensions/tts/azure.js+20 -32
@@ -1,6 +1,5 @@
1import { getRequestHeaders } from '../../../script.js';1import { event_types, eventSource, getRequestHeaders } from '../../../script.js';
2import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from '../../popup.js';2import { SECRET_KEYS, secret_state } from '../../secrets.js';
3import { SECRET_KEYS, findSecret, secret_state, writeSecret } from '../../secrets.js';
4import { getPreviewString, saveTtsProviderSettings } from './index.js';3import { getPreviewString, saveTtsProviderSettings } from './index.js';
5export { AzureTtsProvider };4export { AzureTtsProvider };
65
@@ -26,7 +25,7 @@ class AzureTtsProvider {
26 <h4 for="azure_tts_key" class="flex1 margin0">25 <h4 for="azure_tts_key" class="flex1 margin0">
27 <a href="https://portal.azure.com/" target="_blank">Azure TTS Key</a>26 <a href="https://portal.azure.com/" target="_blank">Azure TTS Key</a>
28 </h4>27 </h4>
29 <div id="azure_tts_key" class="menu_button menu_button_icon">28 <div id="azure_tts_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_azure_tts">
30 <i class="fa-solid fa-key"></i>29 <i class="fa-solid fa-key"></i>
31 <span>Click to set</span>30 <span>Click to set</span>
32 </div>31 </div>
@@ -39,6 +38,20 @@ class AzureTtsProvider {
39 return html;38 return html;
40 }39 }
4140
41 constructor() {
42 this.handler = async function (/** @type {string} */ key) {
43 if (key !== SECRET_KEYS.AZURE_TTS) return;
44 $('#azure_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.AZURE_TTS]);
45 await this.onRefreshClick();
46 }.bind(this);
47 }
48
49 dispose() {
50 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
51 eventSource.removeListener(event, this.handler);
52 });
53 }
54
42 onSettingsChange() {55 onSettingsChange() {
43 // Update dynamically56 // Update dynamically
44 this.settings.region = String($('#azure_tts_region').val());57 this.settings.region = String($('#azure_tts_region').val());
@@ -65,34 +78,9 @@ class AzureTtsProvider {
65 }78 }
6679
67 $('#azure_tts_region').val(this.settings.region).on('input', () => this.onSettingsChange());80 $('#azure_tts_region').val(this.settings.region).on('input', () => this.onSettingsChange());
68 $('#azure_tts_key').toggleClass('success', secret_state[SECRET_KEYS.AZURE_TTS]);81 $('#azure_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.AZURE_TTS]);
69 $('#azure_tts_key').on('click', async () => {82 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
70 const popupText = 'Azure TTS API Key';83 eventSource.on(event, this.handler);
71 const savedKey = secret_state[SECRET_KEYS.AZURE_TTS] ? await findSecret(SECRET_KEYS.AZURE_TTS) : '';
72
73 const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedKey, {
74 customButtons: [{
75 text: 'Remove Key',
76 appendAtEnd: true,
77 result: POPUP_RESULT.NEGATIVE,
78 action: async () => {
79 await writeSecret(SECRET_KEYS.AZURE_TTS, '');
80 $('#azure_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.AZURE_TTS]);
81 toastr.success('API Key removed');
82 await this.onRefreshClick();
83 },
84 }],
85 });
86
87 if (!key) {
88 return;
89 }
90
91 await writeSecret(SECRET_KEYS.AZURE_TTS, String(key));
92
93 toastr.success('API Key saved');
94 $('#azure_tts_key').addClass('success');
95 await this.onRefreshClick();
96 });84 });
9785
98 try {86 try {
public/scripts/extensions/tts/elevenlabs.js+26 -5
@@ -16,8 +16,9 @@ class ElevenLabsTtsProvider {
16 similarity_boost: 0.75,16 similarity_boost: 0.75,
17 style_exaggeration: 0.00,17 style_exaggeration: 0.00,
18 speaker_boost: true,18 speaker_boost: true,
19 speed: 1.0,
19 apiKey: '',20 apiKey: '',
20 model: 'eleven_monolingual_v1',21 model: 'eleven_turbo_v2_5',
21 voiceMap: {},22 voiceMap: {},
22 };23 };
2324
@@ -28,16 +29,23 @@ class ElevenLabsTtsProvider {
28 <input id="elevenlabs_tts_api_key" type="text" class="text_pole" placeholder="<API Key>"/>29 <input id="elevenlabs_tts_api_key" type="text" class="text_pole" placeholder="<API Key>"/>
29 <label for="elevenlabs_tts_model">Model</label>30 <label for="elevenlabs_tts_model">Model</label>
30 <select id="elevenlabs_tts_model" class="text_pole">31 <select id="elevenlabs_tts_model" class="text_pole">
31 <option value="eleven_monolingual_v1">English v1</option>32 <option value="eleven_v3">Eleven v3</option>
32 <option value="eleven_multilingual_v1">Multilingual v1</option>33 <option value="eleven_ttv_v3">Eleven ttv v3</option>
33 <option value="eleven_multilingual_v2">Multilingual v2</option>34 <option value="eleven_multilingual_v2">Multilingual v2</option>
34 <option value="eleven_turbo_v2">Turbo v2</option>35 <option value="eleven_flash_v2_5">Eleven Flash v2.5</option>
36 <option value="eleven_turbo_v2_5">Turbo v2.5</option>
37 <option value="eleven_multilingual_ttv_v2">Multilingual ttv v2</option>
38 <option value="eleven_monolingual_v1">English v1 (Old)</option>
39 <option value="eleven_multilingual_v1">Multilingual v1 (Old)</option>
40 <option value="eleven_turbo_v2">Turbo v2 (Old)</option>
35 </select>41 </select>
36 <input id="eleven_labs_connect" class="menu_button" type="button" value="Connect" />42 <input id="eleven_labs_connect" class="menu_button" type="button" value="Connect" />
37 <label for="elevenlabs_tts_stability">Stability: <span id="elevenlabs_tts_stability_output"></span></label>43 <label for="elevenlabs_tts_stability">Stability: <span id="elevenlabs_tts_stability_output"></span></label>
38 <input id="elevenlabs_tts_stability" type="range" value="${this.defaultSettings.stability}" min="0" max="1" step="0.01" />44 <input id="elevenlabs_tts_stability" type="range" value="${this.defaultSettings.stability}" min="0" max="1" step="0.01" />
39 <label for="elevenlabs_tts_similarity_boost">Similarity Boost: <span id="elevenlabs_tts_similarity_boost_output"></span></label>45 <label for="elevenlabs_tts_similarity_boost">Similarity Boost: <span id="elevenlabs_tts_similarity_boost_output"></span></label>
40 <input id="elevenlabs_tts_similarity_boost" type="range" value="${this.defaultSettings.similarity_boost}" min="0" max="1" step="0.01" />46 <input id="elevenlabs_tts_similarity_boost" type="range" value="${this.defaultSettings.similarity_boost}" min="0" max="1" step="0.01" />
47 <label for="elevenlabs_tts_speed">Speed: <span id="elevenlabs_tts_speed_output"></span></label>
48 <input id="elevenlabs_tts_speed" type="range" value="${this.defaultSettings.speed}" min="0.7" max="1.2" step="0.01" />
41 <div id="elevenlabs_tts_v2_options" style="display: none;">49 <div id="elevenlabs_tts_v2_options" style="display: none;">
42 <label for="elevenlabs_tts_style_exaggeration">Style Exaggeration: <span id="elevenlabs_tts_style_exaggeration_output"></span></label>50 <label for="elevenlabs_tts_style_exaggeration">Style Exaggeration: <span id="elevenlabs_tts_style_exaggeration_output"></span></label>
43 <input id="elevenlabs_tts_style_exaggeration" type="range" value="${this.defaultSettings.style_exaggeration}" min="0" max="1" step="0.01" />51 <input id="elevenlabs_tts_style_exaggeration" type="range" value="${this.defaultSettings.style_exaggeration}" min="0" max="1" step="0.01" />
@@ -65,7 +73,14 @@ class ElevenLabsTtsProvider {
65 }73 }
6674
67 shouldInvolveExtendedSettings() {75 shouldInvolveExtendedSettings() {
68 return this.settings.model === 'eleven_multilingual_v2';76 // Models that support extended settings (style_exaggeration, speaker_boost)
77 const modelsWithExtendedSettings = [
78 'eleven_v3',
79 'eleven_ttv_v3',
80 'eleven_multilingual_v2',
81 'eleven_multilingual_ttv_v2',
82 ];
83 return modelsWithExtendedSettings.includes(this.settings.model);
69 }84 }
7085
71 onSettingsChange() {86 onSettingsChange() {
@@ -74,10 +89,12 @@ class ElevenLabsTtsProvider {
74 this.settings.similarity_boost = $('#elevenlabs_tts_similarity_boost').val();89 this.settings.similarity_boost = $('#elevenlabs_tts_similarity_boost').val();
75 this.settings.style_exaggeration = $('#elevenlabs_tts_style_exaggeration').val();90 this.settings.style_exaggeration = $('#elevenlabs_tts_style_exaggeration').val();
76 this.settings.speaker_boost = $('#elevenlabs_tts_speaker_boost').is(':checked');91 this.settings.speaker_boost = $('#elevenlabs_tts_speaker_boost').is(':checked');
92 this.settings.speed = $('#elevenlabs_tts_speed').val();
77 this.settings.model = $('#elevenlabs_tts_model').find(':selected').val();93 this.settings.model = $('#elevenlabs_tts_model').find(':selected').val();
78 $('#elevenlabs_tts_stability_output').text(Math.round(this.settings.stability * 100) + '%');94 $('#elevenlabs_tts_stability_output').text(Math.round(this.settings.stability * 100) + '%');
79 $('#elevenlabs_tts_similarity_boost_output').text(Math.round(this.settings.similarity_boost * 100) + '%');95 $('#elevenlabs_tts_similarity_boost_output').text(Math.round(this.settings.similarity_boost * 100) + '%');
80 $('#elevenlabs_tts_style_exaggeration_output').text(Math.round(this.settings.style_exaggeration * 100) + '%');96 $('#elevenlabs_tts_style_exaggeration_output').text(Math.round(this.settings.style_exaggeration * 100) + '%');
97 $('#elevenlabs_tts_speed_output').text(this.settings.speed + 'x');
81 $('#elevenlabs_tts_v2_options').toggle(this.shouldInvolveExtendedSettings());98 $('#elevenlabs_tts_v2_options').toggle(this.shouldInvolveExtendedSettings());
82 saveTtsProviderSettings();99 saveTtsProviderSettings();
83 }100 }
@@ -109,6 +126,7 @@ class ElevenLabsTtsProvider {
109 $('#elevenlabs_tts_similarity_boost').val(this.settings.similarity_boost);126 $('#elevenlabs_tts_similarity_boost').val(this.settings.similarity_boost);
110 $('#elevenlabs_tts_style_exaggeration').val(this.settings.style_exaggeration);127 $('#elevenlabs_tts_style_exaggeration').val(this.settings.style_exaggeration);
111 $('#elevenlabs_tts_speaker_boost').prop('checked', this.settings.speaker_boost);128 $('#elevenlabs_tts_speaker_boost').prop('checked', this.settings.speaker_boost);
129 $('#elevenlabs_tts_speed').val(this.settings.speed);
112 $('#elevenlabs_tts_api_key').val(this.settings.apiKey);130 $('#elevenlabs_tts_api_key').val(this.settings.apiKey);
113 $('#elevenlabs_tts_model').val(this.settings.model);131 $('#elevenlabs_tts_model').val(this.settings.model);
114 $('#eleven_labs_connect').on('click', () => { this.onConnectClick(); });132 $('#eleven_labs_connect').on('click', () => { this.onConnectClick(); });
@@ -116,10 +134,12 @@ class ElevenLabsTtsProvider {
116 $('#elevenlabs_tts_stability').on('input', this.onSettingsChange.bind(this));134 $('#elevenlabs_tts_stability').on('input', this.onSettingsChange.bind(this));
117 $('#elevenlabs_tts_style_exaggeration').on('input', this.onSettingsChange.bind(this));135 $('#elevenlabs_tts_style_exaggeration').on('input', this.onSettingsChange.bind(this));
118 $('#elevenlabs_tts_speaker_boost').on('change', this.onSettingsChange.bind(this));136 $('#elevenlabs_tts_speaker_boost').on('change', this.onSettingsChange.bind(this));
137 $('#elevenlabs_tts_speed').on('input', this.onSettingsChange.bind(this));
119 $('#elevenlabs_tts_model').on('change', this.onSettingsChange.bind(this));138 $('#elevenlabs_tts_model').on('change', this.onSettingsChange.bind(this));
120 $('#elevenlabs_tts_stability_output').text(Math.round(this.settings.stability * 100) + '%');139 $('#elevenlabs_tts_stability_output').text(Math.round(this.settings.stability * 100) + '%');
121 $('#elevenlabs_tts_similarity_boost_output').text(Math.round(this.settings.similarity_boost * 100) + '%');140 $('#elevenlabs_tts_similarity_boost_output').text(Math.round(this.settings.similarity_boost * 100) + '%');
122 $('#elevenlabs_tts_style_exaggeration_output').text(Math.round(this.settings.style_exaggeration * 100) + '%');141 $('#elevenlabs_tts_style_exaggeration_output').text(Math.round(this.settings.style_exaggeration * 100) + '%');
142 $('#elevenlabs_tts_speed_output').text(this.settings.speed + 'x');
123 $('#elevenlabs_tts_v2_options').toggle(this.shouldInvolveExtendedSettings());143 $('#elevenlabs_tts_v2_options').toggle(this.shouldInvolveExtendedSettings());
124 try {144 try {
125 await this.checkReady();145 await this.checkReady();
@@ -308,6 +328,7 @@ class ElevenLabsTtsProvider {
308 voice_settings: {328 voice_settings: {
309 stability: Number(this.settings.stability),329 stability: Number(this.settings.stability),
310 similarity_boost: Number(this.settings.similarity_boost),330 similarity_boost: Number(this.settings.similarity_boost),
331 speed: Number(this.settings.speed),
311 },332 },
312 };333 };
313 if (this.shouldInvolveExtendedSettings()) {334 if (this.shouldInvolveExtendedSettings()) {
public/scripts/extensions/tts/index.js+3 -1
@@ -31,6 +31,7 @@ import { GoogleNativeTtsProvider } from './google-native.js';
31import { ChatterboxTtsProvider } from './chatterbox.js';31import { ChatterboxTtsProvider } from './chatterbox.js';
32import { KokoroTtsProvider } from './kokoro.js';32import { KokoroTtsProvider } from './kokoro.js';
33import { TtsWebuiProvider } from './tts-webui.js';33import { TtsWebuiProvider } from './tts-webui.js';
34import { PollinationsTtsProvider } from './pollinations.js';
3435
35const UPDATE_INTERVAL = 1000;36const UPDATE_INTERVAL = 1000;
36const wrapper = new ModuleWorkerWrapper(moduleWorker);37const wrapper = new ModuleWorkerWrapper(moduleWorker);
@@ -129,6 +130,7 @@ const ttsProviders = {
129 Novel: NovelTtsProvider,130 Novel: NovelTtsProvider,
130 OpenAI: OpenAITtsProvider,131 OpenAI: OpenAITtsProvider,
131 'OpenAI Compatible': OpenAICompatibleTtsProvider,132 'OpenAI Compatible': OpenAICompatibleTtsProvider,
133 Pollinations: PollinationsTtsProvider,
132 SBVits2: SBVits2TtsProvider,134 SBVits2: SBVits2TtsProvider,
133 Silero: SileroTtsProvider,135 Silero: SileroTtsProvider,
134 SpeechT5: SpeechT5TtsProvider,136 SpeechT5: SpeechT5TtsProvider,
@@ -866,7 +868,7 @@ async function onMessageEvent(messageId, lastCharIndex) {
866868
867 console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`);869 console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`);
868870
869 if (extension_settings.tts.periodic_auto_generation) {871 if (extension_settings.tts.periodic_auto_generation && isStreamingEnabled()) {
870 ttsJobQueue.push(message);872 ttsJobQueue.push(message);
871 } else {873 } else {
872 processAndQueueTtsMessage(message);874 processAndQueueTtsMessage(message);
public/scripts/extensions/tts/novel.js+18 -12
@@ -132,9 +132,14 @@ class NovelTtsProvider {
132 return { name: voiceName, voice_id: voiceName, lang: 'en-US', preview_url: false };132 return { name: voiceName, voice_id: voiceName, lang: 'en-US', preview_url: false };
133 }133 }
134134
135 async generateTts(text, voiceId) {135 /**
136 const response = await this.fetchTtsGeneration(text, voiceId);136 * Generate TTS audio for the given text using the specified voice.
137 return response;137 * @param {string} text Text to generate
138 * @param {string} voiceId Voice ID
139 * @returns {AsyncGenerator<Response>} Audio response generator
140 */
141 generateTts(text, voiceId) {
142 return this.fetchTtsGeneration(text, voiceId);
138 }143 }
139144
140 //###########//145 //###########//
@@ -172,16 +177,17 @@ class NovelTtsProvider {
172 this.audioElement.currentTime = 0;177 this.audioElement.currentTime = 0;
173178
174 const text = getPreviewString('en-US');179 const text = getPreviewString('en-US');
175 const response = await this.fetchTtsGeneration(text, id);180 for await (const response of this.generateTts(text, id)) {
176 if (!response.ok) {181 const audio = await response.blob();
177 throw new Error(`HTTP ${response.status}`);182 const url = URL.createObjectURL(audio);
183 await new Promise(resolve => {
184 const audioElement = new Audio();
185 audioElement.src = url;
186 audioElement.play();
187 audioElement.onended = () => resolve();
188 });
189 URL.revokeObjectURL(url);
178 }190 }
179
180 const audio = await response.blob();
181 const url = URL.createObjectURL(audio);
182 this.audioElement.src = url;
183 this.audioElement.play();
184 this.audioElement.onended = () => URL.revokeObjectURL(url);
185 }191 }
186192
187 async* fetchTtsGeneration(inputText, voiceId) {193 async* fetchTtsGeneration(inputText, voiceId) {
public/scripts/extensions/tts/openai-compatible.js+17 -4
@@ -1,6 +1,5 @@
1import { getRequestHeaders } from '../../../script.js';1import { event_types, eventSource, getRequestHeaders } from '../../../script.js';
2import { callGenericPopup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js';2import { SECRET_KEYS, secret_state } from '../../secrets.js';
3import { findSecret, SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
4import { getPreviewString, saveTtsProviderSettings } from './index.js';3import { getPreviewString, saveTtsProviderSettings } from './index.js';
54
6export { OpenAICompatibleTtsProvider };5export { OpenAICompatibleTtsProvider };
@@ -27,7 +26,7 @@ class OpenAICompatibleTtsProvider {
27 <div class="flex1">26 <div class="flex1">
28 <input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="500" value="${this.defaultSettings.provider_endpoint}"/>27 <input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="500" value="${this.defaultSettings.provider_endpoint}"/>
29 </div>28 </div>
30 <div id="openai_compatible_tts_key" class="menu_button menu_button_icon">29 <div id="openai_compatible_tts_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_custom_openai_tts">
31 <i class="fa-solid fa-key"></i>30 <i class="fa-solid fa-key"></i>
32 <span>API Key</span>31 <span>API Key</span>
33 </div>32 </div>
@@ -41,6 +40,20 @@ class OpenAICompatibleTtsProvider {
41 return html;40 return html;
42 }41 }
4342
43 constructor() {
44 this.handler = async function (/** @type {string} */ key) {
45 if (key !== SECRET_KEYS.CUSTOM_OPENAI_TTS) return;
46 $('#openai_compatible_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS]);
47 await this.onRefreshClick();
48 }.bind(this);
49 }
50
51 dispose() {
52 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
53 eventSource.removeListener(event, this.handler);
54 });
55 }
56
44 async loadSettings(settings) {57 async loadSettings(settings) {
45 // Populate Provider UI given input settings58 // Populate Provider UI given input settings
46 if (Object.keys(settings).length == 0) {59 if (Object.keys(settings).length == 0) {
@@ -74,34 +87,9 @@ class OpenAICompatibleTtsProvider {
7487
public/scripts/extensions/tts/pollinations.js+0 -0
public/scripts/extensions/vectors/index.js+0 -0
public/scripts/extensions/vectors/settings.html+0 -0
public/scripts/group-chats.js+0 -0
public/scripts/horde.js+0 -0
public/scripts/instruct-mode.js+0 -0
public/scripts/itemized-prompts.js+0 -0
public/scripts/kai-settings.js+0 -0
public/scripts/nai-settings.js+0 -0
public/scripts/openai.js+0 -0
public/scripts/personas.js+0 -0
public/scripts/popup.js+0 -0
public/scripts/power-user.js+0 -0
public/scripts/preset-manager.js+0 -0
public/scripts/secrets.js+0 -0
public/scripts/server-history.js+0 -0
public/scripts/slash-commands.js+0 -0
public/scripts/slash-commands/SlashCommandParser.js+0 -0
public/scripts/st-context.js+0 -0
public/scripts/system-messages.js+0 -0
public/scripts/tags.js+0 -0
public/scripts/templates/importCharacters.html+0 -0
public/scripts/textgen-models.js+0 -0
public/scripts/textgen-settings.js+0 -0
public/scripts/tokenizers.js+0 -0
public/scripts/utils.js+0 -0
public/scripts/variables.js+0 -0
public/scripts/welcome-screen.js+0 -0
public/scripts/world-info.js+0 -0
public/style.css+0 -0
server.js+0 -0
src/byaf.js+0 -0
src/command-line.js+0 -0
src/config-init.js+0 -0
src/constants.js+0 -0
src/electron/index.js+0 -0
src/endpoints/avatars.js+0 -0
src/endpoints/backends/chat-completions.js+0 -0
src/endpoints/backends/scale-alt.js+0 -0
src/endpoints/characters.js+0 -0
src/endpoints/content-manager.js+0 -0
src/endpoints/data-maid.js+0 -0
src/endpoints/extensions.js+0 -0
src/endpoints/groups.js+0 -0
src/endpoints/images.js+0 -0
src/endpoints/moving-ui.js+0 -0
src/endpoints/openrouter.js+0 -0
src/endpoints/presets.js+0 -0
src/endpoints/quick-replies.js+0 -0
src/endpoints/secrets.js+0 -0
src/endpoints/speech.js+0 -0
src/endpoints/sprites.js+0 -0
src/endpoints/stable-diffusion.js+0 -0
src/endpoints/themes.js+0 -0
src/endpoints/thumbnails.js+0 -0
src/endpoints/tokenizers.js+0 -0
src/endpoints/vectors.js+0 -0
src/endpoints/worldinfo.js+0 -0
src/middleware/accessLogWriter.js+0 -0
src/middleware/cacheBuster.js+0 -0
src/middleware/validateFileName.js+0 -0
src/prompt-converters.js+0 -0
src/server-global.js+0 -0
src/server-main.js+0 -0
src/server-startup.js+0 -0
src/types/byaf.d.ts+0 -0
src/types/spec-v2.d.ts+0 -0
src/users.js+0 -0
src/util.js+0 -0
src/vectors/google-vectors.js+0 -0
src/vectors/makersuite-vectors.js+0 -0
webpack.config.js+0 -0
Diff truncated