Merge pull request #4316 from SillyTavern/staging Staging

9a191c41e8b8fa7d203974c0b0debdfe1146a7a0

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

Signed
147 files changed, +2034 -5026Showing whitespace changes
.github/readme-de_de.md+12 -301
@@ -1,6 +1,5 @@
11> [!IMPORTANT]
22> 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
54<a name="readme-top"></a>
65
@@ -11,7 +10,7 @@
1110[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
1312[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
1413[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/networkforks)
1514[![GitHub Issues](https://img.shields.io/github/issues/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/issues)
1615[![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
2827SillyTavern (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
3029Angefangen im Februar 2023 als Fork von TavernAI 1.2.8 hat SillyTavern nun über 100200 Mitwirkende und 2 Jahre unabhängiger Entwicklung hinter sich und dient weiterhin als führende Software für versierte KI-Hobbyisten.
3130
3231## Unsere Vision
3332
@@ -35,29 +34,9 @@ Angefangen im Februar 2023 als Fork von TavernAI 1.2.8 hat SillyTavern nun über
35342. Wir bieten weder Online- oder gehosteten Dienste an, noch verfolgen wir programmgesteuert Benutzerdaten.
36353. 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-## Branches
37+## Brauche ich einen leistungsstarken PC, um SillyTavern auszuführen?
3938
40-SillyTavern wird mit einem Branchsystem entwickelt, um ein reibungsloses Erlebnis für alle Nutzer zu gewährleisten.
39+Die 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-
45-Wenn 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-
49-Da 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-
53-Die 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
6241## Fragen oder Vorschläge?
6342
@@ -83,287 +62,18 @@ Oder nimm direkt Kontakt mit den Entwicklern auf:
8362<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">
8463<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">
8564
8665## CharakterkartenInstallation
87-
88-SillyTavern 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-
90-Das 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-
92-Um 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-
94-Um 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-
108-SillyTavern 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-
118-Tutorials 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-
132-1. Installiere [NodeJS](https://nodejs.org/en) (die neueste LTS-Version wird empfohlen).
133-2. Installiere [Git für Windows](https://gitforwindows.org/).
134-3. Öffne den Windows-Explorer (`Win+E`).
135-4. Gehe zu oder erstelle einen Ordner, der nicht von Windows kontrolliert oder überwacht wird. (z.B.: C:\MySpecialFolder\)
136-5. Öffne ein Eingabeaufforderungsfenster in diesem Ordner, indem du in die 'Adressleiste' oben klickst, `cmd` eingibst und Enter drückst.
137-6. 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-
142-7. Sobald alles geklont ist, doppelklicke auf `Start.bat`, damit NodeJS seine Anforderungen installiert.
143-8. 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-
149-1. Installiere [NodeJS](https://nodejs.org/en) (die neueste LTS-Version wird empfohlen).
150-2. Installiere [GitHub Desktop](https://central.github.com/deployments/desktop/desktop/latest/win32).
151-3. 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).
152-4. 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.
153-5. 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`.
154-6. 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.)
155-7. Nach dem Doppelklicken sollte ein großes schwarzes Konsolenfenster erscheinen, und SillyTavern beginnt, das zu installieren, was es zum Betrieb benötigt.
156-8. Nach dem Installationsprozess sollte das Konsolenfenster so aussehen, und ein SillyTavern-Tab sollte in deinem Browser geöffnet sein.
157-9. Verbinde dich mit einer der [unterstützten APIs](https://docs.sillytavern.app/usage/api-connections/) und beginne zu chatten!
158-
159-## 🐧 Linux & 🍎 MacOS
160-
161-Für MacOS/Linux werden all diese Schritte in einem Terminal durchgeführt.
162-
163-1. Installiere git und nodeJS (die Methode zur Durchführung hängt von deinem Betriebssystem ab).
164-2. 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-
169-3. `cd SillyTavern`, um in den Installationsordner zu navigieren.
170-4. 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-
177-Der 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-
181-1. 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
184-cmd /c winget install -e --id Git.Git
185-```
186-
187-2. 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
190-git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher && start installer.bat
191-```
192-
193-### Für Linux-Nutzer
194-
195-1. Öffne dein bevorzugtes Terminal und installiere git.
196-2. Klone den SillyTavern-Launcher mit:
197-
198-```shell
199-git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
200-```
201-
202-3. Starte die installer.sh mit:
203-
204-```shell
205-chmod +x install.sh && ./install.sh
206-```
207-
208-4. Nach der Installation starte die launcher.sh mit:
209-
210-```shell
211-chmod +x launcher.sh && ./launcher.sh
212-```
213-
214-### Für Mac-Nutzer
215-
216-1. Ö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-
222-2. Installiere git mit:
223-
224-```shell
225-brew install git
226-```
227-
228-3. Klone den SillyTavern-Launcher mit:
229-
230-```shell
231-git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
232-```
233-
234-4. Starte die installer.sh mit:
235-
236-```shell
237-chmod +x install.sh && ./install.sh
238-```
239-
240-5. Nach der Installation starte die launcher.sh mit:
241-
242-```shell
243-chmod +x launcher.sh && ./launcher.sh
244-```
245-
246-## 🐋 Installation über Docker
247-
248-Diese 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-
252-Wir 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-
256-Du 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-
277-1. Öffne deine Befehlszeile.
278-2. 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-
297-SillyTavern speichert deine API-Schlüssel in einer Datei `secrets.json` im Benutzerdatenverzeichnis (`/data/default-user/secrets.json` ist der Standardpfad).
298-
299-Standardmäßig sind API-Schlüssel nach dem Speichern und Aktualisieren der Seite nicht mehr über die Benutzeroberfläche sichtbar.
300-
301-Um die Ansicht deiner Schlüssel zu aktivieren:
302-
303-1. Setze den Wert von `allowKeysExposure` auf `true` in der Datei `config.yaml`.
304-2. Starte den SillyTavern-Server neu.
305-3. Klicke auf den Link 'Verborgene API-Schlüssel anzeigen' in der unteren rechten Ecke des API-Verbindungsfeldes.
306-
307-## Befehlszeilenargumente
308-
309-Sie können Befehlszeilenargumente an den Start des SillyTavern-Servers übergeben, um einige Einstellungen in `config.yaml` zu überschreiben.
310-
311-### Beispiele
312-
313-```shell
314-node server.js --port 8000 --listen false
315-# oder
316-npm run start -- --port 8000 --listen false
317-# oder (nur Windows)
318-Start.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-
349-Dies 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-
351-Lies die ausführliche Anleitung zum Einrichten von Remoteverbindungen in den [Docs](https://docs.sillytavern.app/usage/remoteconnections/).
352-
353-Mö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?
67+Für detaillierte Installationsanweisungen besuche bitte unsere Dokumentation:
35668
357-1. 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/)**
358-2. 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/)**
359-3. 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
36174## 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.**
364-aber OHNE JEGLICHE GARANTIE; nicht einmal die stillschweigende Garantie der
365-MARKTFÄHIGKEIT oder EIGNUNG FÜR EINEN BESTIMMTEN ZWECK. Siehe die
366-GNU Affero General Public License für weitere Details.**
36777
36878* [TavernAI](https://github.com/TavernAI/TavernAI) 1.2.8 von Humi: MIT-Lizenz
36979* Teile von CncAnons TavernAITurbo-Mod werden mit Genehmigung verwendet
@@ -372,6 +82,7 @@ GNU Affero General Public License für weitere Details.**
37282* Symboldesign von Font Awesome <https://fontawesome.com> (Symbole: CC BY 4.0, Schriftarten: SIL OFL 1.1, Code: MIT-Lizenz)
37383* Standardinhalt von @OtisAlejandro (Seraphina-Charakter und Lorebook) und @kallmeflocc (10.000 Discord-Benutzer-Feierhintergrund)
37484* 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
37687## Top Contributors
37788
.github/readme-ja_jp.md+48 -284
@@ -1,6 +1,8 @@
11> [!IMPORTANT]
22> ここに掲載されている情報は、古かったり不完全であったりする可能性があります。最新の情報は英語版をご利用ください。
33
4+<a name="readme-top"></a>
5+
46![][cover]
57
68<div align="center">
@@ -8,7 +10,7 @@
810[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
1012[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
1113[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/networkforks)
1214[![GitHub Issues](https://img.shields.io/github/issues/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/issues)
1315[![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
1719---
1820
1921モバイルデバイスにも適したレイアウト・マルチAPISillyTavernは、多くのLLM API(KoboldAI/CPP、Horde、NovelAI、Ooba、Tabby、OpenAI、OpenRouter、Claude、ScaleMistralなどに対応した統一インターフェースVN ライクなモバイルフレンドリーなレイアウト、ビジュアルノベルモード、Automatic1111 Waifu& モード、StableComfyUI DiffusionAPI画像生成連携、TTS、WorldInfo(伝承本)、カスタマイズ可能なUI、自動翻訳、大量のプロンプトオプション+サードパーティの拡張機能をインストールする機能必要以上に豊富なプロンプトオプション、そしてサードパーティ製拡張機能による無限の成長可能性を提供します。
20-
21-[TavernAI](https://github.com/TavernAI/TavernAI) v.1.2.8 のフォークに基づいています。
22-
23-## 重要事項
24-
25-1. 私たちは[ドキュメントウェブサイト](https://docs.sillytavern.app/) を作成し、ほとんどの質問にお答えしています。
26-
27-2. アップデートしたら使っていた拡張機能を見失いましたか?リリースバージョン 1.10.6 以降、これまで内蔵されていた拡張機能のほとんどがダウンロード可能なアドオンに変更されました。ダウンロードは、拡張機能パネル(トップバーのスタックドブロックアイコン)にある内蔵の "Download Extensions and Assets" メニューから行えます。
28-
29-### Cohee、RossAscends、SillyTavern コミュニティがお届けします
30-
31-### SillyTavern または TavernAI とは何ですか?
32-
33-SillyTavern は、あなたのコンピュータ(および Android スマホ)にインストールできるユーザーインターフェイスで、テキスト生成 AI と対話したり、あなたやコミュニティが作成したキャラクターとチャットやロールプレイをすることができます。
34-
35-SillyTavern は TavernAI 1.2.8 のフォークで、より活発な開発が行われており、多くの主要な機能が追加されています。現時点では、これらは完全に独立したプログラムと考えることができます。
36-
37-### ブランチ
38-
39-SillyTavern は、すべてのユーザーにスムーズな体験を保証するために、2 つのブランチシステムを使用して開発されています。
4022
41-* release -🌟 **ほとんどのユーザーにお勧め。** これは最も安定した推奨ブランチで、メジャーリリースがプッシュされた時のみ更新されます。大半のユーザーに適しています。
23+私たちは[ドキュメントウェブサイト](https://docs.sillytavern.app/)を用意しており、ほとんどの質問に答え、入門の手助けをします。
42-* staging - ⚠️ **カジュアルな使用にはお勧めしない。** このブランチには最新の機能がありますが、いつ壊れるかわからないので注意してください。パワーユーザーとマニア向けです。
4324
44-git CLI の使い方に慣れていなかったり、ブランチが何なのかわからなかったりしても、心配はいりません!releaseブランチが良い選択肢となります。
25+## SillyTavernとは?
4526
46-### Tavern 以外に何が必要ですか?
27+SillyTavern(略してST)は、テキスト生成LLM、画像生成エンジン、TTS音声モデルと対話するための、ローカルにインストールされるユーザーインターフェースです。
4728
48-Tavern は単なるユーザーインターフェイスなので、それだけでは何もできません。ロールプレイキャラクターとして機能する AI システムのバックエンドにアクセスする必要があります。様々なバックエンドがサポートされています。リストはこちらです: OpenAPI API (GPT)、KoboldAI (ローカルまたは Google Colab 上で動作)、その他。詳しくは [FAQ](https://docs.sillytavern.app/usage/faq/) をご覧ください。
29+2023年2月にTavernAI 1.2.8のフォークとして始まり、SillyTavernは現在200人以上の貢献者と2年間の独立した開発を経て、知識豊富なAI愛好家のための主要なソフトウェアとして機能し続けています。
4930
50-### Tavern を実行するには、ハイスペックなPCが必要ですか?
31+## 私たちのビジョン
5132
52-Tavern は単なるフロントエンドのUIであり、必要なハードウェアはごくわずかです。パワフルである必要があるのは、AI システムのバックエンドです。
33+1. 私たちは、ユーザーにできるだけ多くの実用性とLLMプロンプトの制御権限を与えることを目指しています。急な学習曲線も楽しみの一部です!
34+2. 私たちはオンラインサービスやホストされたサービスを提供せず、プログラム的にユーザーデータを追跡することもありません。
35+3. SillyTavernは、熱心なLLM愛好家のコミュニティによってもたらされた情熱的なプロジェクトであり、常に無料でオープンソースです。
5336
5437## モバイルサポートSillyTavernを実行するには強力なPCが必要ですか?
5538
56-> **注**
39+ハードウェア要件は最小限です。NodeJS 18以上を実行できるものであれば何でも動作します。ローカルマシンでLLM推論を行う場合は、少なくとも6GBのVRAMを搭載した3000シリーズのNVIDIAグラフィックスカードを推奨しますが、実際の要件は使用するモデルやバックエンドによって異なる場合があります。
5740
58-> **このフォークは Termux を使って Android スマホで実行できます。ArroganceComplex#2659 のガイドを参照してください:**
41+## 質問や提案はありますか?
5942
60-<https://rentry.org/STAI-Termux>
43+### Discordサーバー
6144
62-## ご質問やご提案
45+| [![][discord-shield-badge]][discord-link] | [私たちのDiscordコミュニティに参加してください!](https://discord.gg/sillytavern) サポートを受けたり、お気に入りのキャラクターやプロンプトを共有したりできます。 |
46+| :---------------------------------------- | :----------------------------------------------------------------------------------------------------------------- |
6347
64-### コミュニティ Discord サーバーを開設しました
48+または、開発者に直接連絡してください:
6549
66-サポートを受けたり、作成したキャラを共有したり、コミュニティの作ったキャラと遊びたいですか?:
50+* Discord: cohee, rossascends, wolfsblvt
67-
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/)
68-### [参加](https://discord.gg/RZdyAEUPvj)
69-
70-***
71-
72-開発者と直接連絡:
73-
74-* Discord: cohee または rossascends
75-* Reddit: /u/RossAscends または /u/sillylossy
7652* [GitHub issueを投稿](https://github.com/SillyTavern/SillyTavern/issues)
7753
7854### このバージョンには以下のコードが含まれますこのプロジェクトが気に入りました!どうすれば貢献できますか?
79-
80-* 大幅に修正された TavernAI 1.2.8 (コードの 50% 以上が書き換えまたは最適化されています)
81-* スワイプ
82-* グループチャット: キャラクター同士が会話できるマルチボットルーム
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-* プロンプト生成フォーマットの調整
90-
91-## 拡張機能
92-
93-SillyTavern は拡張できるようになっており。
94-
95-* キャラクターの感情表現(スプライト)
96-* チャット履歴の自動サマリー
97-* チャットに画像を送り、AI が内容を解釈する
98-* Stable Diffusion 画像生成 (5 つのチャット関連プリセットと 'free mode')
99-* AI 応答メッセージの音声合成(ElevenLabs、Silero、または OS のシステム TTS 経由)
100-
101-含まれている拡張機能の完全なリストとその使い方のチュートリアルは [Docs](https://docs.sillytavern.app/) にあります。
102-
103-## RossAscends による UI/CSS/クオリティオブライフの調整
104-
105-* iOS 用に最適化されたモバイル UI で、ホーム画面へのショートカット保存とフルスクリーンモードでの起動をサポート。
106-* ホットキー
107- * Up = チャットの最後のメッセージを編集する
108- * Ctrl+Up = チャットで最後のユーザーメッセージを編集する
109- * Left = 左スワイプ
110- * Right = 右スワイプ (注: チャットバーに何か入力されている場合、スワイプホットキーが無効になります)
111- * Ctrl+Left = ローカルに保存された変数を見る(ブラウザのコンソールウィンドウにて)
112- * Enter (チャットバー選択時) = AI にメッセージを送る
113- * Ctrl+Enter = 最後の AIのレスポンスを再生成する
114-
115-* ユーザー名の変更と文字の削除でページが更新されなくなりました。
116-
117-* ページロード時に API に自動的に接続するかどうかを切り替えます。
118-* ページの読み込み時に、最近見た文字を自動的に読み込むかどうかを切り替えます。
119-* より良いトークンカウンター - 保存されていないキャラクターに対して機能し、永続的なトークンと一時的なトークンの両方を表示する。
120-
121-* より良い過去のチャット
122- * 新しいチャットのファイル名は、"(文字) - (作成日)" という読みやすい形式で保存されます
123- * チャットのプレビューが 40 文字から 300 文字に増加。
124- * 文字リストの並べ替えに複数のオプション(名前順、作成日順、チャットサイズ順)があります。
125-
126-* デフォルトでは、左右の設定パネルはクリックすると閉じます。
127-* ナビパネルのロックをクリックすると、パネルが開いたままになり、この設定はセッションをまたいで記憶されます。
128-* ナビパネルの開閉状態もセッションをまたいで保存されます。
129-
130-* カスタマイズ可能なチャット UI:
131- * 新しいメッセージが届いたときにサウンドを再生する
132- * 丸型、長方形のアバタースタイルの切り替え
133- * デスクトップのチャットウィンドウを広くする
134- * オプションの半透明ガラス風パネル
135- * 'メインテキスト'、'引用テキスト'、'斜体テキスト'のページカラーをカスタマイズ可能。
136- * カスタマイズ可能な UI 背景色とぼかし量
137-
138-## インストール
139-
140-*注: このソフトウェアはローカルにインストールすることを目的としており、colab や他のクラウドノートブックサービス上では十分にテストされていません。*
141-
142-> **警告**
143-
144-> WINDOWS が管理しているフォルダ(Program Files、System32 など)にはインストールしないでください
145-
146-> START.BAT を管理者権限で実行しないでください
147-
148-### Windows
149-
150-Git 経由でのインストール(更新を容易にするため推奨)
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-
168-ZIP ダウンロードによるインストール(推奨しない)
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-
185-SillyTavern は API キーをサーバーディレクトリの `secrets.json` ファイルに保存します。
186-
187-デフォルトでは、入力後にページをリロードしても、フロントエンドには表示されません。
188-
189-API ブロックのボタンをクリックして、キーを閲覧できるようにする:
190-
191-1. ファイル `config.yaml` で `allowKeysExposure` の値を `true` に設定する。
192-2. SillyTavern サーバを再起動します。
193-
194-## リモート接続
195-
196-SillyTavern をスマホで使用しながら、同じ Wifi ネットワーク上で ST サーバーを PC で実行したい場合に使用します。
197-
198-しかし、これはどこからでもリモート接続を許可するために使用することができます。
199-
200-**重要: SillyTavern はシングルユーザーのプログラムなので、ログインすれば誰でもすべてのキャラクターとチャットを見ることができ、UI 内で設定を変更することができます。**
201-
202-### 1. ホワイトリスト IP の管理
203-
204-* SillyTavern のベースインストールフォルダ内に `whitelist.txt` という新しいテキストファイルを作成します。
205-* テキストエディタでこのファイルを開き、接続を許可したい IP のリストを追加します。
20655
207-*個々の IP とワイルドカード IP 範囲の両方が受け入れられる。例:*
56+1. プルリクエストを送ってください。貢献する方法については、[CONTRIBUTING.md](../CONTRIBUTING.md)をご覧ください。
57+2. 提供されたテンプレートを使用して、機能の提案や問題の報告を送ってください。
58+3. 重複した問題を避けるために、まずこのreadmeファイル全体とドキュメントウェブサイトを確認してください。
20859
209-```txt
60+## スクリーンショット
210-192.168.0.1
211-192.168.0.20
212-```
213-
214-または
215-
216-```txt
217-192.168.0.*
218-```
219-
220-(上記のワイルドカード IP 範囲は、ローカルネットワーク上のどのデバイスでも)
221-
222-CIDR マスクも受け付ける(例: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-
235-ST ホストデバイスが同じ無線 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-
251-http:// を使用し、https:// は使用しないでください
252-
253-### ST をすべての IP に開放する
254-
255-これはお勧めしませんが、`config.yaml` を開き、`whitelistMode` を `false` に変更してください。
256-
257-SillyTavern のベースインストールフォルダにある `whitelist.txt` が存在する場合は削除(または名前の変更)する必要があります。
258-
259-これは通常安全ではないので、これを行う際にはユーザー名とパスワードを設定する必要があります。
260-
261-ユーザー名とパスワードは `config.yaml` で設定します。
262-
263-ST サーバを再起動すると、ユーザ名とパスワードさえ知っていれば、IP に関係なくどのデバイスでも ST サーバに接続できるようになる。
264-
265-### まだ接続できませんか?
266-
267-* `config.yaml` で見つかったポートに対して、インバウンド/アウトバウンドのファイアウォールルールを作成します。これをルーターのポートフォワーディングと間違えないでください。そうしないと、誰かがあなたのチャットログを見つける可能性があり、それはマジで止めましょう。
268-* 設定 > ネットワークとインターネット > イーサネットで、プライベートネットワークのプロファイルタイプを有効にします。そうしないと、前述のファイアウォールルールを使っても接続できません。
269-
270-## パフォーマンスに問題がありますか?
271-
272-ユーザー設定パネルでブラー効果なし(高速 UI)モードを有効にしてみてください。
273-
274-## このプロジェクトが気に入りました!どうすれば貢献できますか?
275-
276-### やるべきこと
277-
278-1. プルリクエストを送る
279-2. 確立されたテンプレートを使って機能提案と課題レポートを送る
280-3. 何か質問する前に、readme ファイルや組み込みのドキュメントを読んでください
281-
282-### やるべきではないこと
28361
284-1. 金銭の寄付を申し出る
62+<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">
285-2. 何の脈絡もなくバグ報告を送る
63+<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">
286-3. すでに何度も回答されている質問をする
28764
28865## 古い背景画像はどこにありますか?インストール
28966
290-100% オリジナルコンテンツのみのポリシーに移行しているため、古い背景画像はこのリポジトリから削除されました。
67+詳細なインストール手順については、私たちのドキュメントをご覧ください:
29168
292-アーカイブはこちら:
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/)**
29373
294-<https://files.catbox.moe/1xevnc.zip>
74+## ライセンスとクレジット
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
30187## ライセンスとクレジットトップコントリビューター
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
32991<!-- LINK GROUP -->
33092[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 @@
1010[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
1212[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
1313[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/networkforks)
1414[![GitHub Issues](https://img.shields.io/github/issues/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/issues)
1515[![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
2727SillyTavern(짧게는 ST)은 텍스트 생성 LLM, 이미지 생성 엔진, TTS 음성 모델 등과 상호작할 수 있는 로컬 설치형 UI 입니다.
2828
29292023년 2월, TavernAI 1.2.8의 포크로 시작한 SillyTavern은 현재 100명이200명이 넘는 기여자를 보유하고 있으며, 2년간의 독자적인 개발을 거쳐 숙련된 AI 애호가들을 위한 선도적인 소프트웨어로 자리매김하고 있습니다.
30-
3130
3231## 우리의 비전
3332
@@ -35,30 +34,9 @@ SillyTavern(짧게는 ST)은 텍스트 생성 LLM, 이미지 생성 엔진, TTS
35342. 저희는 어떠한 온라인 및 호스팅 서브시도 제공하지 않으며, 프로그래밍으로 사용자의 데이터를 추적하지 않습니다.
36353. SillyTavern은 헌신적인 LLM 커뮤니티가 여러분에게 제공하는 열정적인 프로젝트이며, 언제나 무료이며 오픈소스로 제공될 것입니다.
3736
38-## 브랜치
37+## SillyTavern을 위해서 좋은 성능의 PC가 필요한가요?
39-
40-SillyTavern은 모든 사용자가 원활한 경험을 할 수 있도록 두 개의 브랜치를 활용하여 개발되고 있습니다.
41-
42-
43-* `release` -🌟 **대부분의 사용자에게 추천됨.** 가장 안정적이고 권장되는 브랜치이며, 주요 릴리스가 배포될 때만 업데이트됩니다. 대부분의 사용자에게 적합합니다. 일반적으로 한달에 한번 업데이트됩니다.
44-* `staging` - ⚠️ **일반적인 사용에 추천되지 않음.** 최신 기능을 가지고 있지만, 언제든지 문제가 발생할 수 있습니다. 고급 사용자 및 숙련자 전용입니다. 하루에 여러번 업데이트됩니다.
45-
46-만약 git CLI 사용에 익숙하지 않거나 브랜치가 무엇인지 모르겠다면 release 브랜치가 더 나은 선택입니다.
47-
48-## SillyTavern 외에 무엇이 필요한가요?
49-
50-SillyTavern은 인터페이스 역할만 하기 때문에, 실제로 채팅하려면 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
6341## 질문이나 제안이 있으신가요?
6442
@@ -84,293 +62,14 @@ SillyTavern은 인터페이스 역할만 하기 때문에, 실제로 채팅하
8462<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">
8563<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">
8664
65+## 설치
8766
88-## 캐릭터 카드
67+자세한 설치 방법은 저희의 문서를 확인하세요:
89-
90-SillyTavern은 "캐릭터 카드"라는 개념을 중심으로 구축되었습니다. 캐릭터 카드는 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-
111-SillyTavern은 확장(익스텐션)을 지원합니다.
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-
136-1. [NodeJS](https://nodejs.org/ko) 설치 (최신 LTS 버전 권장)
137-2. [Git for Windows](https://gitforwindows.org/) 설치
138-3. 파일 탐색기 열기 (`Win+E`)
139-4. Windows에서 제어하거나 모니터하지 않는 폴더를 찾거나 만드세요. (ex: C:\MySpecialFolder\)
140-5. 상단의 주소 표시줄을 클릭하고 `cmd`를 입력한 후 Enter 키를 눌러 해당 폴더 내에서 명령 프롬프트를 여세요.
141-6. 검은색 창(명령 프롬프트)이 나타나면 다음 중 하나를 입력하고 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-
146-7. clone이 완료되면, `Start.bat`을 더블 클릭하여 NodeJS가 필요한 구성요소를 설치하도록 하세요.
147-8. 그러면 서버가 시작하고, 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-
167-MacOS / Linux 에서는 이 모든 작업이 터미널에서 수행됩니다.
168-
169-1. git과 nodeJS 설치 (이 작업은 OS에 따라 달라집니다.)
170-2. 리포지토리 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-
175-3. `cd SillyTavern` 를 입력해 설치 폴더로 이동하기
176-4. `start.sh` 스크립트를 아래의 명령어 중 하나로 실행하기:
177-
178-* `./start.sh`
179-* `bash start.sh`
180-
181-## ⚡ SillyTavern Launcher를 통해 설치하기
182-
183-SillyTavern 런처는 로컬 LLM 사용을 위한 백엔드 설치를 포함하여 다양한 설정을 도와주는 설치 마법사입니다.
184-
185-
186-### Windows 사용자
187-
188-1. 키보드에서 **`WINDOWS + R`** 키를 눌러 실행 창을 여세요. 그리고 아래의 명령어를 입력해 git을 설치하세요.
189-
190-```shell
191-cmd /c winget install -e --id Git.Git
192-```
193-
194-2. 키보드에서 **`WINDOWS + E`** 키를 눌러 파일 탐색기를 열고 런처를 설치할 폴더로 이동합니다. 원하는 폴더에 도착하면 주소 표시줄에 `cmd`를 입력하고 Enter 키를 누릅니다. 그 후 아래의 명령어를 입력합니다.
195-
196-
197-```shell
198-git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher && start installer.bat
199-```
200-
201-### Linux 사용자
202-
203-1. 선호하는 터미널을 열고 git을 설치하세요.
204-2. SillyTavern-Launcher를 clone 하세요:
205-
206-```shell
207-git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
208-```
209-
210-3. installer.sh를 실행하세요:
211-
212-```shell
213-chmod +x install.sh && ./install.sh
214-```
215-
216-4. 설치가 끝나면 launcher.sh를 실행하세요:
217-
218-```shell
219-chmod +x launcher.sh && ./launcher.sh
220-```
221-
222-### Mac 사용자
223-
224-1. 터미널을 열고 Brew를 설치하세요:
225-
226-```shell
227-/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
228-```
229-
230-2. git을 설치하세요:
231-
232-```shell
233-brew install git
234-```
235-
236-3. SillyTavern-Launcher를 clone 하세요:
237-
238-```shell
239-git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
240-```
241-
242-4. installer.sh를 실행하세요:
243-
244-```shell
245-chmod +x install.sh && ./install.sh
246-```
247-
248-5. 설치가 끝나면 launcher.sh를 실행하세요:
249-
250-```shell
251-chmod +x launcher.sh && ./launcher.sh
252-```
253-
254-## 🐋 Docker를 통해 설치하기
255-
256-이 방법은 Docker가 설치되어 있고, Docker 설치를 위해 커맨드 라인에 접근할 수 있으며, Docker의 일반적인 작동 방식에 익숙하다고 가정합니다.
257-
258-### 이미지 직접 빌드하기
259-
260-SillyTavern을 Docker에서 사용하는 방법에 대한 포괄적인 가이드는 [여기서](http://docs.sillytavern.app/installation/docker/) 확인할 수 있습니다. 이 가이드는 Windows, macOS 및 Linux에서의 설치를 다룹니다! 직접 이미지를 빌드하려면 이 가이드를 읽어보세요.
261-
262-### GitHub Container Registry 사용하기 (가장 쉬움)
263-
264-SillyTavern이 작동하려면 두 개의 필수 디렉터리 매핑과 하나의 포트 매핑이 필요합니다. 명령에서 다음 위치의 선택 항목을 바꿔주세요.
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-
288-1. 커맨드 라인 열기
289-2. 아래의 명령어 실행
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-
308-SillyTavern은 API 키를 사용자 데이터 디렉터리의 `secrets.json` 파일에 저장합니다 (`/data/default-user/secrets.json`이 기본 경로입니다).
309-
310-
311-
312-기본적으로 API 키는 저장하고 페이지를 새로 고침한 후에는 인터페이스에서 보이지 않습니다.
313-
314-키 보기 기능을 활성화하려면 다음 단계를 따르세요:
315-
316-1. `config.yaml` 파일에서 `allowKeysExposure` 값을 `true로` 설정합니다.
317-2. SillyTavern 서버를 다시 시작합니다.
318-3. API 연결 패널 오른쪽 하단에 있는 '숨겨진 API 키 보기' 링크를 클릭합니다.
319-
320-## 커맨드 라인 인수
321-
322-`config.yaml`의 일부 설정을 덮어쓰기 위해 SillyTavern 서버 시작 시 커맨드 라인 인수를 전달할 수 있습니다.
323-
324-
325-### 예시
326-
327-```shell
328-node server.js --port 8000 --listen false
329-# 혹은
330-npm run start -- --port 8000 --listen false
331-# 혹은 (Windows 전용)
332-Start.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
371-1. 사용자 설정 패널(UI 테마 전환 카테고리)에서 흐림 효과를 비활성화하고 동작 줄이기를 활성화합니다.
69+* **[Windows 설치 가이드](https://docs.sillytavern.app/installation/windows/)**
372-2. 응답 스트리밍을 사용하는 경우 스트리밍 FPS를 더 낮은 값(10-15 FPS 권장)으로 설정합니다.
70+* **[MacOS/Linux 설치 가이드](https://docs.sillytavern.app/installation/linuxmacos/)**
373-3. 브라우저에서 렌더링에 GPU 가속을 사용하도록 설정되어 있는지 확인합니다.
71+* **[Android (Termux) 설치 가이드](https://docs.sillytavern.app/installation/android-(termux)/)**
72+* **[Docker 설치 가이드](https://docs.sillytavern.app/installation/docker/)**
37473
37574## 라이센스 및 크레딧
37675
@@ -383,6 +82,7 @@ Start.bat --port 8000 --listen false
38382* Font Awesome의 아이콘 테마 <https://fontawesome.com> (아이콘: CC BY 4.0, 폰트: SIL OFL 1.1, 코드: MIT 라이선스)
38483* 기본 콘텐츠는 @OtisAlejandro (Seraphina 캐릭터 및 로어북)와 @kallmeflocc (10K 디스코드 사용자 축전 배경화면)가 제공함
38584* [@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
38787## 상위 기여자
38888
.github/readme-ru_ru.md+44 -400
@@ -10,7 +10,7 @@
1010[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
1212[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
1313[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/networkforks)
1414[![GitHub Issues](https://img.shields.io/github/issues/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/issues)
1515[![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
1919---
2020
2121Мобайл-френдлиSillyTavern предоставляет единый интерфейс, поддержкадля множествамногих LLM API (KoboldAI/CPP, Horde, NovelAI, Ooba, Tabby, OpenAI, OpenRouter, Claude, ScaleMistral и других), ВНмобайл-образныйфрендли макет, режим Вайфувизуальной новеллы, Stableинтеграцию Diffusionс генерацией изображений через API Automatic1111 и ComfyUI, TTS, поддержка мировWorldInfo (лорбуковлорбуки), кастомизируемый UI, автоперевод, тончайшаятончайшую настройканастройку промптов, +и возможность устанавливать расширения.
2222
23-Основано на форке [TavernAI](https://github.com/TavernAI/TavernAI) версии 1.2.8
23+Чтобы помочь вам быстрее разобраться в SillyTavern, мы создали [сайт с документацией](https://docs.sillytavern.app/). Ответы на большинство вопросов можно найти там.
2424
2525## ВажныеЧто новости!такое SillyTavern?
2626
27-1. Чтобы помочь вам быстрее разобраться в SillyTavern, мы создали [сайт с документацией](https://docs.sillytavern.app/). Ответы на большинство вопросов можно найти там.
27+SillyTavern (или сокращенно ST) - это локально устанавливаемый пользовательский интерфейс, который позволяет вам взаимодействовать с LLM для генерации текста, движками для генерации изображений и моделями голоса TTS.
2828
29-2. Почему пропали расширения после апдейта? Начиная с версии 1.10.6, большинство встроенных расширений были конвертированы в формат загружаемых аддонов. Их можно установить обратно через меню "Download Extensions and Assets" на панели расширений (значок с тремя кубиками сверху).
29+Начавшись в феврале 2023 года как форк TavernAI 1.2.8, SillyTavern теперь насчитывает более 200 контрибьюторов и 2 года независимой разработки, и продолжает служить ведущим программным обеспечением для опытных энтузиастов ИИ.
3030
31-3. Не поддерживается следующая платформа: android arm LEtime-web. 32-битный Android требует внешнюю зависимость, которую нельзя установить посредством npm. Для её установки потребуется следующая команда: `pkg install esbuild`. После этого продолжайте установку по общей инструкции.
31+## Наше видение
3232
33-### Разрабатывается Cohee, RossAscends и всем сообществом SillyTavern
33+1. Мы стремимся предоставить пользователям как можно больше полезности и контроля над их промптами LLM. Крутая кривая обучения - это часть веселья!
34+2. Мы не предоставляем никаких онлайн или хостинговых услуг, а также программно не отслеживаем данные пользователей.
35+3. SillyTavern - это проект, созданный преданным сообществом энтузиастов LLM, и он всегда будет бесплатным и с открытым исходным кодом.
3436
35-### Что такое SillyTavern и TavernAI?
37+## Нужен ли мне мощный компьютер для запуска SillyTavern?
3638
37-SillyTavern — это интерфейс, который устанавливается на ПК (и на Android), который даёт возможность общаться с генеративным ИИ и чатиться/ролеплеить с вашими собственными персонажами или персонажами других пользователей.
39+Требования к оборудованию минимальны: он будет работать на всем, что может запустить NodeJS 18 или выше. Если вы собираетесь выполнять инференс LLM на своем локальном компьютере, мы рекомендуем видеокарту NVIDIA 3000-й серии с не менее чем 6 ГБ видеопамяти, но фактические требования могут варьироваться в зависимости от модели и используемого вами бэкенда.
3840
39-SillyTavern — это форк версии 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-
48-SillyTavern разрабатывается в двух ветках, чтобы всем категориям пользователей было удобно.
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-
61-SillyTavern — это просто интерфейс, поэтому запустить его можно на любой картошке. Мощным должен быть бэкенд с ИИ.
62-
63-## Есть вопросы или предложения?
64-
65-### У нас появился сервер в Discord
6644
6745| [![][discord-shield-badge]][discord-link] | [Вступайте в наше Discord-сообщество!](https://discord.gg/sillytavern) ЗадавайтеПолучайте вопросыподдержку, делитесь любимыми персонажами и промптами. |
6846| :---------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------- |
6947
7048ТакжеИли можносвяжитесь написатьс разработчикамразработчиками напрямую:
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-
91-SillyTavern поддерживает расширения.
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-
183-1. Установите git и nodeJS (как именно - зависит от вашей ОС)
184-2. Клонируйте репозиторий
185-
186-- для ветки release: `git clone https://github.com/SillyTavern/SillyTavern -b release`
187-- для ветки staging: `git clone https://github.com/SillyTavern/SillyTavern -b staging`
188-
189-3. Перейдите в папку установки с помощью `cd SillyTavern`.
190-4. Запустите скрипт `start.sh` с помощью одной из команд:
191-
192-- `./start.sh`
193-- `bash start.sh`
194-
195-## Установка с помощью SillyTavern Launcher
196-
197-### Для пользователей Linux
198-1. Откройте любимый терминал и установите git
199-2. Загрузите Sillytavern Launcher с помощью команды: `git clone https://github.com/SillyTavern/SillyTavern-Launcher.git`
200-3. Перейдите в SillyTavern-Launcher: `cd SillyTavern-Launcher`
201-4. Запустите лаунчер установки: `chmod +x install.sh && ./install.sh`, затем выберите, что конкретно хотите установить
202-5. После завершения установки, запустите лаунчер следующей командой: `chmod +x launcher.sh && ./launcher.sh`
203-
204-### Для пользователей Mac
205-1. Откройте терминал и установите brew: `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"`
206-2. Затем установите git: `brew install git`
207-3. Загрузите Sillytavern Launcher: `git clone https://github.com/SillyTavern/SillyTavern-Launcher.git`
208-4. Перейдите в SillyTavern-Launcher: `cd SillyTavern-Launcher`
209-5. Запустите лаунчер установки: `chmod +x install.sh && ./install.sh` and choose what you wanna install
210-6. После завершения установки, запустите лаунчер следующей командой: `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-
243-1. Откройте командную строку
244-2. Выполните следующую команду
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-## Управление ключами от API
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+* [Опубликовать issue на GitHub](https://github.com/SillyTavern/SillyTavern/issues)
26053
261-SillyTavern сохраняет ключи от ваших API в файле `secrets.json` в папке на сервере.
54+### Мне нравится ваш проект! Как я могу внести свой вклад?
26255
263-По умолчанию, ключи не будут отображаться на фронте после их ввода и перезагрузки страницы.
56+1. Отправляйте пулл-реквесты. Узнайте, как внести свой вклад: [CONTRIBUTING.md](../CONTRIBUTING.md)
57+2. Отправляйте предложения по функциям и отчеты о проблемах, используя предоставленные шаблоны.
58+3. Прочтите весь этот файл readme и сайт документации, чтобы избежать отправки дублирующихся проблем.
26459
265-Чтобы включить возможность отображения ключей путём нажатия кнопки в блоке API:
60+## Скриншоты
266-
267-1. Зайдите в файл `config.yaml` и установите `allowKeysExposure` в положение `true`.
268-2. Перезапустите сервер SillyTavern.
269-
270-## Аргументы командной строки
271-
272-Вы можете передавать аргументы командной строки при запуске сервера SillyTavern, чтобы переопределять настройки из `config.yaml`.
273-
274-### Примеры
275-
276-```shell
277-node server.js --port 8000 --listen false
278-# или
279-npm run start -- --port 8000 --listen false
280-# или (только на Windows)
281-Start.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
326-192.168.0.1
327-192.168.0.20
328-```
329-
330-или
331-
332-```txt
333-192.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-
395-1. Присылайте пулл реквесты
396-2. Присылайте идеи и баг-репорты, оформленные по установленным шаблонам
397-3. Прежде чем задавать вопросы, прочтите readme и документацию
398-
399-### ЧЕГО НЕ ДЕЛАТЬ
400-
401-1. Предлагать донаты
402-2. Присылать баг-репорты безо всякого контекста
403-3. Задавать вопросы, на которые уже отвечали
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
44591<!-- LINK GROUP -->
446-[back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square
44792[cover]: https://github.com/user-attachments/assets/01a6ae9a-16aa-45f2-8bff-32b5dc587e44
44893[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
45094[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 的一个分支开始,如
34342. 我们不提供任何在线或托管服务,也不会以编程方式跟踪任何用户数据。
35353. SillyTavern 是一个由专注的 LLM 爱好者社区为您带来的充满激情的项目,并且将永远是免费和开源的。
3636
37-## 分支
37+## 我需要一台性能强大的电脑来运行 SillyTavern 吗?
3838
39-SillyTavern 采用双分支进行开发,以确保所有用户都能获得流畅的使用体验。
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
6141## 有问题或建议?
6242
@@ -67,9 +47,9 @@ SillyTavern 采用双分支进行开发,以确保所有用户都能获得流
6747
6848或者直接与开发人员联系:
6949
7050-* Discord: cohee, rossascends, wolfsblvt
7151-* 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/)
7252-* [提交 GitHub 问题](https://github.com/SillyTavern/SillyTavern/issues)
7353
7454### 我喜欢你的项目!我该如何贡献自己的力量?
7555
@@ -82,315 +62,33 @@ SillyTavern 采用双分支进行开发,以确保所有用户都能获得流
8262<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">
8363<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">
8464
8565## 角色卡安装
86-
87-SillyTavern 围绕“角色卡”的概念构建。角色卡是设定 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-
105-SillyTavern 支持扩展。
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-
129-1. 安装 [NodeJS](https://nodejs.org/en)(建议使用最新的 LTS 版本)
130-2. 安装 [Git for Windows](https://gitforwindows.org/)
131-3. 打开 Windows 资源管理器 (`Win+E`)
132-4. 浏览或创建一个不受 Windows 控制或监控的文件夹(例如:C:\MySpecialFolder\)
133-5. 通过点击顶部的“地址栏”,输入 `cmd`,然后按 Enter,在该文件夹内打开命令提示符。
134-6. 弹出黑框(命令提示符)后,键入以下其中一项并按 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-
139-7. 等待所有内容克隆完成后,双击 `Start.bat` 以使 NodeJS 安装其依赖项。
140-8. 然后服务器将启动,SillyTavern 将在您的浏览器中弹出。
141-
142-#### 通过 GitHub Desktop 安装
143-
144-(这**仅**允许在 GitHub Desktop 中使用 git,如果您也想在命令行上使用 `git`,则还需要安装 [Git for Windows](https://gitforwindows.org/))
145-
146-1. 安装 [NodeJS](https://nodejs.org/en)(建议使用最新的 LTS 版本)
147-2. 安装 [GitHub Desktop](https://central.github.com/deployments/desktop/desktop/latest/win32)
148-3. 安装 GitHub Desktop 后,点击 `Clone a repository from the internet....`(注意:此步骤**无需**创建 GitHub 帐户)
149-4. 在菜单中,点击 URL 选项卡,输入此 URL `https://github.com/SillyTavern/SillyTavern`,然后点击 Clone。您可以更改本地路径以更改 SillyTavern 的下载位置。
150-5. 要打开 SillyTavern,请使用 Windows 资源管理器浏览到克隆存储库的文件夹。默认情况下,存储库将克隆到此处:`C:\Users\[您的 Windows 用户名]\Documents\GitHub\SillyTavern`
151-6. 双击 `start.bat` 文件。(注意:文件名的 `.bat` 部分可能被您的操作系统隐藏,在这种情况下,它将显示为一个名为“`Start`”的文件。双击此文件以运行 SillyTavern)
152-7. 双击后,应打开一个大的黑色命令控制台窗口,SillyTavern 将开始安装其运行所需的组件。
153-8. 安装过程完成后,如果一切正常,命令控制台窗口应如下所示,并且您的浏览器中应打开一个 SillyTavern 选项卡:
154-9. 连接到任何[支持的 API](https://docs.sillytavern.app/usage/api-connections/) 并开始聊天!
155-
156-### 🐧 Linux & 🍎 MacOS
157-
158-对于 MacOS / Linux,所有这些都将在终端中完成。
159-
160-1. 安装 git 和 nodeJS(具体方法取决于您的操作系统)
161-2. 克隆仓库
162-
163-- Release 分支:`git clone https://github.com/SillyTavern/SillyTavern -b release`
164-- Staging 分支: `git clone https://github.com/SillyTavern/SillyTavern -b staging`
165-
166-3. `cd SillyTavern` 导航到安装文件夹。
167-4. 使用以下命令之一运行 `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
183-docker-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/)**
208-
70+* **[MacOS/Linux 安装指南](https://docs.sillytavern.app/installation/linuxmacos/)**
209-#### 运行容器
71+* **[Android (Termux) 安装指南](https://docs.sillytavern.app/installation/android-(termux)/)**
210-
72+* **[Docker 安装指南](https://docs.sillytavern.app/installation/docker/)**
211-1. 打开您的命令行
212-2. 在您要存储配置和数据文件的文件夹中运行以下命令:
213-
214-```bash
215-SILLYTAVERN_VERSION="latest"
216-PUBLIC_PORT="8000"
217-CONFIG_PATH="./config"
218-DATA_PATH="./data"
219-PLUGINS_PATH="./plugins"
220-EXTENSIONS_PATH="./extensions"
221-
222-docker 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-
240-SillyTavern Launcher 是一个安装向导,可帮助您进行多种选项的设置,包括为本地推理安装后端。
241-
242-### 对于 Windows 用户
243-
244-1. 在键盘上:按 **`WINDOWS + R`** 打开“运行”对话框。然后,运行以下命令安装 git:
245-
246-```shell
247-cmd /c winget install -e --id Git.Git
248-```
249-
250-2. 在键盘上:按 **`WINDOWS + E`** 打开文件资源管理器,然后导航到要安装启动器的文件夹。进入所需文件夹后,在地址栏中键入 `cmd` 并按 Enter。然后,运行以下命令:
251-
252-```shell
253-git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher && start installer.bat
254-```
255-
256-### 对于 Linux 用户
257-
258-1. 打开您喜欢的终端并安装 git
259-2. 使用以下命令克隆 SillyTavern-Launcher:
260-
261-```shell
262-git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
263-```
264-
265-3. 使用以下命令启动 installer.sh:
266-
267-```shell
268-chmod +x install.sh && ./install.sh
269-```
270-
271-4. 安装后使用以下命令启动 launcher.sh:
272-
273-```shell
274-chmod +x launcher.sh && ./launcher.sh
275-```
276-
277-### 对于 Mac 用户
278-
279-1. 打开终端并使用以下命令安装 brew:
280-
281-```shell
282-/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
283-```
284-
285-2. 使用以下命令安装 git:
286-
287-```shell
288-brew install git
289-```
290-
291-3. 使用以下命令克隆 SillyTavern-Launcher:
292-
293-```shell
294-git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
295-```
296-
297-4. 使用以下命令启动 installer.sh:
298-
299-```shell
300-chmod +x install.sh && ./install.sh
301-```
302-
303-5. 安装后使用以下命令启动 launcher.sh:
304-
305-```shell
306-chmod +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
327-node server.js --port 8000 --listen false
328-# 或
329-npm run start -- --port 8000 --listen false
330-# 或 (仅限 Windows)
331-Start.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
37474## 许可证和致谢
37575
37676**本程序的分发是希望它能有用,但不提供任何保证;甚至没有对适销性或特定用途适用性的默示保证。有关更多详细信息,请参阅 GNU Affero 通用公共许可证。**
37777
37878-* [TavernAI](https://github.com/TavernAI/TavernAI) 1.2.8 by Humi: MIT 许可证
37979-* CncAnon 的 TavernAITurbo mod 的部分内容经许可使用
38080-* 视觉小说模式的灵感来自 PepperTaco 的工作 (<https://github.com/peppertaco/Tavern/>)
38181-* Noto Sans 字体 by Google (OFL 许可证)
38282-* 图标主题 by Font Awesome <https://fontawesome.com> (图标: CC BY 4.0, 字体: SIL OFL 1.1, 代码: MIT 许可证)
38383-* 默认内容由 @OtisAlejandro (Seraphina 角色和世界书) 和 @kallmeflocc (10K Discord 用户庆祝背景) 提供
38484-* Docker 指南由 [@mrguymiah](https://github.com/mrguymiah) 和 [@Bronya-Rand](https://github.com/Bronya-Rand) 提供
38585-* kokoro-js 库由 [@hexgrad](https://github.com/hexgrad) 提供 (Apache-2.0 许可证)
386-- 中文翻译由 [@XXpE3](https://github.com/XXpE3) 完成,中文 ISSUES 可以联系 @XXpE3
38786
38887## 主要贡献者
38988
39089[![Contributors](https://contrib.rocks/image?repo=SillyTavern/SillyTavern)](https://github.com/SillyTavern/SillyTavern/graphs/contributors)
39190
39291<!-- LINK GROUP -->
393-
39492[cover]: https://github.com/user-attachments/assets/01a6ae9a-16aa-45f2-8bff-32b5dc587e44
39593[discord-link]: https://discord.gg/sillytavern
39694[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 @@
1010[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
1212[![GitHub 星標](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
1313[![GitHub 分支](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/networkforks)
1414[![GitHub 問題](https://img.shields.io/github/issues/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/issues)
1515[![GitHub 拉取請求](https://img.shields.io/github/issues-pr/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/pulls)
1616
@@ -26,7 +26,7 @@ SillyTavern 提供一個統一的前端介面,整合多種大型語言模型
2626
2727SillyTavern(簡稱 ST)是一款本地安裝的使用者介面,讓您能與大型語言模型(LLM)、影像生成引擎以及語音合成模型互動的前端。
2828
2929SillyTavern 起源於 2023 年 2 月,作為 TavernAI 1.2.8 的分支版本發展至今。目前已有超過 100200 位貢獻者,並擁有超過兩年的獨立開發歷史。如今,它已成為 AI 愛好者中備受推崇的軟體之一。
3030
3131## 我們的願景
3232
@@ -34,29 +34,9 @@ SillyTavern 起源於 2023 年 2 月,作為 TavernAI 1.2.8 的分支版本發
34342. 我們不提供任何線上或託管服務,也不會程式化追蹤任何使用者數據。
35353. SillyTavern 是由一群熱衷於 LLM 的開發者社群所打造的專案,並將永遠保持免費與開源。
3636
37-## 分支介紹
37+## 我需要高效能電腦才能運行 SillyTavern 嗎?
3838
39-SillyTavern 採用雙分支開發模式,確保為所有使用者提供流暢的體驗。
39+SillyTavern 的硬體需求相當低。任何能夠運行 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-
52-SillyTavern 的硬體需求相當低。任何能夠運行 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
6141## 有任何問題或建議?
6242
@@ -82,287 +62,19 @@ SillyTavern 的硬體需求相當低。任何能夠運行 NodeJS 18 或更高版
8262<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">
8363<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">
8464
8565## 角色卡安裝指南
86-
87-SillyTavern 的核心概念是「角色卡」(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-
107-SillyTavern 支持多種擴充功能。
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-
131-1. 安裝 [NodeJS](https://nodejs.org/en)(建議使用最新的 LTS 版本)
132-2. 安裝 [Git for Windows](https://gitforwindows.org/)
133-3. 打開 Windows 檔案總管(`Win+E`)
134-4. 創建/使用一個不受 Windows 系統控制或監控的資料夾(例如:C:\MySpecialFolder\)
135-5. 在該資料夾內開啟命令提示字元(Command Prompt):點擊地址欄,輸入 `cmd` 並按下 Enter
136-6. 當命令提示字元黑框彈出時,輸入以下其中一條指令後,按下 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-
141-7. 當程式碼下載完成後,雙擊 `Start.bat`,NodeJS 將自動安裝所需的依賴項
142-8. 本地伺服器啟動後,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-
162-1. 安裝 git 和 NodeJS(具體方法因操作系統而異)
163-2. 複製儲存庫(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-
168-3. 使用命令 `cd SillyTavern` 以進入安裝資料夾
169-4. 使用以下其中一條命令,以執行 `start.sh` 腳本:
170-
171-* `./start.sh`
172-* `bash start.sh`
173-
174-## ⚡ 使用 SillyTavern Launcher 安裝
175-
176-SillyTavern Launcher 是一個安裝嚮導,協助您設定多種選項,包括安裝本地推理(inference)的後端。
177-
178-### 對於 Windows 使用者
179-
180-1. 在鍵盤上按下 **`WINDOWS + R`** 打開「執行」對話框,然後輸入以下指令以安裝 git:
181-
182-```shell
183-cmd /c winget install -e --id Git.Git
184-```
185-
186-2. 在鍵盤上按下 **`WINDOWS + E`** 打開檔案總管,導航至您想要安裝 Launcher 的資料夾。在目標資料夾的地址欄輸入 `cmd` 並按下 Enter。接著執行以下命令:
187-
188-```shell
189-git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher && start installer.bat
190-```
191-
192-### 對於 Linux 使用者
193-
194-1. 打開您喜歡的終端機(Terminal),安裝 git
195-2. 使用以下指令以複製 Sillytavern-Launcher:
196-
197-```shell
198-git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
199-```
200-
201-3. 執行安裝腳本(installer.sh):
202-
203-```shell
204-chmod +x install.sh && ./install.sh
205-```
206-
207-4. 安裝完成後,執行啟動腳本(launcher.sh):
208-
209-```shell
210-chmod +x launcher.sh && ./launcher.sh
211-```
21266
213-### 對於 Mac 使用者
67+有關詳細的安裝說明,請訪問我們的文檔:
21468
215-1. 打開終端機(Terminal),並使用以下指令安裝 Homebrew:
69+* **[Windows 安裝指南](https://docs.sillytavern.app/installation/windows/)**
216-
70+* **[MacOS/Linux 安裝指南](https://docs.sillytavern.app/installation/linuxmacos/)**
217-```shell
71+* **[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-
221-2. 使用 Homebrew 以安裝 git:
222-
223-```shell
224-brew install git
225-```
226-
227-3. 使用以下指令以複製 Sillytavern-Launcher:
228-
229-```shell
230-git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
231-```
232-
233-4. 執行安裝腳本(installer.sh):
234-
235-```shell
236-chmod +x install.sh && ./install.sh
237-```
238-
239-5. 安裝完成後,執行啟動腳本(launcher.sh):
240-
241-```shell
242-chmod +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-
276-1. 打開命令列(Command Line)
277-2. 執行以下指令:
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-
296-SillyTavern 將您的 API 金鑰(Keys)保存在使用者數據目錄中的 `secrets.json` 文件內(默認路徑為`/data/default-user/secrets.json`)
297-
298-默認情況下,API 金鑰在您保存並重新載入頁面後,將不會自介面中顯示
299-
300-如需啟用查看金鑰功能:
301-
302-1. 在 `config.yaml` 文件中,將 `allowKeysExposure` 的「值」設為 `true`
303-2. 重新啟動 SillyTavern 伺服器
304-3. 點擊 API 連線頁面右下角的「查看隱藏的 API 金鑰(View hidden API keys)」超連結
305-
306-## 命令列參數(Command-line Arguments)
307-
308-您可以在啟動 SillyTavern 伺服器時傳遞命令列參數,以覆蓋 `config.yaml` 文件中的某些設定。
309-
310-### 範例
311-
312-```shell
313-node server.js --port 8000 --listen false
314-# or
315-npm run start -- --port 8000 --listen false
316-# or(僅適用於 Windows)
317-Start.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-
356-1. 在「使用者設定」選單(設定介面主題)中,禁用模糊效果(Blur Effect),並開啟「減少動畫效果」(Reduced Motion)
357-2. 若使用響應串流傳輸,請將串流的 FPS 設定為較低的值(建議設定為 10-15 FPS)
358-3. 確保瀏覽器已啟用 GPU 加速以進行渲染
35973
36074## 授權與致謝
36175
36276**本程式(SillyTavern)的發布是基於其可能對使用者有所幫助的期許,但不提供任何形式的保證;包括但不限於對可銷售性(marketability)或特定用途適用性的隱含保證。如需更多詳情,請參閱 GNU Affero 通用公共許可證。**
36377
364-This 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-
36678* [TavernAI](https://github.com/TavernAI/TavernAI) 1.2.8 由 Humi 提供:MIT 許可
36779* 經授權使用部分來自 CncAnon 的 TavernAITurbo 模組
36880* 視覺小說模式(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
37082* 主題圖示由 Font Awesome <https://fontawesome.com> 提供(圖示:CC BY 4.0,字體:SIL OFL 1.1,程式碼:MIT 許可)
37183* 預設資源來源於 @OtisAlejandro(包含角色 Seraphina 與知識書)與 @kallmeflocc(SillyTavern 官方 Discord 伺服器成員突破 10K 的慶祝背景)
37284* 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
37487## 主要貢獻者
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
31312. We do not provide any online or hosted services, nor programmatically track any user data.
32323. 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-## Branches
34+## Do I need a powerful PC to run SillyTavern?
3535
36-SillyTavern is being developed using a two-branch system to ensure a smooth experience for all users.
36+The 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-
41-If 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-
45-Since 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-
49-The 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
5838## Questions or suggestions?
5939
@@ -79,316 +59,14 @@ Or get in touch with the developers directly:
7959<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">
8060<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">
8161
8262## Character CardsInstallation
83-
84-SillyTavern 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-
86-To 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-
88-To 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-
102-SillyTavern 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-
112-Tutorials 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-
126-1. Install [NodeJS](https://nodejs.org/en) (latest LTS version is recommended)
127-2. Install [Git for Windows](https://gitforwindows.org/)
128-3. Open Windows Explorer (`Win+E`)
129-4. Browse to or Create a folder that is not controlled or monitored by Windows. (ex: C:\MySpecialFolder\)
130-5. Open a Command Prompt inside that folder by clicking in the 'Address Bar' at the top, typing `cmd`, and pressing Enter.
131-6. 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-
136-7. Once everything is cloned, double-click `Start.bat` to make NodeJS install its requirements.
137-8. 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-
155-For MacOS / Linux all of these will be done in a Terminal.
156-
157-1. Install git and nodeJS (the method for doing this will vary depending on your OS)
158-2. 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-
163-3. `cd SillyTavern` to navigate into the install folder.
164-4. Run the `start.sh` script with one of these commands:
165-
166-* `./start.sh`
167-* `bash start.sh`
168-
169-## 🐋 Installing via Docker
170-
171-These instructions assume you have installed Docker, are able to access your command line for the installation of containers, and familiar with their general operation.
172-
173-### Using the GitHub Container Registry
174-
175-#### Docker Compose (easiest)
176-
177-Grab 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
180-docker-compose up
181-```
182-
183-Customize 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-
187-You will need two mandatory directory mappings and a port mapping to allow SillyTavern to function. In the command, replace your selections in the following places:
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-
208-1. Open your Command Line
209-2. Run the following command in a folder where you want to store the configuration and data files:
210-
211-```bash
212-SILLYTAVERN_VERSION="latest"
213-PUBLIC_PORT="8000"
214-CONFIG_PATH="./config"
215-DATA_PATH="./data"
216-PLUGINS_PATH="./plugins"
217-EXTENSIONS_PATH="./extensions"
218-
219-docker 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-
233-We have a comprehensive guide on using SillyTavern in Docker [here](http://docs.sillytavern.app/installation/docker/) which covers installations on Windows, macOS and Linux! Give it a read if you wish to build the image yourself.
234-
235-### Common issues with Docker
236-
237-#### SELinux Permission Issues with Mounted Volumes
238-
239-Linux 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-
241-Two 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-
246-Example:
247-
248-```yaml
249-# docker-compose.yml
250-volumes:
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-
259-SillyTavern 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-
263-1. On your keyboard: press **`WINDOWS + R`** to open Run dialog box. Then, run the following command to install git:
264-
265-```shell
266-cmd /c winget install -e --id Git.Git
267-```
268-
269-2. On your keyboard: press **`WINDOWS + E`** to open File Explorer, then navigate to the folder where you want to install the launcher. Once in the desired folder, type `cmd` into the address bar and press enter. Then, run the following command:
270-
271-```shell
272-git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher && start installer.bat
273-```
274-
275-### For Linux users
276-
277-1. Open your favorite terminal and install git
278-2. Git clone the Sillytavern-Launcher with:
279-
280-```shell
281-git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
282-```
283-
284-3. Start the installer.sh with:
285-
286-```shell
287-chmod +x install.sh && ./install.sh
288-```
289-
290-4. After installation start the launcher.sh with:
291-
292-```shell
293-chmod +x launcher.sh && ./launcher.sh
294-```
295-
296-### For Mac users
297-
298-1. 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-
304-2. Install git with:
305-
306-```shell
307-brew install git
308-```
309-
310-3. Git clone the Sillytavern-Launcher with:
311-
312-```shell
313-git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
314-```
315-
316-4. Start the installer.sh with:
317-
318-```shell
319-chmod +x install.sh && ./install.sh
320-```
321-
322-5. After installation start the launcher.sh with:
323-
324-```shell
325-chmod +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-
341-You can pass command-line arguments to SillyTavern server startup to override some settings in `config.yaml`.
342-
343-### Examples
344-
345-```shell
346-node server.js --port 8000 --listen false
347-# or
348-npm run start -- --port 8000 --listen false
349-# or (Windows only)
350-Start.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-
387-Most 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
389-Read the detailed guide on how to set up remote connections in the [Docs](https://docs.sillytavern.app/usage/remoteconnections/).
64+For detailed installation instructions, please visit our documentation:
39065
391-You 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
39371## License and credits
39472
default/config.yaml+11 -2
@@ -100,7 +100,7 @@ disableCsrfProtection: false
100100securityOverride: false
101101# -- LOGGING CONFIGURATION --
102102logging:
103103 # Enable access logging to access.log file and console output
104104 # Records new connections with timestamp, IP address and user agent
105105 enableAccessLog: true
106106 # Minimum log level to display in the terminal (DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3)
@@ -139,7 +139,7 @@ thumbnails:
139139 # JPG thumbnail quality (0-100)
140140 quality: 95
141141 # Maximum thumbnail dimensions per type [width, height]
142142 dimensions: { 'bg': [160, 90], 'avatar': [96, 144], 'persona': [96, 144] }
143143
144144# PERFORMANCE-RELATED CONFIGURATION
145145performance:
@@ -151,6 +151,15 @@ performance:
151151 # Enables disk caching for character cards. Improves performances with large card libraries.
152152 useDiskCache: true
153153
154+# CACHE BUSTER CONFIGURATION
155+# IMPORTANT: Requires localhost or a domain with HTTPS, otherwise will not work!
156+cacheBuster:
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+
154163# Allow secret keys exposure via API
155164allowKeysExposure: false
156165# Skip new default content checks
default/content/index.json+20 -0
@@ -688,6 +688,10 @@
688688 "type": "instruct"
689689 },
690690 {
691+ "filename": "presets/instruct/KoboldAI.json",
692+ "type": "instruct"
693+ },
694+ {
691695 "filename": "presets/context/Llama-3-Instruct-Names.json",
692696 "type": "context"
693697 },
@@ -818,5 +822,21 @@
818822 {
819823 "filename": "presets/context/Dots1.json",
820824 "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"
821841 }
822842]
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+}
11 \ 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 @@
22 "chat_completion_source": "openai",
33 "openai_model": "gpt-4-turbo",
44 "claude_model": "claude-3-5-sonnet-20240620",
5- "windowai_model": "",
65 "openrouter_model": "OR_Website",
76 "openrouter_use_fallback": false,
87 "openrouter_group_models": false,
98 "openrouter_sort_models": "alphabetically",
109 "ai21_model": "jamba-1.5-large",
1110 "mistralai_model": "mistral-large-latest",
1211 "custom_model": "",
1312 "custom_url": "",
@@ -226,12 +225,10 @@
226225 ]
227226 }
228227 ],
229- "api_url_scale": "",
230228 "show_external_models": false,
231229 "assistant_prefill": "",
232230 "assistant_impersonation": "",
233231 "claude_use_sysprompt": false,
234- "use_alt_scale": false,
235232 "squash_system_messages": false,
236233 "image_inlining": false,
237234 "bypass_status_check": false,
default/content/settings.json+1 -2
@@ -623,8 +623,7 @@
623623 "wi_format": "{0}",
624624 "openai_model": "gpt-4-turbo",
625625 "claude_model": "claude-3-5-sonnet-20240620",
626626 "ai21_model": "jamba-1.5-large",
627- "windowai_model": "",
628627 "openrouter_model": "OR_Website",
629628 "reverse_proxy": "",
630629 "chat_completion_source": "openai",
index.d.ts+5 -0
@@ -66,4 +66,9 @@ declare global {
6666 * Parsed command line arguments.
6767 */
6868 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;
6974}
package-lock.json+53 -25
@@ -1,12 +1,12 @@
11{
22 "name": "sillytavern",
33 "version": "1.13.12",
44 "lockfileVersion": 3,
55 "requires": true,
66 "packages": {
77 "": {
88 "name": "sillytavern",
99 "version": "1.13.12",
1010 "hasInstallScript": true,
1111 "license": "AGPL-3.0",
1212 "dependencies": {
@@ -40,7 +40,7 @@
4040 "@popperjs/core": "^2.11.8",
4141 "@zeldafan0225/ai_horde": "^5.2.0",
4242 "archiver": "^7.0.1",
4343 "bing-translate-api": "^4.01.20",
4444 "body-parser": "^1.20.2",
4545 "bowser": "^2.11.0",
4646 "bytes": "^3.1.2",
@@ -55,8 +55,9 @@
5555 "diff-match-patch": "^1.0.5",
5656 "dompurify": "^3.2.6",
5757 "droll": "^0.2.1",
58+ "env-paths": "^3.0.0",
5859 "express": "^4.21.0",
5960 "form-data": "^4.0.34",
6061 "fuse.js": "^7.1.0",
6162 "google-translate-api-browser": "^3.0.1",
6263 "google-translate-api-x": "^10.7.2",
@@ -77,7 +78,7 @@
7778 "multer": "^2.0.2",
7879 "node-fetch": "^3.3.2",
7980 "node-persist": "^4.0.4",
8081 "open": "^10.1.2.0",
8182 "png-chunk-text": "^1.0.0",
8283 "png-chunks-extract": "^1.0.0",
8384 "proxy-agent": "^6.5.0",
@@ -95,13 +96,13 @@
9596 "wavefile": "^11.0.0",
9697 "webpack": "^5.98.0",
9798 "write-file-atomic": "^5.0.1",
9899 "ws": "^8.18.23",
99100 "yaml": "^2.8.0",
100101 "yargs": "^17.7.1",
101102 "yauzl": "^3.2.0"
102103 },
103104 "bin": {
104105 "sillytavern": "src/server-global.js"
105106 },
106107 "devDependencies": {
107108 "@types/archiver": "^6.0.3",
@@ -117,9 +118,9 @@
117118 "@types/jquery-cropper": "^1.0.4",
118119 "@types/jquery.transit": "^0.9.33",
119120 "@types/jqueryui": "^1.12.24",
120121 "@types/lodash": "^4.17.1720",
121122 "@types/mime-types": "^3.0.1",
122123 "@types/multer": "^12.40.130",
123124 "@types/node": "^18.19.84",
124125 "@types/node-persist": "^3.1.8",
125126 "@types/png-chunk-text": "^1.0.3",
@@ -1972,9 +1973,9 @@
19721973 }
19731974 },
19741975 "node_modules/@types/lodash": {
19751976 "version": "4.17.1720",
19761977 "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.1720.tgz",
19771978 "integrity": "sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNoH3MHACvFUEiujabxhaI/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
19781979 "dev": true,
19791980 "license": "MIT"
19801981 },
@@ -1999,9 +2000,9 @@
19992000 "license": "MIT"
20002001 },
20012002 "node_modules/@types/multer": {
20022003 "version": "12.40.130",
20032004 "resolved": "https://registry.npmjs.org/@types/multer/-/multer-12.40.130.tgz",
20042005 "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcawC3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==",
20052006 "dev": true,
20062007 "license": "MIT",
20072008 "dependencies": {
@@ -2832,9 +2833,9 @@
28322833 }
28332834 },
28342835 "node_modules/bing-translate-api": {
28352836 "version": "4.01.20",
28362837 "resolved": "https://registry.npmjs.org/bing-translate-api/-/bing-translate-api-4.01.20.tgz",
28372838 "integrity": "sha512-JJ8XUehnxzOhHU91oy86xEtp8OOMjVEjCZJX042fKxoO19NNvxJ5omeCcxQNFoPbDqVpBJwqiGVquL0oPdQm1QoP2663Yd5MXX4kbB/3LdS9YgPiE+ls9+2iFZH2ZXigWhWyHT3R4m6aCup4TNJd3/U4gqHHnQoxTaIW7uOf4+vA==",
28382839 "license": "MIT",
28392840 "dependencies": {
28402841 "got": "^11.8.6"
@@ -4087,6 +4088,18 @@
40874088 "url": "https://github.com/fb55/entities?sponsor=1"
40884089 }
40894090 },
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+ },
40904103 "node_modules/es-define-property": {
40914104 "version": "1.0.1",
40924105 "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -6420,15 +6433,15 @@
64206433 }
64216434 },
64226435 "node_modules/open": {
64236436 "version": "10.1.2.0",
64246437 "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.0.tgz",
64256438 "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oImYgBpdJHPyQ2UE5x+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXwhlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
64266439 "license": "MIT",
64276440 "dependencies": {
64286441 "default-browser": "^5.2.1",
64296442 "define-lazy-prop": "^3.0.0",
64306443 "is-inside-container": "^1.0.0",
64316444 "is-wsl-utils": "^30.1.0"
64326445 },
64336446 "engines": {
64346447 "node": ">=18"
@@ -8503,9 +8516,9 @@
85038516 }
85048517 },
85058518 "node_modules/ws": {
85068519 "version": "8.18.23",
85078520 "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.23.tgz",
85088521 "integrity": "sha512-DMricUmwGZUVr+PEIGCY5tSlUt50cqyMXfCzX+AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lpoOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQGARUDw2XV2N4tvzg==",
85098522 "license": "MIT",
85108523 "engines": {
85118524 "node": ">=10.0.0"
@@ -8523,6 +8536,21 @@
85238536 }
85248537 }
85258538 },
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+ },
85268554 "node_modules/xhr": {
85278555 "version": "2.6.0",
85288556 "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz",
package.json+10 -8
@@ -30,7 +30,7 @@
3030 "@popperjs/core": "^2.11.8",
3131 "@zeldafan0225/ai_horde": "^5.2.0",
3232 "archiver": "^7.0.1",
3333 "bing-translate-api": "^4.01.20",
3434 "body-parser": "^1.20.2",
3535 "bowser": "^2.11.0",
3636 "bytes": "^3.1.2",
@@ -45,8 +45,9 @@
4545 "diff-match-patch": "^1.0.5",
4646 "dompurify": "^3.2.6",
4747 "droll": "^0.2.1",
48+ "env-paths": "^3.0.0",
4849 "express": "^4.21.0",
4950 "form-data": "^4.0.34",
5051 "fuse.js": "^7.1.0",
5152 "google-translate-api-browser": "^3.0.1",
5253 "google-translate-api-x": "^10.7.2",
@@ -67,7 +68,7 @@
6768 "multer": "^2.0.2",
6869 "node-fetch": "^3.3.2",
6970 "node-persist": "^4.0.4",
7071 "open": "^10.1.2.0",
7172 "png-chunk-text": "^1.0.0",
7273 "png-chunks-extract": "^1.0.0",
7374 "proxy-agent": "^6.5.0",
@@ -85,7 +86,7 @@
8586 "wavefile": "^11.0.0",
8687 "webpack": "^5.98.0",
8788 "write-file-atomic": "^5.0.1",
8889 "ws": "^8.18.23",
8990 "yaml": "^2.8.0",
9091 "yargs": "^17.7.1",
9192 "yauzl": "^3.2.0"
@@ -111,10 +112,11 @@
111112 "type": "git",
112113 "url": "https://github.com/SillyTavern/SillyTavern.git"
113114 },
114115 "version": "1.13.12",
115116 "scripts": {
116117 "start": "node server.js",
117118 "debug": "node --inspect server.js",
119+ "start:global": "node server.js --global",
118120 "start:electron": "cd ./src/electron && npm run start",
119121 "start:deno": "deno run --allow-run --allow-net --allow-read --allow-write --allow-sys --allow-env server.js",
120122 "start:bun": "bun server.js",
@@ -126,7 +128,7 @@
126128 "plugins:install": "node plugins install"
127129 },
128130 "bin": {
129131 "sillytavern": "./src/server-global.js"
130132 },
131133 "rules": {
132134 "no-path-concat": "off",
@@ -147,9 +149,9 @@
147149 "@types/jquery-cropper": "^1.0.4",
148150 "@types/jquery.transit": "^0.9.33",
149151 "@types/jqueryui": "^1.12.24",
150152 "@types/lodash": "^4.17.1720",
151153 "@types/mime-types": "^3.0.1",
152154 "@types/multer": "^12.40.130",
153155 "@types/node": "^18.19.84",
154156 "@types/node-persist": "^3.1.8",
155157 "@types/png-chunk-text": "^1.0.3",
public/css/promptmanager.css+4 -0
@@ -112,6 +112,10 @@
112112 flex-direction: column;
113113}
114114
115+.completion_prompt_manager_popup_entry_form .select2-container {
116+ margin: 5px 0;
117+}
118+
115119#completion_prompt_manager_popup .completion_prompt_manager_popup_entry_form_control:has(#completion_prompt_manager_popup_entry_form_prompt) {
116120 flex: 1;
117121 display: flex;
public/css/select2-overrides.css+12 -59
@@ -19,14 +19,15 @@
1919.select2-container .select2-selection .select2-selection__clear {
2020 color: var(--SmartThemeBodyColor);
2121 font-size: 20px;
22- padding: 0;
2322 position: absolute;
2423 right: 5px;
25- top: 0;
24+ margin-top: 0; /* compensate for the increased font size */
25+ margin-right: 0;
2626}
2727
2828.select2-container .select2-search--inline textarea.select2-search__field {
2929 opacity: 0.8;
30+ margin-top: 7px; /* get placeholder text to the center */
3031}
3132
3233.select2-selection--single .select2-selection__placeholder {
@@ -39,8 +40,8 @@
3940
4041.select2-container .select2-selection--single .select2-selection__rendered {
4142 color: var(--SmartThemeBodyColor);
4243 line-height: revertunset;
4344 padding-left: unset3px 5px;
4445}
4546
4647.select2-container .select2-results>.select2-results__options {
@@ -63,9 +64,10 @@
6364 background-color: var(--black30a);
6465 color: var(--SmartThemeBodyColor);
6566 border: 1px solid var(--SmartThemeBorderColor);
66- border-radius: 7px;
6767 font-family: var(--mainFontFamily);
6868 padding: 3px 5px;
69+ display: unset; /* override the ST textarea display style */
70+ border-radius: 0; /* border radius was interfering with cursor */
6971}
7072
7173/* Customize the selected option */
@@ -93,7 +95,10 @@
9395 border: 1px solid var(--SmartThemeBorderColor);
9496 border-radius: 7px;
9597 font-family: var(--mainFontFamily);
96- padding: 3px 5px;
98+}
99+
100+.select2-container .select2-selection--multiple.select2-selection--clearable {
101+ padding-right: 20px;
97102}
98103
99104.select2-container.select2-container--focus .select2-selection--multiple,
@@ -230,55 +235,3 @@ span.select2.select2-container .select2-selection__choice__remove:hover {
230235.select2_choice_clickable_buttonstyle+span.select2-container .select2-selection__choice__display:hover {
231236 background-color: var(--white30a);
232237}
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 @@
470470 margin-left: 20px;
471471}
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-
481473.textarea_compact {
482474 font-size: calc(var(--mainFontSize) * 0.95);
483475 line-height: 1.2;
public/css/tags.css+3 -1
@@ -157,7 +157,9 @@
157157 flex-direction: row;
158158 flex-wrap: wrap;
159159 align-items: flex-start;
160160 margin-top: 5px;
161+ max-height: 40dvh;
162+ overflow-y: auto;
161163}
162164
163165.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,
9393body.charListGrid #rm_print_characters_block .group_select .group_select_counter,
9494#user_avatar_block.gridView .avatar-container .ch_name,
9595#user_avatar_block.gridView .avatar-container .bogus_folder_counter,
9696#user_avatar_block.gridView .avatar-container .group_select_counter {,
97+#user_avatar_block.gridView .avatar-container .ch_additional_info {
9798 width: 100%;
9899 max-width: 100px;
99100 text-align: center;
public/css/world-info.css+14 -1
@@ -107,7 +107,6 @@
107107 height: auto;
108108 margin-top: 0;
109109 margin-bottom: 0;
110- min-height: calc(var(--mainFontSize) + 14px);
111110}
112111
113112.delete_entry_button {
@@ -286,3 +285,17 @@ select.keyselect+span.select2-container .select2-selection--multiple {
286285.world_entry:has(input[name="delay_until_recursion"]:not(:checked)) .world_entry_form_control:has(input[name="delayUntilRecursionLevel"]) {
287286 display: none;
288287}
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 {
7373 * @param args - The arguments for the conversion function.
7474 */
7575 function convertVideoToAnimatedWebp(args: ConvertVideoArgs): Promise<Uint8Array>;
76+
77+ interface ColorPickerEvent extends JQuery.ChangeEvent<HTMLElement> {
78+ detail: {
79+ rgba: string;
80+ };
81+ }
7682}
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+115 -106
@@ -300,7 +300,7 @@
300300 </label>
301301 </div>
302302 </div>
303303 <small id="prompt_cost_block" data-tg-type="openrouter">
304304 <span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="or_prompt_cost">&ndash;</span>
305305 </small>
306306 </div>
@@ -691,7 +691,7 @@
691691 </span>
692692 </div>
693693 </div>
694694 <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">
695695 <div class="range-block-title" data-i18n="Temperature">
696696 Temperature
697697 </div>
@@ -743,7 +743,7 @@
743743 </div>
744744 </div>
745745 </div>
746746 <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,scale,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek,xai,pollinations">
747747 <div class="range-block-title" data-i18n="Top P">
748748 Top P
749749 </div>
@@ -1314,7 +1314,7 @@
13141314 <input class="neo-range-slider" type="range" id="epsilon_cutoff_textgenerationwebui" name="volume" min="0" max="9" step="0.01">
13151315 <input class="neo-range-input" type="number" min="0" max="9" step="0.01" data-for="epsilon_cutoff_textgenerationwebui" id="epsilon_cutoff_counter_textgenerationwebui">
13161316 </div>
13171317 <div data-tg-type="ooba,aphrodite,koboldcpp,llamacpp" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
13181318 <small>
13191319 <span data-i18n="Top nsigma">Top nsigma</span>
13201320 <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 @@
14991499 </div>
15001500 </div>
15011501 </div>
15021502 <div data-tg-type="ooba,infermaticai,koboldcpp,llamacpp,ollama,tabby" id="mirostat_block_ooba" class="wide100p">
15031503 <h4 class="wide100p textAlignCenter">
15041504 <label data-i18n="Mirostat (mode=1 is only for llama.cpp)">Mirostat</label>
15051505 <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 @@
18111811 <div data-name="temperature" draggable="true"><span>Temperature</span><small></small></div>
18121812 <div data-name="dynamic_temperature" draggable="true"><span>Dynamic Temperature</span><small></small></div>
18131813 <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>
18141815 <div data-name="top_k" draggable="true"><span>Top K</span><small></small></div>
18151816 <div data-name="top_p" draggable="true"><span>Top P</span><small></small></div>
18161817 <div data-name="typical_p" draggable="true"><span>Typical P</span><small></small></div>
@@ -1969,7 +1970,7 @@
19691970 </span>
19701971 </div>
19711972 </div>
19721973 <div class="range-block" data-source="makersuite,vertexai,aimlapi,openrouter,claude,xai,nanogpt">
19731974 <label for="openai_enable_web_search" class="checkbox_label flexWrap widthFreeExpand">
19741975 <input id="openai_enable_web_search" type="checkbox" />
19751976 <span data-i18n="Enable web search">Enable web search</span>
@@ -2078,7 +2079,7 @@
20782079 </span>
20792080 </div>
20802081 </div>
20812082 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations,perplexity">
20822083 <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.">
20832084 <label for="openai_reasoning_effort">
20842085 <span data-i18n="Reasoning Effort">Reasoning Effort</span>
@@ -2092,7 +2093,7 @@
20922093 <option data-i18n="openai_reasoning_effort_high" value="high">High</option>
20932094 <option data-i18n="openai_reasoning_effort_maximum" value="max">Maximum</option>
20942095 </select>
20952096 <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.">
20962097 OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.
20972098 </div>
20982099 <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 @@
21312132 </div>
21322133 </div>
21332134 </div>
21342135 <div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,scale,custom">
21352136 <div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias">
21362137 Logit Bias
21372138 </div>
@@ -2801,8 +2802,6 @@
28012802 <option value="openrouter">OpenRouter</option>
28022803 <option value="perplexity">Perplexity</option>
28032804 <option value="pollinations">Pollinations</option>
2804- <option value="scale">Scale</option>
2805- <option value="windowai">Window AI</option>
28062805 <option value="xai">xAI (Grok)</option>
28072806 </optgroup>
28082807 </select>
@@ -3022,34 +3021,11 @@
30223021 <option value="claude-3-5-haiku-latest">claude-3-5-haiku-latest</option>
30233022 <option value="claude-3-5-haiku-20241022">claude-3-5-haiku-20241022</option>
30243023 <option value="claude-3-opus-20240229">claude-3-opus-20240229</option>
3025- <option value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
30263024 <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>
30293025 </optgroup>
30303026 </select>
30313027 </div>
30323028 </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>
30533029 <form id="openrouter_form" data-source="openrouter" action="javascript:void(null);" method="post" enctype="multipart/form-data">
30543030 <h4 data-i18n="OpenRouter API Key">OpenRouter API Key</h4>
30553031 <div>
@@ -3119,36 +3095,6 @@
31193095 <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>
31203096 </small>
31213097 </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>
31523098 <form id="ai21_form" data-source="ai21" action="javascript:void(null);" method="post" enctype="multipart/form-data">
31533099 <h4 data-i18n="AI21 API Key">AI21 API Key</h4>
31543100 <div class="flex-container">
@@ -3165,7 +3111,11 @@
31653111 <option value="jamba-mini">jamba-mini</option>
31663112 <option value="jamba-large">jamba-large</option>
31673113 </optgroup>
31683114 <optgroup label="Jamba 1.67">
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)">
31693119 <option value="jamba-1.6-mini">jamba-1.6-mini</option>
31703120 <option value="jamba-1.6-large">jamba-1.6-large</option>
31713121 </optgroup>
@@ -3415,6 +3365,7 @@
34153365 <option value="pixtral-12b-latest">pixtral-12b-latest</option>
34163366 <option value="pixtral-large-latest">pixtral-large-latest</option>
34173367 <option value="devstral-small-latest">devstral-small-latest</option>
3368+ <option value="devstral-medium-latest">devstral-medium-latest</option>
34183369 <option value="magistral-small-latest">magistral-small-latest</option>
34193370 <option value="magistral-medium-latest">magistral-medium-latest</option>
34203371 </optgroup>
@@ -3430,6 +3381,7 @@
34303381 <option value="mistral-small-2409">mistral-small-2409</option>
34313382 <option value="mistral-small-2501">mistral-small-2501</option>
34323383 <option value="mistral-small-2503">mistral-small-2503</option>
3384+ <option value="mistral-small-2506">mistral-small-2506</option>
34333385 <option value="mistral-medium-2312">mistral-medium-2312</option>
34343386 <option value="mistral-medium-2505">mistral-medium-2505</option>
34353387 <option value="mistral-large-2402">mistral-large-2402</option>
@@ -3446,8 +3398,12 @@
34463398 <option value="pixtral-12b-2409">pixtral-12b-2409</option>
34473399 <option value="pixtral-large-2411">pixtral-large-2411</option>
34483400 <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>
34493403 <option value="magistral-small-2506">magistral-small-2506</option>
34503404 <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>
34513407 </optgroup>
34523408 <optgroup id="mistralai_other_models" label="Other"></optgroup>
34533409 </select>
@@ -3464,35 +3420,17 @@
34643420 </div>
34653421 <h4 data-i18n="Groq Model">Groq Model</h4>
34663422 <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>
3469- <option value="qwen-2.5-coder-32b">qwen-2.5-coder-32b</option>
3470- </optgroup>
3471- <optgroup label="DeepSeek">
3472- <option value="deepseek-r1-distill-qwen-32b">deepseek-r1-distill-qwen-32b</option>
34733424 <option value="deepseek-r1-distill-llama-70b">deepseek-r1-distill-llama-70b</option>
3474- </optgroup>
3475- <optgroup label="Google">
34763425 <option value="gemma2-9b-it">gemma2-9b-it</option>
3477- </optgroup>
3478- <optgroup label="Meta">
34793426 <option value="meta-llama/llama-4-scout-17b-16e-instruct">meta-llama/llama-4-scout-17b-16e-instruct</option>
34803427 <option value="meta-llama/llama-4-maverick-17b-128e-instruct">meta-llama/llama-4-maverick-17b-128e-instruct</option>
34813428 <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>
34873429 <option value="llama-3.3-70b-versatile">llama-3.3-70b-versatile </option>
34883430 <option value="llama-guard-3-8b">llama-guard-3-8b</option>
34893431 <option value="llama3-70b-8192">llama3-70b-8192</option>
34903432 <option value="llama3-8b-8192">llama3-8b-8192</option>
3491- </optgroup>
3492- <optgroup label="Mistral AI">
34933433 <option value="mistral-saba-24b">mistral-saba-24b</option>
3494- <option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
3495- </optgroup>
34963434 </select>
34973435 </div>
34983436 <div id="nanogpt_form" data-source="nanogpt">
@@ -3545,6 +3483,7 @@
35453483 <option value="sonar-pro">sonar-pro</option>
35463484 <option value="sonar-reasoning">sonar-reasoning</option>
35473485 <option value="sonar-reasoning-pro">sonar-reasoning-pro</option>
3486+ <option value="sonar-deep-research">sonar-deep-research</option>
35483487 </optgroup>
35493488 <optgroup label="Offline Models">
35503489 <option value="r1-1776">r1-1776</option>
@@ -3659,6 +3598,7 @@
36593598 </div>
36603599 <h4 data-i18n="xAI Model">xAI Model</h4>
36613600 <select id="model_xai_select">
3601+ <option value="grok-4-0709">grok-4-0709</option>
36623602 <option value="grok-3-beta">grok-3-beta</option>
36633603 <option value="grok-3-fast-beta">grok-3-fast-beta</option>
36643604 <option value="grok-3-mini-beta">grok-3-mini-beta</option>
@@ -3728,7 +3668,7 @@
37283668 <div class="api_loading menu_button menu_button_icon" data-i18n="Cancel">Cancel</div>
37293669 <div data-source="custom" id="customize_additional_parameters" class="menu_button menu_button_icon" data-i18n="Additional Parameters">Additional Parameters</div>
37303670 <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>
37313671 <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!"><span data-i18n="Test Message">Test Message</span></div>
37323672 </div>
37333673 <div class="online_status">
37343674 <div class="online_status_indicator"></div>
@@ -4241,6 +4181,13 @@
42414181 </div>
42424182 <div>
42434183 <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>
42444191 <div>
42454192 <small>
42464193 <span data-i18n="Non-markdown strings">
@@ -4307,7 +4254,7 @@
43074254 </span>
43084255 </div>
43094256 <div class="range-block-range">
43104257 <select id="world_info" class="select2_multi_sameline" multiple>
43114258 <option value="" data-i18n="-- World Info not found --">-- World Info not found -- </option>
43124259 </select>
43134260 </div>
@@ -5522,7 +5469,6 @@
55225469 <div id="HotSwapWrapper" class="alignitemscenter flex-container margin0auto wide100p">
55235470 <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">
55245471 </div>
5525- <hr class="wide100p">
55265472 </div>
55275473 </div>
55285474
@@ -5638,19 +5584,26 @@
56385584 </div>
56395585 </div>
56405586 </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">
56435589 <div id="creators_notes_div" class="title_restorable flexGap5 wide100p ">
56445590 <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>
56465591 <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>
56475592 <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>
56485593 </div>
5649- <div id="creator_notes_spoiler" class="flex1"></div>
5594+ <div class="flex-container widthFitContent">
5650- <!-- A button to show / hide description_div and description_textarea and first_message_div and firstmessage_textarea-->
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>
56515603 </div>
5604+ </div>
5605+ <small id="creators_note_desc_hidden" data-i18n="Character details are hidden.">Character details are hidden.</small>
56525606 <div id="descriptionWrapper" class="flex-container flexFlowColumn flex1">
5653- <hr>
56545607 <div id="description_div" class="title_restorable">
56555608 <div class="flex-container alignitemscenter">
56565609 <span data-i18n="Character Description" class="mdhotkey_location">Description</span>
@@ -5843,20 +5796,22 @@
58435796 </div>
58445797 <div id="rm_character_import" class="right_menu" style="display: none;">
58455798 <form id="form_import" action="javascript:void(null);" method="post" enctype="multipart/form-data">
58465799 <input multiple type="file" id="character_import_file" accept=".json, image/png, .yaml, .yml, .charx, .byaf" name="avatar">
58475800 <input id="character_import_file_type" name="file_type" class="text_pole" value="" autocomplete="off">
58485801 </form>
58495802 <input type="file" id="character_replace_file" accept=".json, image/png, .yaml, .yml, .charx, .byaf" name="replace_avatar" hidden>
58505803 </div>
58515804 <div name="Character List Panel" id="rm_characters_block" class="right_menu">
58525805 <div id="charListFixedTop">
58535806 <formdiv id="form_character_search_form" action="javascript:void(null);rm_button_bar">
58545807 <div id="rm_button_create" title="Create New Character" data-i18n="[title]Create New Character" class="menu_button fa-solid fa-user-plus"></div>
58555808 <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>
58565809 <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>
58575810 <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">
58605815 <option data-field="search" data-order="desc" data-i18n="Search" hidden>Search</option>
58615816 <option data-field="name" data-order="asc" data-i18n="A-Z">A-Z</option>
58625817 <option data-field="name" data-order="desc" data-i18n="Z-A">Z-A</option>
@@ -5870,12 +5825,15 @@
58705825 <option data-field="data_size" data-order="asc" data-i18n="Least tokens">Least tokens</option>
58715826 <option data-field="name" data-order="random" data-i18n="Random">Random</option>
58725827 </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>
58745833 <div class="rm_tag_controls">
58755834 <div class="tags rm_tag_filter"></div>
58765835 <div class="tags rm_tag_bogus_drilldown"></div>
58775836 </div>
5878- <hr>
58795837 </div>
58805838
58815839 <div id="rm_print_characters_pagination">
@@ -5927,6 +5885,7 @@
59275885 <div class="flex-container wide100pLess70px character_select_container">
59285886 <div class="wide100p character_name_block">
59295887 <span class="ch_name flex1"></span>
5888+ <small class="ch_additional_info"></small>
59305889 </div>
59315890 <div class="ch_description"></div>
59325891 <div class="avatar_container_states buttons_block">
@@ -6273,7 +6232,7 @@
62736232 </span>
62746233 </small>
62756234 <small class="textAlignCenter" data-i18n="Primary Keywords">Primary Keywords</small>
62766235 <select class="keyprimaryselect keyselect select2_multi_sameline" name="key" data-i18n="[placeholder]Keywords or Regexes" placeholder="Keywords or Regexes" multiple="multiple"></select>
62776236 <textarea class="text_pole keyprimarytextpole mobile" name="key" rows="1" data-i18n="[placeholder]Comma separated list" placeholder="Comma separated list" style="display: none;"></textarea>
62786237 <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">
62796238 ⌨️
@@ -6295,7 +6254,7 @@
62956254 </span>
62966255 </small>
62976256 <small class="textAlignCenter" data-i18n="Optional Filter">Optional Filter</small>
62986257 <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>
62996258 <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>
63006259 <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">
63016260 ⌨️
@@ -6482,13 +6441,37 @@
64826441 </label>
64836442 </div>
64846443 <div class="range-block-range">
64856444 <select name="characterFilter" class="select2_multi_sameline" multiple>
64866445 <option value="" data-i18n="-- Characters not found --">
64876446 -- Characters not found --
64886447 </option>
64896448 </select>
64906449 </div>
64916450 </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>
64926475 </div>
64936476 <div name="WIEntryBottomControls" class="flex-container flex1 justifySpaceBetween world_entry_form_horizontal">
64946477 <div class="flex-container flexFlowColumn flexNoGap wi-enter-footer-text">
@@ -6719,6 +6702,22 @@
67196702 </select>
67206703 <div class="text_muted" data-i18n="To whom this message will be attributed.">To whom this message will be attributed.</div>
67216704 </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>
67226721 </div>
67236722 <div class="flex-container gap10px">
67246723 <div class="completion_prompt_manager_popup_entry_form_control flex1">
@@ -7066,6 +7065,16 @@
70667065 <div class="right_menu_button mes_file_delete fa-solid fa-trash-can" title="Remove the file" data-i18n="[title]Remove the file"></div>
70677066 </div>
70687067 </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>
70697078 </div>
70707079 <div id="movingDivs">
70717080 <div id="floatingPrompt" class="drawer-content flexGap5">
public/lib.js+3 -0
@@ -21,6 +21,7 @@ import droll from 'droll';
2121import morphdom from 'morphdom';
2222import { toggle as slideToggle } from 'slidetoggle';
2323import chalk from 'chalk';
24+import yaml from 'yaml';
2425
2526/**
2627 * Expose the libraries to the 'window' object.
@@ -98,6 +99,7 @@ export default {
9899 morphdom,
99100 slideToggle,
100101 chalk,
102+ yaml,
101103};
102104
103105export {
@@ -121,4 +123,5 @@ export {
121123 morphdom,
122124 slideToggle,
123125 chalk,
126+ yaml,
124127};
public/locales/ar-sa.json+1 -3
@@ -375,9 +375,6 @@
375375 "Group by vendors Description": "ضع نماذج OpenAI في مجموعة واحدة، والنماذج الإنسانية في مجموعة أخرى، وما إلى ذلك. ويمكن دمجها مع الفرز.",
376376 "Allow fallback routes": "السماح بمسارات الاحتياط",
377377 "Allow fallback routes Description": "يختار النموذج البديل تلقائيًا إذا كان النموذج المحدد غير قادر على تلبية طلبك.",
378- "Scale API Key": "مفتاح API لـ Scale",
379- "Clear your cookie": "امسح ملف تعريف الارتباط الخاص بك",
380- "Alt Method": "طريقة بديلة",
381378 "AI21 API Key": "مفتاح API لـ AI21",
382379 "AI21 Model": "نموذج AI21",
383380 "Google AI Studio API Key": "مفتاح واجهة برمجة تطبيقات Google AI Studio",
@@ -1380,6 +1377,7 @@
13801377 "char_import_7": "للمضيفين المسموح بهم)",
13811378 "char_import_8": "شخصية RisuRealm (رابط مباشر)",
13821379 "char_import_9": "شخصية Soulkyn (رابط مباشر)",
1380+ "char_import_10": "شخصية Perchance (رابط مباشر أو UUID + .gz)",
13831381 "Supports importing multiple characters.": "يدعم استيراد أحرف متعددة.",
13841382 "Write each URL or ID into a new line.": "اكتب كل عنوان URL أو معرف في سطر جديد.",
13851383 "Export for character": "تصدير للشخصية",
public/locales/de-de.json+1 -3
@@ -375,9 +375,6 @@
375375 "Group by vendors Description": "Platzieren Sie OpenAI-Modelle in einer Gruppe, anthropogene Modelle in einer anderen Gruppe usw. Kann mit Sortierung kombiniert werden.",
376376 "Allow fallback routes": "Fallback-Routen zulassen",
377377 "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",
381378 "AI21 API Key": "AI21 API-Schlüssel",
382379 "AI21 Model": "AI21-Modell",
383380 "Google AI Studio API Key": "Google AI Studio API-Schlüssel",
@@ -1380,6 +1377,7 @@
13801377 "char_import_7": "für erlaubte Hosts)",
13811378 "char_import_8": "RisuRealm-Charakter (Direktlink)",
13821379 "char_import_9": "Soulkyn-Charakter (Direktlink)",
1380+ "char_import_10": "Perchance-Charakter (Direktlink oder UUID + .gz)",
13831381 "Supports importing multiple characters.": "Unterstützt den Import mehrerer Zeichen.",
13841382 "Write each URL or ID into a new line.": "Schreiben Sie jede URL oder ID in eine neue Zeile.",
13851383 "Export for character": "Export für Zeichen",
public/locales/es-es.json+1 -3
@@ -375,9 +375,6 @@
375375 "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.",
376376 "Allow fallback routes": "Permitir rutas de respaldo",
377377 "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",
381378 "AI21 API Key": "Clave API de AI21",
382379 "AI21 Model": "Modelo de AI21",
383380 "Google AI Studio API Key": "Clave API de Google AI Studio",
@@ -1380,6 +1377,7 @@
13801377 "char_import_7": "para hosts permitidos)",
13811378 "char_import_8": "Personaje RisuRealm (Enlace directo)",
13821379 "char_import_9": "Personaje Soulkyn (Enlace directo)",
1380+ "char_import_10": "Personaje Perchance (enlace directo o UUID + .gz)",
13831381 "Supports importing multiple characters.": "Admite la importación de múltiples caracteres.",
13841382 "Write each URL or ID into a new line.": "Escriba cada URL o ID en una nueva línea.",
13851383 "Export for character": "Exportar para personaje",
public/locales/fr-fr.json+1 -3
@@ -355,9 +355,6 @@
355355 "Group by vendors": "Regrouper par fournisseurs",
356356 "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.",
357357 "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",
361358 "AI21 API Key": "Clé API AI21",
362359 "AI21 Model": "Modèle AI21",
363360 "Google AI Studio API Key": "Clé API Google AI Studio",
@@ -1302,6 +1299,7 @@
13021299 "char_import_7": "pour les hôtes autorisés)",
13031300 "char_import_8": "Personnage de RisuRealm (lien direct)",
13041301 "char_import_9": "Personnage de Soulkyn (lien direct)",
1302+ "char_import_10": "Personnage de Perchance (lien direct ou UUID + .gz)",
13051303 "Supports importing multiple characters.": "Prend en charge l'importation de plusieurs caractères.",
13061304 "Write each URL or ID into a new line.": "Écrivez chaque URL ou identifiant dans une nouvelle ligne.",
13071305 "Export for character": "Exportation pour le personnage",
public/locales/is-is.json+1 -3
@@ -375,9 +375,6 @@
375375 "Group by vendors Description": "Setjið OpenAI módel í einn hóp, Anthropic módel í annan hóp osfrv. Hægt að sameina við flokkun.",
376376 "Allow fallback routes": "Leyfa bakfallssvæði",
377377 "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",
381378 "AI21 API Key": "Lykill API fyrir AI21",
382379 "AI21 Model": "AI21 Módel",
383380 "Google AI Studio API Key": "Google AI Studio API lykill",
@@ -1380,6 +1377,7 @@
13801377 "char_import_7": "fyrir leyfilega gestgjafa)",
13811378 "char_import_8": "RisuRealm karakter (beinn hlekkur)",
13821379 "char_import_9": "Soulkyn karakter (beinn hlekkur)",
1380+ "char_import_10": "Perchance karakter (beinn hlekkur eða UUID + .gz)",
13831381 "Supports importing multiple characters.": "Styður innflutning á mörgum stöfum.",
13841382 "Write each URL or ID into a new line.": "Skrifaðu hverja vefslóð eða auðkenni í nýja línu.",
13851383 "Export for character": "Flytja út fyrir persónu",
public/locales/it-it.json+1 -3
@@ -375,9 +375,6 @@
375375 "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.",
376376 "Allow fallback routes": "Consenti percorsi alternativi",
377377 "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",
381378 "AI21 API Key": "Chiave API di AI21",
382379 "AI21 Model": "Modello AI21",
383380 "Google AI Studio API Key": "Chiave API Google AI Studio",
@@ -1380,6 +1377,7 @@
13801377 "char_import_7": "per gli host consentiti)",
13811378 "char_import_8": "Personaggio RisuRealm (collegamento diretto)",
13821379 "char_import_9": "Personaggio Soulkyn (collegamento diretto)",
1380+ "char_import_10": "Perchance Personaggio (collegamento diretto o UUID + .gz)",
13831381 "Supports importing multiple characters.": "Supporta l'importazione di più caratteri.",
13841382 "Write each URL or ID into a new line.": "Scrivi ogni URL o ID in una nuova riga.",
13851383 "Export for character": "Esporta per carattere",
public/locales/ja-jp.json+1 -3
@@ -375,9 +375,6 @@
375375 "Group by vendors Description": "OpenAI モデルを 1 つのグループに、Anthropic モデルを別のグループに配置するなどします。ソートと組み合わせることができます。",
376376 "Allow fallback routes": "フォールバックルートを許可",
377377 "Allow fallback routes Description": "選択したモデルが要求を満たせない場合、代替モデルが自動的に選択されます。",
378- "Scale API Key": "ScaleのAPIキー",
379- "Clear your cookie": "クッキーを消去する",
380- "Alt Method": "代替手法",
381378 "AI21 API Key": "AI21のAPIキー",
382379 "AI21 Model": "AI21モデル",
383380 "Google AI Studio API Key": "Google AI Studio APIキー",
@@ -1382,6 +1379,7 @@
13821379 "char_import_7": "許可されたホストの場合)",
13831380 "char_import_8": "RisuRealm キャラクター (直接リンク)",
13841381 "char_import_9": "Soulkyn キャラクター (直接リンク)",
1382+ "char_import_10": "Perchance キャラクター (直接リンクまたは UUID + .gz)",
13851383 "Supports importing multiple characters.": "複数のキャラクターのインポートをサポートします。",
13861384 "Write each URL or ID into a new line.": "各 URL または ID を新しい行に入力します。",
13871385 "Export for character": "キャラクターのエクスポート",
public/locales/ko-kr.json+1 -3
@@ -384,9 +384,6 @@
384384 "LEGACY": "유산",
385385 "Force Instruct Mode formatting": "강제 지시 모드 형식",
386386 "Force_Instruct_Mode_formatting_Description": "Instruct Mode와 이 모드가 모두 활성화된 경우 프롬프트는 SillyTavern에 의해 현재 형식을 사용하여 형식화됩니다.\n 고급 형식 설정(시스템 프롬프트 지시 제외) 비활성화된 경우 프롬프트는 OpenRouter에 의해 형식화됩니다.",
387- "Scale API Key": "Scale API 키",
388- "Clear your cookie": "쿠키 지우기",
389- "Alt Method": "대체 방법",
390387 "AI21 API Key": "AI21 API 키",
391388 "AI21 Model": "AI21 모델",
392389 "Google AI Studio API Key": "Google AI Studio API 키",
@@ -1399,6 +1396,7 @@
13991396 "char_import_7": "허용된 호스트의 경우)",
14001397 "char_import_8": "RisuRealm 캐릭터 (직접링크)",
14011398 "char_import_9": "Soulkyn 캐릭터 (직접링크)",
1399+ "char_import_10": "Perchance 캐릭터 (직접 링크 또는 UUID + .gz)",
14021400 "Supports importing multiple characters.": "여러 문자 가져오기를 지원합니다.",
14031401 "Write each URL or ID into a new line.": "각 URL 또는 ID를 새 줄에 작성합니다.",
14041402 "Export for character": "캐릭터 내보내기",
public/locales/nl-nl.json+1 -3
@@ -375,9 +375,6 @@
375375 "Group by vendors Description": "Plaats OpenAI-modellen in één groep, antropische modellen in een andere groep, enz. Kan worden gecombineerd met sorteren.",
376376 "Allow fallback routes": "Fallback-routes toestaan",
377377 "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",
381378 "AI21 API Key": "AI21 API-sleutel",
382379 "AI21 Model": "AI21-model",
383380 "MakerSuite API Key": "MakerSuite API-sleutel",
@@ -1380,6 +1377,7 @@
13801377 "char_import_7": "voor toegestane hosts)",
13811378 "char_import_8": "RisuRealm-personage (directe link)",
13821379 "char_import_9": "Soulkyn-personage (directe link)",
1380+ "char_import_10": "Perchance-personage (directe link of UUID + .gz)",
13831381 "Supports importing multiple characters.": "Ondersteunt het importeren van meerdere tekens.",
13841382 "Write each URL or ID into a new line.": "Schrijf elke URL of ID op een nieuwe regel.",
13851383 "Export for character": "Exporteren voor karakter",
public/locales/pt-pt.json+1 -3
@@ -375,9 +375,6 @@
375375 "Group by vendors Description": "Coloque os modelos OpenAI em um grupo, os modelos antrópicos em outro grupo, etc.",
376376 "Allow fallback routes": "Permitir rotas de fallback",
377377 "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",
381378 "AI21 API Key": "Chave da API AI21",
382379 "AI21 Model": "Modelo AI21",
383380 "Google AI Studio API Key": "Chave API Google AI Studio",
@@ -1380,6 +1377,7 @@
13801377 "char_import_7": "para hosts permitidos)",
13811378 "char_import_8": "Personagem RisuRealm (link direto)",
13821379 "char_import_9": "Personagem Soulkyn (link direto)",
1380+ "char_import_10": "Personagem Perchance (Link Direto ou UUID + .gz)",
13831381 "Supports importing multiple characters.": "Suporta importação de vários caracteres.",
13841382 "Write each URL or ID into a new line.": "Escreva cada URL ou ID em uma nova linha.",
13851383 "Export for character": "Exportar para personagem",
public/locales/ru-ru.json+1 -5
@@ -142,8 +142,6 @@
142142 "Get your key from": "Получите ключ в",
143143 "Anthropic's developer console": "консоли разработчика Anthropic",
144144 "Claude Model": "Модель Claude",
145- "Scale API Key": "Ключ от Scale API",
146- "Alt Method": "Альтернативный метод",
147145 "AI21 API Key": "Ключ от API AI21",
148146 "AI21 Model": "Модель AI21",
149147 "View API Usage Metrics": "Посмотреть статистику использования API",
@@ -966,6 +964,7 @@
966964 "char_import_6": "Прямая ссылка на PNG-файл (список разрешённых хостов находится в",
967965 "char_import_7": ")",
968966 "char_import_9": "Персонаж с Soulkyn (прямая ссылка)",
967+ "char_import_10": "Персонаж с Perchance (прямая ссылка или UUID + .gz)",
969968 "Grammar String": "Грамматика",
970969 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF или EBNF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.",
971970 "Account": "Аккаунт",
@@ -1254,7 +1253,6 @@
12541253 "vLLM Model": "Модель vLLM",
12551254 "Aphrodite Model": "Модель Aphrodite",
12561255 "Peek a password": "Посмотреть пароль",
1257- "Clear your cookie": "Clear your cookie",
12581256 "Add Chat Start and Example Separator to a list of stopping strings.": "Использовать Начало чата и Разделитель примеров сообщений в качестве стоп-строк.",
12591257 "Context Order": "Context Order",
12601258 "Summary": "Summary",
@@ -1748,8 +1746,6 @@
17481746 "dot quota_error": "имеется достаточно кредитов.",
17491747 "If you have sufficient credits, please try again later.": "Если кредитов достаточно, то повторите попытку позднее.",
17501748 "Proxy preset '${0}' not found": "Пресет '${0}' не найден",
1751- "Window.ai returned an error": "Window.ai ответил ошибкой",
1752- "Extension is not installed": "Расширение не установлено",
17531749 "Update or remove your reverse proxy settings.": "Измените или удалите ваши настройки прокси.",
17541750 "An error occurred while importing prompts. More info available in console.": "В процессе импорта произошла ошибка. Подробную информацию см. в консоли.",
17551751 "Could not import prompts. Export failed validation.": "Не удалось импортировать промпты. Не пройдена валидация при экспорте.",
public/locales/uk-ua.json+1 -3
@@ -375,9 +375,6 @@
375375 "Group by vendors Description": "Помістіть моделі OpenAI в одну групу, моделі Anthropic в іншу групу тощо. Можна поєднати з сортуванням.",
376376 "Allow fallback routes": "Дозволити резервні маршрути",
377377 "Allow fallback routes Description": "Автоматично вибирає альтернативну модель, якщо вибрана модель не може задовольнити ваш запит.",
378- "Scale API Key": "Ключ API для Scale",
379- "Clear your cookie": "Очистіть файл cookie",
380- "Alt Method": "Альтернативний метод",
381378 "AI21 API Key": "Ключ API для AI21",
382379 "AI21 Model": "Модель AI21",
383380 "Google AI Studio API Key": "Ключ API Google AI Studio",
@@ -1380,6 +1377,7 @@
13801377 "char_import_7": "для дозволених хостів)",
13811378 "char_import_8": "Персонаж RisuRealm (пряме посилання)",
13821379 "char_import_9": "Персонаж Soulkyn (пряме посилання)",
1380+ "char_import_10": "Персонаж Perchance (пряме посилання або UUID + .gz)",
13831381 "Supports importing multiple characters.": "Підтримується імпорт кількох символів.",
13841382 "Write each URL or ID into a new line.": "Напишіть кожну URL-адресу або ідентифікатор у новому рядку.",
13851383 "Export for character": "Експорт для персонажа",
public/locales/vi-vn.json+1 -3
@@ -375,9 +375,6 @@
375375 "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.",
376376 "Allow fallback routes": "Cho phép các tuyến đường phụ",
377377 "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ế",
381378 "AI21 API Key": "AI21 API Key",
382379 "AI21 Model": "Model của AI21",
383380 "Google AI Studio API Key": "Google AI Studio API Key",
@@ -1380,6 +1377,7 @@
13801377 "char_import_7": "đối với các máy chủ được phép)",
13811378 "char_import_8": "RisuRealm (URL trực tiếp)",
13821379 "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)",
13831381 "Supports importing multiple characters.": "Hỗ trợ nhập nhiều ký tự.",
13841382 "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.",
13851383 "Export for character": "Xuất cho nhân vật",
public/locales/zh-cn.json+1 -3
@@ -429,9 +429,6 @@
429429 "Group by vendors": "按厂商分组",
430430 "Group by vendors Description": "将 OpenAI 模型放在一组,将 Anthropic 模型放在另一组,等等。可以与排序结合。",
431431 "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": "备用方法",
435432 "AI21 API Key": "AI21 API 密钥",
436433 "AI21 Model": "AI21 模型",
437434 "Google AI Studio API Key": "Google AI Studio API 密钥",
@@ -1894,6 +1891,7 @@
18941891 "char_import_7": ")",
18951892 "char_import_8": "RisuRealm 角色(直链)",
18961893 "char_import_9": "Soulkyn 角色(直链)",
1894+ "char_import_10": "Perchance 角色(直链或UUID + .gz)",
18971895 "Supports importing multiple characters.": "支持导入多个角色。",
18981896 "Write each URL or ID into a new line.": "将每个 URL 或 ID 写入新行。",
18991897 "Enter the Git URL of the extension to install": "输入扩展程序的 Git URL 以安装",
public/locales/zh-tw.json+1 -103
@@ -376,9 +376,6 @@
376376 "Group by vendors Description": "將 OpenAI、Anthropic 等模型放各自供應商的群組中。可以與排序功能結合使用。",
377377 "Allow fallback routes": "允許備援路徑",
378378 "Allow fallback routes Description": "如果選擇的模型無法滿足要求,會自動選擇替代模型。",
379- "Scale API Key": "Scale API 金鑰",
380- "Clear your cookie": "清除您的 Cookie",
381- "Alt Method": "替代方法",
382379 "AI21 API Key": "AI21 API 金鑰",
383380 "AI21 Model": "AI21 模型",
384381 "Google AI Studio API Key": "Google AI Studio API 金鑰",
@@ -1385,6 +1382,7 @@
13851382 "char_import_7": "對於允許的主機)",
13861383 "char_import_8": "RisuRealm 角色(直接連結)",
13871384 "char_import_9": "Soulkyn 角色(直接連結)",
1385+ "char_import_10": "Perchance 角色(直接連結或 ID + .gz)",
13881386 "Supports importing multiple characters.": "支援匯入多個字元。",
13891387 "Write each URL or ID into a new line.": "將每個 URL 或 ID 寫入新行。",
13901388 "Export for character": "匯出字元",
@@ -2515,106 +2513,6 @@
25152513 "Note: Themes can be made/applied by going to the ": "注意:主題可通過前往以下位置進行創建/應用",
25162514 "Theme Name": "主題名稱",
25172515 "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.": "重新摘要已手動編輯的訊息記憶",
26182516 "Type the folder name of the theme you want to apply.": "輸入您想套用的主題資料夾名稱。",
26192517 "Place your theme data in a folder.": "請將主題資料存於該資料夾內。",
26202518 "Unsure where to start? Type ": "不確定如何開始?輸入:",
public/script.js+598 -2001
@@ -1,12 +1,9 @@
11import {
22 showdown,
33 moment,
4- Fuse,
54 DOMPurify,
65 hljs,
7- localforage,
86 Handlebars,
9- DiffMatchPatch,
107 SVGInject,
118 Popper,
129 initLibraryShims,
@@ -19,10 +16,11 @@ import {
1916 generateKoboldWithStreaming,
2017 kai_settings,
2118 loadKoboldSettings,
22- formatKoboldUrl,
2319 getKoboldGenerationData,
2420 kai_flags,
2521 setKoboldFlagskoboldai_settings,
22+ koboldai_setting_names,
23+ initKoboldSettings,
2624} from './scripts/kai-settings.js';
2725
2826import {
@@ -31,10 +29,9 @@ import {
3129 generateTextGenWithStreaming,
3230 getTextGenGenerationData,
3331 textgen_types,
34- getTextGenServer,
35- validateTextGenUrl,
3632 parseTextgenLogprobs,
3733 parseTabbyLogprobs,
34+ initTextGenSettings,
3835} from './scripts/textgen-settings.js';
3936
4037import {
@@ -91,7 +88,6 @@ import {
9188 sortEntitiesList,
9289 registerDebugFunction,
9390 flushEphemeralStoppingStrings,
94- context_presets,
9591 resetMovableStyles,
9692 forceCharacterEditorTokenize,
9793 applyPowerUserSettings,
@@ -120,13 +116,13 @@ import {
120116 generateNovelWithStreaming,
121117 getNovelGenerationData,
122118 getKayraMaxContextTokens,
123- getNovelTier,
124- loadNovelPreset,
125119 loadNovelSettings,
126120 nai_settings,
127121 adjustNovelInstructionPrompt,
128- loadNovelSubscriptionData,
129122 parseNovelAILogprobs,
123+ novelai_settings,
124+ novelai_setting_names,
125+ initNovelAISettings,
130126} from './scripts/nai-settings.js';
131127
132128import {
@@ -139,9 +135,10 @@ import {
139135 horde_settings,
140136 loadHordeSettings,
141137 generateHorde,
142138 checkHordeStatusgetStatusHorde,
143139 getHordeModels,
144140 adjustHordeGenerationParams,
141+ isHordeGenerationNotAllowed,
145142 MIN_LENGTH,
146143 initHorde,
147144} from './scripts/horde.js';
@@ -168,7 +165,6 @@ import {
168165 isValidUrl,
169166 ensureImageFormatSupported,
170167 flashHighlight,
171- isTrueBoolean,
172168 toggleDrawer,
173169 isElementInViewport,
174170 copyText,
@@ -180,10 +176,10 @@ import {
180176 renderPaginationDropdown,
181177 paginationDropdownChangeHandler,
182178} from './scripts/utils.js';
183179import { debounce_timeout, GENERATION_TYPE_TRIGGERS, IGNORE_SYMBOL } from './scripts/constants.js';
184180
185181import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors, saveMetadataDebounced } from './scripts/extensions.js';
186182import { COMMENT_NAME_DEFAULT, executeSlashCommandsOnChatInputCONNECT_API_MAP, getSlashCommandsHelpexecuteSlashCommandsOnChatInput, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommandsstopScriptExecution, stopScriptExecutionUNIQUE_APIS } from './scripts/slash-commands.js';
187183import {
188184 tag_map,
189185 tags,
@@ -205,15 +201,9 @@ import {
205201 applyTagsOnCharacterSelect,
206202 applyTagsOnGroupSelect,
207203 tag_import_setting,
204+ applyCharacterTagsToMessageDivs,
208205} from './scripts/tags.js';
209-import {
206+import { initSecrets, readSecretState } from './scripts/secrets.js';
210- SECRET_KEYS,
211- initSecrets,
212- readSecretState,
213- secret_state,
214- writeSecret,
215-} from './scripts/secrets.js';
216-import { EventEmitter } from './lib/eventemitter.js';
217207import { markdownExclusionExt } from './scripts/showdown-exclusion.js';
218208import { markdownUnderscoreExt } from './scripts/showdown-underscore.js';
219209import { NOTE_MODULE_NAME, initAuthorsNote, metadata_keys, setFloatingPrompt, shouldWIAddPrompt } from './scripts/authors-note.js';
@@ -228,14 +218,10 @@ import {
228218 formatInstructModePrompt,
229219 formatInstructModeExamples,
230220 getInstructStoppingSequences,
231- autoSelectInstructPreset,
232221 formatInstructModeSystemPrompt,
233- selectInstructPreset,
234- instruct_presets,
235- selectContextPreset,
236222} from './scripts/instruct-mode.js';
237223import { initLocales, t } from './scripts/i18n.js';
238224import { getFriendlyTokenizerName, getTokenCount, getTokenCountAsync, initTokenizers, saveTokenCache, TOKENIZER_SUPPORTED_KEY } from './scripts/tokenizers.js';
239225import {
240226 user_avatar,
241227 getUserAvatars,
@@ -249,38 +235,19 @@ import {
249235} from './scripts/personas.js';
250236import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';
251237import { hideLoader, showLoader } from './scripts/loader.js';
252238import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';
253-import {
239+import { initTextGenModels } from './scripts/textgen-models.js';
254- loadFeatherlessModels,
240+import { 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';
267-import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat, formatCreatorNotes, initChatUtilities } from './scripts/chats.js';
268241import { getPresetManager, initPresetManager } from './scripts/preset-manager.js';
269242import { evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js';
270243import { currentUser, setUserControls } from './scripts/user.js';
271244import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
272245import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
273246import { initScrapers } from './scripts/scrapers.js';
274-import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';
275-import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';
276-import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './scripts/slash-commands/SlashCommandArgument.js';
277-import { SlashCommandBrowser } from './scripts/slash-commands/SlashCommandBrowser.js';
278247import { initCustomSelectedSamplers, validateDisabledSamplers } from './scripts/samplerSelect.js';
279248import { DragAndDropHandler } from './scripts/dragdrop.js';
280249import { INTERACTABLE_CONTROL_CLASS, initKeyboard } from './scripts/keyboard.js';
281250import { initDynamicStyles } from './scripts/dynamic-styles.js';
282-import { SlashCommandEnumValue, enumTypes } from './scripts/slash-commands/SlashCommandEnumValue.js';
283-import { commonEnumProviders, enumIcons } from './scripts/slash-commands/SlashCommandCommonEnumsProvider.js';
284251import { initInputMarkdown } from './scripts/input-md-formatting.js';
285252import { AbortReason } from './scripts/util/AbortReason.js';
286253import { initSystemPrompts } from './scripts/sysprompt.js';
@@ -291,12 +258,14 @@ import { applyBrowserFixes } from './scripts/browser-fixes.js';
291258import { initServerHistory } from './scripts/server-history.js';
292259import { initSettingsSearch } from './scripts/setting-search.js';
293260import { initBulkEdit } from './scripts/bulk-edit.js';
294-import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
295261import { getContext } from './scripts/st-context.js';
296262import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';
297263import { accountStorage } from './scripts/util/AccountStorage.js';
298264import { initWelcomeScreen, openPermanentAssistantChat, openPermanentAssistantCard, getPermanentAssistantAvatar } from './scripts/welcome-screen.js';
299265import { initDataMaid } from './scripts/data-maid.js';
266+import { clearItemizedPrompts, deleteItemizedPrompts, findItemizedPromptSet, initItemizedPrompts, itemizedParams, itemizedPrompts, loadItemizedPrompts, promptItemize, replaceItemizedPromptText, saveItemizedPrompts } from './scripts/itemized-prompts.js';
267+import { getSystemMessageByType, initSystemMessages, SAFETY_CHAT, sendSystemMessage, system_message_types, system_messages } from './scripts/system-messages.js';
268+import { event_types, eventSource } from './scripts/events.js';
300269
301270// API OBJECT FOR EXTERNAL WIRING
302271globalThis.SillyTavern = {
@@ -304,7 +273,6 @@ globalThis.SillyTavern = {
304273 getContext,
305274};
306275
307-//exporting functions and vars for mods
308276export {
309277 user_avatar,
310278 setUserAvatar,
@@ -314,6 +282,27 @@ export {
314282 isOdd,
315283 countOccurrences,
316284 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,
317306};
318307
319308/**
@@ -351,225 +340,7 @@ toastr.options = {
351340 },
352341};
353342
354-// Allow target="_blank" in links
355-DOMPurify.addHook('afterSanitizeAttributes', function (node) {
356- if ('target' in node) {
357- node.setAttribute('target', '_blank');
358- node.setAttribute('rel', 'noopener');
359- }
360-});
361-
362-DOMPurify.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-
389-DOMPurify.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
480-export 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-
565-export const eventSource = new EventEmitter([event_types.APP_READY]);
566-
567-eventSource.on(event_types.CHAT_CHANGED, processChatSlashCommands);
568-
569343export const characterGroupOverlay = new BulkEditOverlay();
570-const characterContextMenu = new CharacterContextMenu(characterGroupOverlay);
571-eventSource.on(event_types.CHARACTER_PAGE_LOADED, characterGroupOverlay.onPageLoad);
572-console.debug('Character context menu initialized', characterContextMenu);
573344
574345// Markdown converter
575346export 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
577348export let converter;
578349
579350// array for prompt token calculations
580-console.debug('initializing Prompt Itemization Array on Startup');
581-const promptStorage = localforage.createInstance({ name: 'SillyTavern_Prompts' });
582-export let itemizedPrompts = [];
583351
584352export const systemUserName = 'SillyTavern System';
585353export const neutralCharacterName = 'Assistant';
@@ -652,25 +420,6 @@ export const saveCharacterDebounced = debounce(() => $('#create_button').trigger
652420export const printCharactersDebounced = debounce(() => { printCharacters(false); }, DEFAULT_PRINT_TIMEOUT);
653421
654422/**
655- * @enum {string} System message types
656- */
657-export 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-/**
674423 * @enum {number} Extension prompt types
675424 */
676425export const extension_prompt_types = {
@@ -691,106 +440,6 @@ export const extension_prompt_roles = {
691440
692441export const MAX_INJECTION_DEPTH = 10000;
693442
694-// Initialized in getSystemMessages()
695-const SAFETY_CHAT = [];
696-
697-export let system_messages = {};
698-
699-async 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-
794443async function getClientVersion() {
795444 try {
796445 const response = await fetch('/version');
@@ -882,6 +531,7 @@ export let create_save = {
882531 depth_prompt_depth: depth_prompt_depth_default,
883532 depth_prompt_role: depth_prompt_role_default,
884533 extensions: {},
534+ extra_books: [],
885535};
886536
887537//animation right menu
@@ -892,8 +542,6 @@ let popup_type = '';
892542let chat_file_for_del = '';
893543export let online_status = 'no_connection';
894544
895-export let api_server = '';
896-
897545export let is_send_press = false; //Send generation
898546
899547let this_del_mes = -1;
@@ -904,9 +552,6 @@ var this_edit_mes_id;
904552
905553//settings
906554export let settings;
907-export let koboldai_settings;
908-export let koboldai_setting_names;
909-var preset_settings = 'gui';
910555export let amount_gen = 80; //default max length of AI generated responses
911556export let max_context = 2048;
912557
@@ -914,9 +559,6 @@ var swipes = true;
914559export let extension_prompts = {};
915560
916561export let main_api;// = "kobold";
917-//novel settings
918-export let novelai_settings;
919-export let novelai_setting_names;
920562/** @type {AbortController} */
921563let abortController;
922564
@@ -927,8 +569,6 @@ var kobold_horde_model = '';
927569
928570export let token;
929571
930-var PromptArrayItemForRawPromptDisplay;
931-var priorPromptArrayItemForRawPromptDisplay;
932572
933573/** The tag of the active character. (NOT the id) */
934574export let active_character = '';
@@ -937,11 +577,17 @@ export let active_group = '';
937577
938578export const entitiesFilter = new FilterHelper(printCharactersDebounced);
939579
940580export function getRequestHeaders({ omitContentType = false } = {}) {
941581 returnconst headers = {
942582 'Content-Type': 'application/json',
943583 'X-CSRF-Token': token,
944584 };
585+
586+ if (omitContentType) {
587+ delete headers['Content-Type'];
588+ }
589+
590+ return headers;
945591}
946592
947593export function getSlideToggleOptions() {
@@ -993,6 +639,7 @@ async function firstLoadInit() {
993639 initStandaloneMode();
994640 initLibraryShims();
995641 addShowdownPatch(showdown);
642+ addDOMPurifyHooks();
996643 reloadMarkdownProcessor();
997644 applyBrowserFixes();
998645 await getClientVersion();
@@ -1003,12 +650,15 @@ async function firstLoadInit() {
1003650 initDefaultSlashCommands();
1004651 initTextGenModels();
1005652 initOpenAI();
653+ initTextGenSettings();
654+ initKoboldSettings();
655+ initNovelAISettings();
1006656 initSystemPrompts();
1007657 initExtensions();
1008658 initExtensionSlashCommands();
1009659 ToolManager.initToolSlashCommands();
1010660 await initPresetManager();
1011661 await getSystemMessagesinitSystemMessages();
1012662 await getSettings();
1013663 initKeyboard();
1014664 initDynamicStyles();
@@ -1037,6 +687,7 @@ async function firstLoadInit() {
1037687 await initScrapers();
1038688 initCustomSelectedSamplers();
1039689 initDataMaid();
690+ initItemizedPrompts();
1040691 addDebugFunctions();
1041692 doDailyExtensionUpdatesCheck();
1042693 await hideLoader();
@@ -1101,303 +752,6 @@ export function setActiveGroup(entityOrKey) {
1101752 if (active_group) active_character = null;
1102753}
1103754
1104-/**
1105- * Gets the itemized prompts for a chat.
1106- * @param {string} chatId Chat ID to load
1107- */
1108-export 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- */
1130-export 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- */
1148-export 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- */
1166-export 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- */
1181-export async function clearItemizedPrompts() {
1182- try {
1183- await promptStorage.clear();
1184- itemizedPrompts = [];
1185- } catch {
1186- console.log('Error clearing itemized prompts');
1187- }
1188-}
1189-
1190-async 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-
1202-async 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-
1248-async 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-
1385-async 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-
1401755export function startStatusLoading() {
1402756 $('.api_loading').show();
1403757 $('.api_button').addClass('disabled');
@@ -1836,6 +1190,12 @@ function getCharacterSource(chId = this_chid) {
18361190 return `https://realm.risuai.net/character/${realmId}`;
18371191 }
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+
18391199 return '';
18401200}
18411201
@@ -1845,7 +1205,7 @@ export async function getCharacters() {
18451205 headers: getRequestHeaders(),
18461206 body: JSON.stringify({}),
18471207 });
18481208 if (response.ok === true) {
18491209 const previousAvatar = this_chid !== undefined ? characters[this_chid]?.avatar : null;
18501210 characters.splice(0, characters.length);
18511211 const getData = await response.json();
@@ -1874,6 +1234,12 @@ export async function getCharacters() {
18741234
18751235 await getGroups();
18761236 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+ }
18771243 }
18781244}
18791245
@@ -2080,7 +1446,7 @@ export async function clearChat() {
20801446 } else { console.debug('saw no avatars'); }
20811447
20821448 await saveItemizedPrompts(getCurrentChatId());
20831449 itemizedPrompts.length = []0;
20841450}
20851451
20861452export async function deleteLastMessage() {
@@ -2496,20 +1862,20 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) {
24961862 eventSource.emit(event_types.IMAGE_SWIPED, { message: mes, element: messageElement, direction: 'right' });
24971863 });
24981864 }
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');
24991870 }
25001871
25011872 // Add video to message
25021873 if (mes.extra?.video) {
25031874 const container = messageElement.find$('#message_video_template .mes_blockmes_video_container').clone();
1875+ messageElement.find('.mes_video_container').remove();
1876+ messageElement.find('.mes_block').append(container);
25041877 const chatHeight = $('#chat').prop('scrollHeight');
2505-
1878+ 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-
25131879 video.off('loadedmetadata').on('loadedmetadata', function () {
25141880 if (!adjustScroll) {
25151881 return;
@@ -2521,6 +1887,8 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) {
25211887 });
25221888
25231889 video.attr('src', mes.extra?.video);
1890+ } else {
1891+ messageElement.find('.mes_video_container').remove();
25241892 }
25251893
25261894 // Add file to message
@@ -2553,6 +1921,9 @@ export function addCopyToCodeBlocks(messageElement) {
25531921 copyButton.classList.add('fa-solid', 'fa-copy', 'code-copy', 'interactable');
25541922 copyButton.title = 'Copy code';
25551923 codeBlocks.get(i).appendChild(copyButton);
1924+ copyButton.addEventListener('click', function (e) {
1925+ e.stopPropagation();
1926+ });
25561927 copyButton.addEventListener('pointerup', async function () {
25571928 const text = codeBlocks.get(i).innerText;
25581929 await copyText(text);
@@ -2590,7 +1961,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
25901961 mes.swipes = [mes.mes];
25911962 }
25921963
25931964 let avatarImg = getUserAvatargetThumbnailUrl('persona', user_avatar);
25941965 const isSystem = mes.is_system;
25951966 const title = mes.title;
25961967 generatedPromptCache = '';
@@ -2740,6 +2111,8 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
27402111 if (!insertAfter && !insertBefore && scroll) {
27412112 scrollChatToBottom();
27422113 }
2114+
2115+ applyCharacterTagsToMessageDivs({ mesIds: newMessageId });
27432116}
27442117
27452118/**
@@ -2963,35 +2336,43 @@ export function getStoppingStrings(isImpersonate, isContinue) {
29632336
29642337/**
29652338 * Background generation based on the provided prompt.
29662339 * @paramtypedef {stringobject} quiet_prompt Instruction prompt for the AIGenerateQuietPromptParams
2967- * @param {boolean} quietToLoud Whether the message should be sent in a foreground (loud) or background (quiet) mode
2340+ * @prop {string} [quietPrompt] Instruction prompt for the AI
29682341 * @paramprop {boolean} skipWIAN[quietToLoud] whetherWhether tothe skipmessage additionshould ofbe Worldsent Infoin anda Author'sforeground Note(loud) intoor thebackground prompt(quiet) mode
29692342 * @paramprop {stringboolean} quietImage[skipWIAN] ImageWhether to useskip foraddition theof quietWorld Info and Author's Note into the prompt
29702343 * @paramprop {string} quietName[quietImage] NameImage to use for the quiet prompt (defaults to "System:")
29712344 * @paramprop {numberstring} [responseLengthquietName] Maximum responseName length.to Ifuse unset,for the globalquiet defaultprompt value(defaults isto used."System:")
29722345 * @paramprop {number} force_chid Character[responseLength] IDMaximum toresponse uselength. forIf thisunset, generationthe run.global Worksdefault invalue groupsis onlyused.
2973- * @returns
2346+ * @prop {number} [forceChId] Character ID to use for this generation run. Works in groups only.
2974- */
2347+ * @prop {object} [jsonSchema] JSON schema to use for the structured generation. Usually requires a special instruction.
2975-export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, quietImage = null, quietName = null, responseLength = null, force_chid = null) {
2348+ * @param {GenerateQuietPromptParams} params Parameters for the quiet prompt generation
2976- console.log('got into genQuietPrompt');
2349+ * @returns {Promise<string>} Generated text. If using structured output, will contain a serialized JSON object.
2350+ */
2351+export async function generateQuietPrompt({ quietPrompt = '', quietToLoud = false, skipWIAN = false, quietImage = null, quietName = null, responseLength = null, forceChId = null, jsonSchema = null } = {}) {
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+
29772357 const responseLengthCustomized = typeof responseLength === 'number' && responseLength > 0;
29782358 let eventHook = () => { };
29792359 try {
29802360 /** @type {GenerateOptions} */
29812361 const optionsgenerateOptions = {
2982- quiet_prompt,
2362+ quiet_prompt: quietPrompt ?? '',
2983- quietToLoud,
2363+ quietToLoud: quietToLoud ?? false,
29842364 skipWIAN: skipWIAN ?? false,
29852365 force_name2: true,
29862366 quietImage: quietImage ?? null,
29872367 quietName: quietName ?? null,
29882368 force_chid: force_chidforceChId ?? null,
2369+ jsonSchema: jsonSchema ?? null,
29892370 };
29902371 if (responseLengthCustomized) {
29912372 TempResponseLength.save(main_api, responseLength);
29922373 eventHook = TempResponseLength.setupEventHook(main_api);
29932374 }
29942375 const result = await Generate('quiet', optionsgenerateOptions);
29952376 return removeReasoningFromString(result);
29962377 } finally {
29972378 if (responseLengthCustomized && TempResponseLength.isCustomized()) {
@@ -3017,60 +2398,6 @@ export async function processCommands(message) {
30172398}
30182399
30192400/**
3020- * Gets a system message by type.
3021- * @param {string} type Type of system message
3022- * @param {string} [text] Text to be sent
3023- * @param {object} [extra] Additional data to be added to the message
3024- * @returns {object} System message object
3025- */
3026-export function getSystemMessageByType(type, text, extra = {}) {
3027- const systemMessage = system_messages[type];
3028-
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- */
3058-export 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-/**
30742401 * Extracts the contents of bias macros from a message.
30752402 * @param {string} message Message text
30762403 * @returns {string} Message bias extracted from the message (or an empty string if not found)
@@ -3368,11 +2695,9 @@ export function parseMesExamples(examplesStr, isInstruct) {
33682695}
33692696
33702697export function isStreamingEnabled() {
3371- const noStreamSources = [chat_completion_sources.SCALE];
33722698 return (
33732699 (main_api == 'openai' &&
33742700 oai_settings.stream_openai &&
3375- !noStreamSources.includes(oai_settings.chat_completion_source) &&
33762701 !(oai_settings.chat_completion_source == chat_completion_sources.OPENAI && ['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model))
33772702 )
33782703 || (main_api == 'kobold' && kai_settings.streaming_kobold && kai_flags.can_use_streaming)
@@ -3547,7 +2872,7 @@ class StreamingProcessor {
35472872
35482873 // Token count update.
35492874 const tokenCountText = this.reasoningHandler.reasoning + processedText;
35502875 const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCountawait getTokenCountAsync(tokenCountText, 0) : 0;
35512876 if (currentTokenCount) {
35522877 chat[messageId]['extra']['token_count'] = currentTokenCount;
35532878 if (this.messageTokenCounterDom instanceof HTMLElement) {
@@ -3561,7 +2886,7 @@ class StreamingProcessor {
35612886 'send_date': chat[messageId]['send_date'],
35622887 'gen_started': chat[messageId]['gen_started'],
35632888 'gen_finished': chat[messageId]['gen_finished'],
35642889 'extra': JSON.parse(JSON.stringifystructuredClone(chat[messageId]['extra'])),
35652890 };
35662891 }
35672892
@@ -3666,7 +2991,7 @@ class StreamingProcessor {
36662991 'send_date': chat[messageId]['send_date'],
36672992 'gen_started': chat[messageId]['gen_started'],
36682993 'gen_finished': chat[messageId]['gen_finished'],
36692994 'extra': JSON.parse(JSON.stringifystructuredClone(chat[messageId]['extra'])),
36702995 };
36712996 }
36722997 }
@@ -3740,37 +3065,101 @@ class StreamingProcessor {
37403065}
37413066
37423067/**
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 from
3069+ * @param {string | object[]} prompt Input prompt. Can be a string or an array of chat-style messages, i.e. [{role: '', content: ''}, ...]
37453070 * @param {string} api API to use. Main API is used if not specified.
37463071 * @param {boolean} instructOverride true to override instruct mode, false to use the default value
37473072 * @param {boolean} quietToLoud true to generate a message in system mode, false to generate a message in character mode
37483073 * @param {string} [systemPrompt] System prompt to use. Only Instruct mode or OpenAI.
37493074 * @param {numberstring} [responseLengthprefill] Maximum response length. IfPrefill unset,for the global default value is usedprompt.
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+ */
3077+export 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
37513145 * @returns {Promise<string>} Generated message
37523146 */
37533147export 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+
37543153 if (!api) {
37553154 api = main_api;
37563155 }
37573156
37583157 const abortController = new AbortController();
37593158 const responseLengthCustomized = typeof responseLength === 'number' && responseLength > 0;
3760- const isInstruct = power_user.instruct.enabled && api !== 'openai' && api !== 'novel' && !instructOverride;
3761- const isQuiet = true;
37623159 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
37753164 try {
37763165 if (responseLengthCustomized) {
@@ -3782,12 +3171,12 @@ export async function generateRaw(prompt, api, instructOverride, quietToLoud, sy
37823171 switch (api) {
37833172 case 'kobold':
37843173 case 'koboldhorde':
37853174 if (kai_settings.preset_settings === 'gui') {
37863175 generateData = { prompt: prompt, gui_settings: true, max_length: amount_gen, max_context_length: max_context, api_server: kai_settings.api_server };
37873176 } else {
37883177 const isHorde = api === 'koboldhorde';
37893178 const koboldSettings = koboldai_settings[koboldai_setting_names[kai_settings.preset_settings]];
37903179 generateData = getKoboldGenerationData(prompt.toString(), koboldSettings, amount_gen, max_context, isHorde, 'quiet');
37913180 }
37923181 TempResponseLength.restore(api);
37933182 break;
@@ -3802,10 +3191,7 @@ export async function generateRaw(prompt, api, instructOverride, quietToLoud, sy
38023191 TempResponseLength.restore(api);
38033192 break;
38043193 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- }
38093195 eventHook = TempResponseLength.setupEventHook(api);
38103196 } break;
38113197 }
@@ -3813,9 +3199,9 @@ export async function generateRaw(prompt, api, instructOverride, quietToLoud, sy
38133199 let data = {};
38143200
38153201 if (api === 'koboldhorde') {
38163202 data = await generateHorde(prompt.toString(), generateData, abortController.signal, false);
38173203 } else if (api === 'openai') {
38183204 data = await sendOpenAIRequest('quiet', generateData, abortController.signal, { jsonSchema });
38193205 } else {
38203206 const generateUrl = getGenerateUrl(api);
38213207 const response = await fetch(generateUrl, {
@@ -3840,6 +3226,10 @@ export async function generateRaw(prompt, api, instructOverride, quietToLoud, sy
38403226 throw new Error(data.response);
38413227 }
38423228
3229+ if (jsonSchema) {
3230+ return extractJsonFromData(data, { mainApi: api });
3231+ }
3232+
38433233 // format result, exclude user prompt bias
38443234 const message = cleanUpMessage({
38453235 getMessage: extractMessageFromData(data),
@@ -3972,15 +3362,35 @@ function removeLastMessage() {
39723362}
39733363
39743364/**
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+/**
39753386 * MARK:Generate()
39763387 * Runs a generation using the current chat context.
39773388 * @param {string} type Generation type
39783389 * @param {GenerateOptions} options Generation options
39793390 * @param {boolean} dryRun Whether to actually generate a message or just assemble the prompt
39803391 * @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
39823392 */
39833393export async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName, jsonSchema = null, depth = 0 } = {}, dryRun = false) {
39843394 console.log('Generate entered');
39853395 setGenerationProgress(0);
39863396 generation_started = new Date();
@@ -4134,7 +3544,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
41343544 'impersonate',
41353545 'quiet',
41363546 'continue',
4137- 'ask_command',
41383547 ];
41393548 //for normal messages sent from user..
41403549 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
43163725 // Make quiet prompt available for WIAN
43173726 setExtensionPrompt('QUIET_PROMPT', quiet_prompt || '', extension_prompt_types.IN_PROMPT, 0, true);
43183727 const chatForWI = coreChat.map(x => world_info_include_names ? `${x.name}: ${x.mes}` : x.mes).reverse();
3728+ /** @type {import('./scripts/world-info.js').WIGlobalScanData} */
43193729 const globalScanData = {
43203730 personaDescription: persona,
43213731 characterDescription: description,
@@ -4323,6 +3733,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
43233733 characterDepthPrompt: charDepthPrompt,
43243734 scenario: scenario,
43253735 creatorNotes: creatorNotes,
3736+ trigger: GENERATION_TYPE_TRIGGERS.includes(type) ? type : 'normal',
43263737 };
43273738 const { worldInfoString, worldInfoBefore, worldInfoAfter, worldInfoExamples, worldInfoDepth } = await getWorldInfoPrompt(chatForWI, this_max_context, dryRun, globalScanData);
43283739 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
49374348 gui_settings: true,
49384349 max_length: maxLength,
49394350 max_context_length: max_context,
49404351 api_server: kai_settings.api_server,
49414352 };
49424353
49434354 if (kai_settings.preset_settings != 'gui') {
49444355 const isHorde = main_api == 'koboldhorde';
49454356 const presetSettings = koboldai_settings[koboldai_setting_names[kai_settings.preset_settings]];
49464357 const maxContext = (adjustedParams && horde_settings.auto_adjust_context_length) ? adjustedParams.maxContextLength : max_context;
49474358 generate_data = getKoboldGenerationData(finalPrompt, presetSettings, maxLength, maxContext, isHorde, type);
49484359 }
@@ -4974,7 +4385,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
49744385 cyclePrompt: cyclePrompt,
49754386 systemPromptOverride: system,
49764387 jailbreakPromptOverride: jailbreak,
4977- personaDescription: persona,
49784388 messages: oaiMessages,
49794389 messageExamples: oaiMessageExamples,
49804390 }, dryRun);
@@ -5122,7 +4532,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
51224532 });
51234533 }
51244534 } else {
51254535 return await sendGenerationRequest(type, generate_data, { jsonSchema });
51264536 }
51274537 }
51284538
@@ -5154,6 +4564,12 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
51544564 throw new Error(data?.response);
51554565 }
51564566
4567+ if (jsonSchema) {
4568+ unblockGeneration(type);
4569+ generatedPromptCache = '';
4570+ return extractJsonFromData(data);
4571+ }
4572+
51574573 //const getData = await response.json();
51584574 let getMessage = extractMessageFromData(data);
51594575 let title = extractTitleFromData(data);
@@ -5567,7 +4983,7 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul
55674983
55684984 // Lock user avatar to a persona.
55694985 if (avatar in power_user.personas) {
55704986 message.force_avatar = getUserAvatargetThumbnailUrl('persona', avatar);
55714987 }
55724988
55734989 if (messageBias) {
@@ -5680,7 +5096,7 @@ function addChatsSeparator(mesSendString) {
56805096 }
56815097}
56825098
56835099export async function duplicateCharacter() {
56845100 if (this_chid === undefined || !characters[this_chid]) {
56855101 toastr.warning(t`You must first select a character to duplicate!`);
56865102 return '';
@@ -5710,231 +5126,6 @@ async function duplicateCharacter() {
57105126 return '';
57115127}
57125128
5713-export 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-
5832-export 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-
5851-async 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-
59385129function setInContextMessages(msgInContextCount, type) {
59395130 $('#chat .mes').removeClass('lastInContext');
59405131
@@ -5956,15 +5147,21 @@ function setInContextMessages(msgInContextCount, type) {
59565147}
59575148
59585149/**
5150+ * @typedef {object} AdditionalRequestOptions
5151+ * @property {JsonSchema} [jsonSchema]
5152+ */
5153+
5154+/**
59595155 * Sends a non-streaming request to the API.
59605156 * @param {string} type Generation type
59615157 * @param {object} data Generation data
5158+ * @param {AdditionalRequestOptions} [options] Additional options for the generation request
59625159 * @returns {Promise<object>} Response data from the API
59635160 * @throws {Error|object}
59645161 */
59655162export async function sendGenerationRequest(type, data, options = {}) {
59665163 if (main_api === 'openai') {
59675164 return await sendOpenAIRequest(type, data.prompt, abortController.signal, options);
59685165 }
59695166
59705167 if (main_api === 'koboldhorde') {
@@ -5990,16 +5187,17 @@ export async function sendGenerationRequest(type, data) {
59905187 * Sends a streaming request to the API.
59915188 * @param {string} type Generation type
59925189 * @param {object} data Generation data
5190+ * @param {AdditionalRequestOptions} [options] Additional options for the generation request
59935191 * @returns {Promise<any>} Streaming generator
59945192 */
59955193export async function sendStreamingRequest(type, data, options = {}) {
59965194 if (abortController?.signal?.aborted) {
59975195 throw new Error('Generation was aborted.');
59985196 }
59995197
60005198 switch (main_api) {
60015199 case 'openai':
60025200 return await sendOpenAIRequest(type, data.prompt, streamingProcessor.abortController.signal, options);
60035201 case 'textgenerationwebui':
60045202 return await generateTextGenWithStreaming(data, streamingProcessor.abortController.signal);
60055203 case 'novel':
@@ -6141,6 +5339,58 @@ export function extractMessageFromData(data, activeApi = null) {
61415339}
61425340
61435341/**
5342+ * Extracts JSON from the response data.
5343+ * @param {object} data Response data
5344+ * @returns {string} Extracted JSON string from the response data
5345+ */
5346+export 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+/**
61445394 * Extracts multiswipe swipes from the response data.
61455395 * @param {Object} data Response data
61465396 * @param {string} type Type of generation
@@ -6551,7 +5801,7 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
65515801 send_date: item['send_date'],
65525802 gen_started: item['gen_started'],
65535803 gen_finished: item['gen_finished'],
65545804 extra: JSON.parse(JSON.stringifystructuredClone(item['extra'])),
65555805 };
65565806 } else {
65575807 item['swipe_id'] = 0;
@@ -6561,7 +5811,7 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
65615811 send_date: chat[chat.length - 1]['send_date'],
65625812 gen_started: chat[chat.length - 1]['gen_started'],
65635813 gen_finished: chat[chat.length - 1]['gen_finished'],
65645814 extra: JSON.parse(JSON.stringifystructuredClone(chat[chat.length - 1]['extra'])),
65655815 };
65665816 }
65675817
@@ -7196,7 +6446,7 @@ async function read_avatar_load(input) {
71966446 await delay(DEFAULT_SAVE_EDIT_TIMEOUT);
71976447
71986448 const formData = new FormData(/** @type {HTMLFormElement} */($('#form_create').get(0)));
71996449 await fetch(getThumbnailUrl('avatar', formData.get('avatar_url').toString()), {
72006450 method: 'GET',
72016451 cache: 'no-cache',
72026452 headers: {
@@ -7225,8 +6475,15 @@ async function read_avatar_load(input) {
72256475 }
72266476}
72276477
7228-export 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+ */
6485+export function getThumbnailUrl(type, file, t = false) {
6486+ return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}${t ? `&t=${Date.now()}` : ''}`;
72306487}
72316488
72326489export 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
72666523 }
72676524 else if (entity.type === 'persona') {
72686525 avatarTemplate.attr({ 'data-pid': id, 'data-chid': null });
72696526 avatarTemplate.find('img').attr('src', getUserAvatargetThumbnailUrl('persona', entity.item.avatar));
72706527 avatarTemplate.attr('title', `[Persona] ${entity.item.name}\nFile: ${entity.item.avatar}`);
72716528 }
72726529
@@ -7518,6 +6775,8 @@ export function changeMainAPI() {
75186775 $('#ai_module_block_novel').css('display', 'none');
75196776 }
75206777
6778+ $('#prompt_cost_block').toggle(selectedVal === 'textgenerationwebui');
6779+
75216780 // Hide common settings for OpenAI
75226781 console.debug('value?', selectedVal);
75236782 if (selectedVal == 'openai') {
@@ -7530,10 +6789,6 @@ export function changeMainAPI() {
75306789 main_api = selectedVal;
75316790 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-
75376792 if (main_api == 'koboldhorde') {
75386793 getStatusHorde();
75396794 getHordeModels(true);
@@ -7611,59 +6866,7 @@ export async function getSettings() {
76116866 // Allow subscribers to mutate settings
76126867 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-
76656869 //Load AI model config settings
7666-
76676870 amount_gen = settings.amount_gen;
76686871 if (settings.max_context !== undefined)
76696872 max_context = parseInt(settings.max_context);
@@ -7674,16 +6877,14 @@ export async function getSettings() {
76746877 showSwipeButtons();
76756878
76766879 // Kobold
76776880 loadKoboldSettings(data, settings.kai_settings ?? settings, settings);
76786881
76796882 // Novel
76806883 loadNovelSettings(data, settings.nai_settings ?? settings);
7681- $(`#settings_preset_novel option[value=${novelai_setting_names[nai_settings.preset_settings_novel]}]`).attr('selected', 'true');
76826884
76836885 // TextGen
76846886 loadTextGenSettings(data, settings);
76856887
7686-
76876888 // OpenAI
76886889 loadOpenAISettings(data, settings.oai_settings ?? settings);
76896890
@@ -7726,13 +6927,9 @@ export async function getSettings() {
77266927
77276928 main_api = settings.main_api;
77286929 $('#main_api').val(main_api);
77296930 $('`#main_api option[value=' + ${main_api + '}]'`).attr('selected', 'true');
7730- 'selected',
7731- 'true',
7732- );
77336931 changeMainAPI();
77346932
7735-
77366933 //Load User's Name and Avatar
77376934 initUserAvatar(settings.user_avatar);
77386935 setPersonaDescription();
@@ -7741,10 +6938,6 @@ export async function getSettings() {
77416938 active_character = settings.active_character;
77426939 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-
77486941 setWorldInfoSettings(settings.world_info_settings ?? settings, data);
77496942
77506943 selected_button = settings.selected_button;
@@ -7769,12 +6962,6 @@ export async function getSettings() {
77696962 await eventSource.emit(event_types.SETTINGS_LOADED);
77706963}
77716964
7772-function selectKoboldGuiPreset() {
7773- $('#settings_preset option[value=gui]')
7774- .attr('selected', 'true')
7775- .trigger('change');
7776-}
7777-
77786965//MARK: saveSettings()
77796966export async function saveSettings(loopCounter = 0) {
77806967 if (!settingsReady) {
@@ -7794,19 +6981,13 @@ export async function saveSettings(loopCounter = 0) {
77946981 TempResponseLength.restore(null);
77956982 }
77966983
7797- //console.log('Entering settings with name1 = '+name1);
6984+ const payload = {
7798- return jQuery.ajax({
7799- type: 'POST',
7800- url: '/api/settings/save',
7801- data: JSON.stringify({
78026985 firstRun: firstRun,
78036986 accountStorage: accountStorage.getState(),
78046987 currentVersion: currentVersion,
78056988 username: name1,
78066989 active_character: active_character,
78076990 active_group: active_group,
7808- api_server: api_server,
7809- preset_settings: preset_settings,
78106991 user_avatar: user_avatar,
78116992 amount_gen: amount_gen,
78126993 max_context: max_context,
@@ -7825,21 +7006,26 @@ export async function saveSettings(loopCounter = 0) {
78257006 background: background_settings,
78267007 proxies: proxies,
78277008 selected_proxy: selected_proxy,
7828- }, null, 4),
7009+ };
7829- beforeSend: function () { },
7010+
7830- cache: false,
7011+ try {
7831- dataType: 'json',
7012+ const result = await fetch('/api/settings/save', {
78327013 contentType method: 'application/jsonPOST',
7833- //processData: false,
7014+ headers: getRequestHeaders(),
7834- success: async function (data) {
7015+ body: JSON.stringify(payload),
7835- eventSource.emit(event_types.SETTINGS_UPDATED);
7016+ cache: 'no-cache',
7836- },
7837- error: function (jqXHR, exception) {
7838- toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Settings could not be saved`);
7839- console.log(exception);
7840- console.log(jqXHR);
7841- },
78427017 });
7018+
7019+ if (!result.ok) {
7020+ throw new Error(`Failed to save settings: ${result.statusText}`);
7021+ }
7022+
7023+ settings = payload;
7024+ await eventSource.emit(event_types.SETTINGS_UPDATED);
7025+ } catch (error) {
7026+ console.error('Error saving settings:', error);
7027+ toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Settings could not be saved`);
7028+ }
78437029}
78447030
78457031/**
@@ -8094,7 +7280,7 @@ export async function getPastCharacterChats(characterId = null) {
80947280/**
80957281 * Helper for `displayPastChats`, to make the same info consistently available for other functions
80967282 */
80977283export function getCurrentChatDetails() {
80987284 if (!characters[this_chid] && !selected_group) {
80997285 return { sessionName: '', group: null, characterName: '', avatarImgURL: '' };
81007286 }
@@ -8649,7 +7835,6 @@ export function showSwipeButtons() {
86497835 !swipes ||
86507836 Number($('.mes:last').attr('mesid')) < 0 ||
86517837 chat[chat.length - 1].is_user ||
8652- chat[chat.length - 1].extra?.image ||
86537838 (selected_group && is_group_generating)
86547839 ) { return; }
86557840
@@ -8665,6 +7850,13 @@ export function showSwipeButtons() {
86657850 chat[chat.length - 1]['swipe_id'] = 0; // set it to id 0
86667851 chat[chat.length - 1]['swipes'] = []; // empty the array
86677852 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+ };
86687860 }
86697861
86707862 const currentMessage = $('#chat').children().filter(`[mesid="${chat.length - 1}"]`);
@@ -8791,12 +7983,10 @@ export async function saveChatConditional() {
87917983 * @param {EventTarget} eventTarget Event target to trigger the event on.
87927984 */
87937985async function importCharacterChat(formData, eventTarget) {
8794- const headers = getRequestHeaders();
8795- delete headers['Content-Type'];
87967986 const fetchResult = await fetch('/api/chats/import', {
87977987 method: 'POST',
87987988 body: formData,
8799- headers: headers,
7989+ headers: getRequestHeaders({ omitContentType: true }),
88007990 cache: 'no-cache',
88017991 });
88027992
@@ -8884,15 +8074,6 @@ export function setGenerationProgress(progress) {
88848074 }
88858075}
88868076
8887-function 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-
88968077export function cancelTtsPlay() {
88978078 if ('speechSynthesis' in window) {
88988079 speechSynthesis.cancel();
@@ -8904,19 +8085,25 @@ function updateAlternateGreetingsHintVisibility(root) {
89048085 $(root).find('.alternate_grettings_hint').toggle(numberOfGreetings == 0);
89058086}
89068087
89078088async function openCharacterWorldPopup() {
89088089 const chid = $('#set_character_world').data('chid');
8909-
89108090 if (menu_type != 'create' && chid === undefined) {
89118091 toastr.error('Does not have an Id for this character in world select menu.');
89128092 return;
89138093 }
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);
89178097 const worldIndexcharName = value(menu_type !== 'create' ? Number(value)create_save.name : NaNcharacters[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] : '';
89208107 const previousValue = $('#character_world').val();
89218108 $('#character_world').val(name);
89228109
@@ -8944,26 +8131,26 @@ function openCharacterWorldPopup() {
89448131 await createOrEditCharacter();
89458132 }
89468133
89478134 setWorldInfoButtonClass(undefined, !!valuename);
89488135 }
89498136
89508137 function onExtraWorldInfoChangedhandleExtrasWorldSelect() {
89518138 const selectorFieldValueselectedValues = $('.character_extra_world_info_selector'this).val();
89528139 const selectedWorlds = Array.isArray(selectorFieldValueselectedValues) ? selectorFieldValueselectedValues : [];
89538140 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);
89608149 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 });
89648153 };
8965-
8966- charLore.push(newCharLoreEntry);
89678154 } else if (tempExtraBooks.length === 0) {
89688155 charLore.splice(existingCharIndex, 1);
89698156 } else {
@@ -8974,62 +8161,43 @@ function openCharacterWorldPopup() {
89748161 saveSettingsDebounced();
89758162 }
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.
89798166 const extraSelectprimarySelect = template.find('.character_extra_world_info_selectorcharacter_world_info_selector');
8980- const name = (menu_type == 'create' ? create_save.name : characters[chid]?.data?.name) || 'Nameless';
8167+ world_names.forEach((item, i) => {
8981- const worldId = (menu_type == 'create' ? create_save.world : characters[chid]?.data?.extensions?.world) || '';
8168+ primarySelect.append(new Option(item, String(i), item === worldId, item === worldId));
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,
89918169 });
8992- }
89938170
89948171 // AppedAppend to baseextras dropdown.
8172+ const extrasSelect = template.find('.character_extra_world_info_selector');
8173+ const existingCharLore = world_info.charLore?.find((e) => e.name === fileName);
89958174 world_names.forEach((item, i) => {
8996- const option = document.createElement('option');
8175+ const array = (menu_type == 'create' ? create_save.extra_books : existingCharLore?.extraBooks);
8997- option.value = String(i);
8176+ const isSelected = !!array?.includes(item);
8998- option.innerText = item;
8177+ extrasSelect.append(new Option(item, String(i), isSelected, isSelected));
8999- option.selected = item === worldId;
9000- select.append(option);
90018178 });
90028179
9003- // Append to extras dropdown
8180+ const popup = new Popup(template, POPUP_TYPE.TEXT, '', {
9004- if (world_names.length > 0) {
8181+ onOpen: function (popup) {
9005- extraSelect.empty();
8182+ const popupDialog = $(popup.dlg);
9006- }
9007- world_names.forEach((item, i) => {
9008- const option = document.createElement('option');
9009- option.value = String(i);
9010- option.innerText = item;
90118183
9012- const existingCharLore = world_info.charLore?.find((e) => e.name === getCharaFilename());
8184+ primarySelect.on('change', handlePrimaryWorldSelect);
9013- if (existingCharLore) {
8185+ extrasSelect.on('change', handleExtrasWorldSelect);
9014- option.selected = existingCharLore.extraBooks.includes(item);
9015- } else {
9016- option.selected = false;
9017- }
9018- extraSelect.append(option);
9019- });
90208186
9021- select.on('change', onSelectCharacterWorld);
8187+ // Not needed on mobile.
9022- extraSelect.on('mousedown change', async function (e) {
8188+ if (!isMobile()) {
9023- // If there's no world names, don't do anything
8189+ extrasSelect.select2({
9024- if (world_names.length === 0) {
8190+ width: '100%',
9025- e.preventDefault();
8191+ placeholder: t`No auxiliary Lorebooks set. Click here to select.`,
9026- return;
8192+ allowClear: true,
8193+ closeOnSelect: false,
8194+ dropdownParent: popupDialog,
8195+ });
90278196 }
9028-
8197+ },
9029- onExtraWorldInfoChanged();
90308198 });
90318199
9032- callPopup(template, 'text');
8200+ await popup.show();
90338201}
90348202
90358203function openAlternateGreetings() {
@@ -9122,8 +8290,7 @@ async function createOrEditCharacter(e) {
91228290 formData.set('avatar', convertedFile);
91238291 }
91248292
91258293 const headers = getRequestHeaders({ omitContentType: true });
9126- delete headers['Content-Type'];
91278294
91288295 if ($('#form_create').attr('actiontype') == 'createcharacter') {
91298296 if (String($('#character_name_pole').val()).length === 0) {
@@ -9192,6 +8359,15 @@ async function createOrEditCharacter(e) {
91928359 field.callback && field.callback(fieldValue);
91938360 });
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+
91958371 $('#character_popup-button-h3').text('Create character');
91968372
91978373 create_save.avatar = null;
@@ -9339,7 +8515,7 @@ export function swipe_left(_event, { source, repeated } = {}) {
93398515 const this_mes_block_height = this_mes_block[0].scrollHeight;
93408516 chat[chat.length - 1]['mes'] = chat[chat.length - 1]['swipes'][chat[chat.length - 1]['swipe_id']];
93418517 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
93428518 chat[chat.length - 1]['extra'] = JSON.parse(JSON.stringifystructuredClone(chat[chat.length - 1].swipe_info[chat[chat.length - 1]['swipe_id']]?.extra || chat[chat.length - 1].extra));
93438519
93448520 if (chat[chat.length - 1].extra) {
93458521 // if message has memory attached - remove it to allow regen
@@ -9401,6 +8577,7 @@ export function swipe_left(_event, { source, repeated } = {}) {
94018577 easing: animation_easing,
94028578 queue: false,
94038579 complete: async function () {
8580+ appendMediaToMessage(chat[chat.length - 1], $(this).parent().children('.mes_block'));
94048581 await eventSource.emit(event_types.MESSAGE_SWIPED, (chat.length - 1));
94058582 saveChatDebounced();
94068583 },
@@ -9476,7 +8653,7 @@ export function swipe_right(_event = null, { source, repeated } = {}) {
94768653 'send_date': chat[chat.length - 1]['send_date'],
94778654 'gen_started': chat[chat.length - 1]['gen_started'],
94788655 'gen_finished': chat[chat.length - 1]['gen_finished'],
94798656 'extra': JSON.parse(JSON.stringifystructuredClone(chat[chat.length - 1]['extra'])),
94808657 };
94818658 //assign swipe info array with last message from chat
94828659 }
@@ -9500,6 +8677,11 @@ export function swipe_right(_event = null, { source, repeated } = {}) {
95008677 if (chat[chat.length - 1].extra.display_text) {
95018678 delete chat[chat.length - 1].extra.display_text;
95028679 }
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;
95038685 }
95048686 if (!Array.isArray(chat[chat.length - 1]['swipe_info'])) {
95058687 chat[chat.length - 1]['swipe_info'] = [];
@@ -9512,7 +8694,7 @@ export function swipe_right(_event = null, { source, repeated } = {}) {
95128694 } 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
95138695 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
95148696 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
95158697 chat[chat.length - 1]['extra'] = JSON.parse(JSON.stringifystructuredClone(chat[chat.length - 1].swipe_info[chat[chat.length - 1]['swipe_id']]?.extra || chat[chat.length - 1].extra || []));
95168698 run_swipe_right = true; //then prepare to do normal right swipe to show next message
95178699 }
95188700
@@ -9590,266 +8772,54 @@ export function swipe_right(_event = null, { source, repeated } = {}) {
95908772 queue: false,
95918773 complete: function () {
95928774 this_mes_div.children('.mes_block').transition({
95938775 x: '0px',
95948776 duration: animation_duration > 0 ? swipe_duration : 0,
95958777 easing: animation_easing,
95968778 queue: false,
95978779 complete: async function () {
95988780 await eventSource.emit(event_types.MESSAGE_SWIPED, appendMediaToMessage(chat[chat.length - 1)], swipeMessage);
9599- if (run_generate && !is_send_press && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) {
8781+ await eventSource.emit(event_types.MESSAGE_SWIPED, (chat.length - 1));
9600- console.debug('caught here 2');
8782+ if (run_generate && !is_send_press && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) {
9601- is_send_press = true;
8783+ console.debug('caught here 2');
9602- await Generate('swipe');
8784+ is_send_press = true;
9603- } else {
8785+ await Generate('swipe');
9604- if (parseInt(chat[chat.length - 1]['swipe_id']) !== chat[chat.length - 1]['swipes'].length) {
8786+ } else {
9605- saveChatDebounced();
8787+ if (parseInt(chat[chat.length - 1]['swipe_id']) !== chat[chat.length - 1]['swipes'].length) {
9606- }
8788+ saveChatDebounced();
9607- }
9608- },
9609- });
9610- },
9611- });
9612- },
9613- });
9614- this_mes_div.children('.avatar').transition({ // moves avatar along with swipe
9615- x: '-' + swipe_range,
9616- duration: animation_duration > 0 ? swipe_duration : 0,
9617- easing: animation_easing,
9618- queue: false,
9619- complete: function () {
9620- this_mes_div.children('.avatar').transition({
9621- x: swipe_range,
9622- duration: 0,
9623- easing: animation_easing,
9624- queue: false,
9625- complete: function () {
9626- this_mes_div.children('.avatar').transition({
9627- x: '0px',
9628- duration: animation_duration > 0 ? swipe_duration : 0,
9629- easing: animation_easing,
9630- queue: false,
9631- complete: function () {
9632-
9633- },
9634- });
9635- },
9636- });
9637- },
9638- });
9639- }
9640-}
9641-
9642-/**
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- */
9653-export 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
9708-export 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
9711-for (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-}
9719-for (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-
9728-async 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-
9748-async 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-
9768-async function enableInstructCallback() {
9769- $('#instruct_enabled').prop('checked', true).trigger('input').trigger('change');
9770- return '';
9771-}
9772-
9773-async function disableInstructCallback() {
9774- $('#instruct_enabled').prop('checked', false).trigger('input').trigger('change');
9775- return '';
9776-}
9777-
9778-/**
9779- * @param {string} text API name
9780- */
9781-async 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;
98338789 }
9834-
9835- if (connectionRequired && apiConfig.button) {
9836- $(apiConfig.button).trigger('click');
98378790 }
8791+ },
8792+ });
8793+ },
8794+ });
8795+ },
8796+ });
8797+ this_mes_div.children('.avatar').transition({ // moves avatar along with swipe
8798+ x: '-' + swipe_range,
8799+ duration: animation_duration > 0 ? swipe_duration : 0,
8800+ easing: animation_easing,
8801+ queue: false,
8802+ complete: function () {
8803+ this_mes_div.children('.avatar').transition({
8804+ x: swipe_range,
8805+ duration: 0,
8806+ easing: animation_easing,
8807+ queue: false,
8808+ complete: function () {
8809+ this_mes_div.children('.avatar').transition({
8810+ x: '0px',
8811+ duration: animation_duration > 0 ? swipe_duration : 0,
8812+ easing: animation_easing,
8813+ queue: false,
8814+ complete: function () {
98388815
9839- const quiet = isTrueBoolean(args?.quiet);
8816+ },
9840- const toast = quiet ? jQuery() : toastr.info(t`API set to ${text}, trying to connect..`);
8817+ });
9841-
8818+ },
9842- try {
8819+ });
9843- if (connectionRequired) {
8820+ },
9844- await waitUntilCondition(() => online_status !== 'no_connection', 5000, 100);
8821+ });
9845- }
9846- console.log('Connection successful');
9847- } catch {
9848- console.log('Could not connect after 5 seconds, skipping.');
98498822 }
9850-
9851- toastr.clear(toast);
9852- return text;
98538823}
98548824
98558825/**
@@ -9870,6 +8840,7 @@ export async function processDroppedFiles(files, data = new Map()) {
98708840
98718841 const allowedExtensions = [
98728842 'charx',
8843+ 'byaf',
98738844 ];
98748845
98758846 const avatarFileNames = [];
@@ -9933,7 +8904,7 @@ async function importCharacter(file, { preserveFileName = '', importTags = false
99338904 }
99348905
99358906 const ext = file.name.match(/\.(\w+)$/);
99368907 if (!ext || !(['json', 'png', 'yaml', 'yml', 'charx', 'byaf'].includes(ext[1].toLowerCase()))) {
99378908 return;
99388909 }
99398910
@@ -9944,19 +8915,22 @@ async function importCharacter(file, { preserveFileName = '', importTags = false
99448915 formData.append('file_type', format);
99458916 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',
99508921 data body: formData,
9951- async: true,
8922+ headers: getRequestHeaders({ omitContentType: true }),
99528923 cache: false'no-cache',
9953- contentType: false,
9954- processData: false,
99558924 });
99568925
8926+ if (!result.ok) {
8927+ throw new Error(`Failed to import character: ${result.statusText}`);
8928+ }
8929+
8930+ const data = await result.json();
8931+
99578932 if (data.error) {
9958- toastr.error(t`The file is likely invalid or corrupted.`, t`Could not import character`);
8933+ throw new Error(`Server returned an error: ${data.error}`);
9959- return;
99608934 }
99618935
99628936 if (data.file_name !== undefined) {
@@ -9966,11 +8940,14 @@ async function importCharacter(file, { preserveFileName = '', importTags = false
99668940 let avatarFileName = `${data.file_name}.png`;
99678941 if (importTags) {
99688942 await importCharactersTags([avatarFileName]);
9969-
99708943 selectImportedChar(data.file_name);
99718944 }
99728945 return avatarFileName;
99738946 }
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`);
8950+ }
99748951}
99758952
99768953async function importFromURL(items, files) {
@@ -9995,32 +8972,6 @@ async function importFromURL(items, files) {
99958972 }
99968973}
99978974
9998-async 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-
100248975export async function doNewChat({ deleteCurrentChat = false } = {}) {
100258976 //Make a new chat for selected character
100268977 if ((!selected_group && this_chid == undefined) || menu_type == 'create') {
@@ -10055,33 +9006,6 @@ export async function doNewChat({ deleteCurrentChat = false } = {}) {
100559006
100569007}
100579008
10058-async 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-
10067-async 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-
100859009/**
100869010 * Renames a group or character chat.
100879011 * @param {object} param Parameters for renaming chat
@@ -10195,12 +9119,6 @@ export async function updateRemoteChatName(characterId, newName) {
101959119 }
101969120}
101979121
10198-/**
10199- * /getchatname` slash command
10200- */
10201-async function doGetChatName() {
10202- return getCurrentChatDetails().sessionName;
10203-}
102049122
102059123function doCharListDisplaySwitch() {
102069124 power_user.charListGrid = !power_user.charListGrid;
@@ -10208,11 +9126,6 @@ function doCharListDisplaySwitch() {
102089126 saveSettingsDebounced();
102099127}
102109128
10211-function doCloseChat() {
10212- $('#option_close_chat').trigger('click');
10213- return '';
10214-}
10215-
102169129/**
102179130 * Function to handle the deletion of a character, given a specific popup type and character ID.
102189131 * 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 } = {}) {
103239236 sendSystemMessage(system_message_types.ASSISTANT_NOTE);
103249237}
103259238
10326-function doTogglePanels() {
10327- $('#option_settings').trigger('click');
10328- return '';
10329-}
10330-
103319239/**
103329240 * Event handler to open a navbar drawer when a drawer open button is clicked.
103339241 * Handles click events on .drawer-opener elements.
@@ -10363,7 +9271,7 @@ export async function doNavbarIconClick() {
103639271 for (const el of $openDrawers) {
103649272 $(el).toggleClass('closedDrawer openDrawer');
103659273 }
103669274 if ($openDrawers.length && animation_duration) {
103679275 await delay(animation_duration);
103689276 }
103699277 icon.toggleClass('openIcon closedIcon');
@@ -10421,18 +9329,9 @@ function addDebugFunctions() {
104219329 registerDebugFunction('generationTest', 'Send a generation request', 'Generates text using the currently selected API.', async () => {
104229330 const text = prompt('Input text:', 'Hello');
104239331 toastr.info('Working on it...');
104249332 const message = await generateRaw(text,{ null,prompt: false,text false});
104259333 alert(message);
104269334 });
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-
104369335 registerDebugFunction('toggleEventTracing', 'Toggle event tracing', 'Useful to see what triggered a certain event.', () => {
104379336 localStorage.setItem('eventTracing', localStorage.getItem('eventTracing') === 'true' ? 'false' : 'true');
104389337 toastr.info('Event tracing is now ' + (localStorage.getItem('eventTracing') === 'true' ? 'enabled' : 'disabled'));
@@ -10471,252 +9370,41 @@ API Settings: ${JSON.stringify(getSettingsContents[getSettingsContents.main_api
104719370 });
104729371}
104739372
9373+function initCharacterSearch() {
9374+ const debouncedCharacterSearch = debounce((searchQuery) => {
9375+ entitiesFilter.setFilterData(FILTER_TYPES.SEARCH, searchQuery);
9376+ });
104749377
10475-// MARK: DOM Handlers Start
9378+ const searchForm = $('#form_character_search_form');
10476-jQuery(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',
9383+
10486- callback: duplicateCharacter,
9384+ searchInput.on('input', function () {
10487- helpString: 'Duplicates the currently selected character.',
9385+ const searchQuery = String($(this).val());
10488- }));
9386+ debouncedCharacterSearch(searchQuery);
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);
106009387 });
10601- },
9388+
10602- helpString: 'Opens a temporary chat with Assistant.',
9389+ searchButton.on('click', function () {
10603- }));
9390+ const newVisibility = !searchForm.is(':visible');
10604- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9391+ searchForm.toggle(newVisibility);
10605- name: 'panels',
9392+ searchButton.toggleClass('active', newVisibility);
10606- callback: doTogglePanels,
9393+ accountStorage.setItem(storageKey, String(newVisibility));
10607- aliases: ['togglepanels'],
9394+ if (newVisibility) {
10608- helpString: 'Toggle UI panels on/off',
9395+ searchInput.trigger('focus');
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);
106819396 }
9397+ });
106829398
10683- const newState = isTrueBoolean(state);
9399+ eventSource.on(event_types.APP_READY, () => {
10684- newState ? enableInstructCallback() : disableInstructCallback();
9400+ const isVisible = accountStorage.getItem(storageKey) === 'true';
10685- return String(power_user.instruct.enabled);
9401+ searchForm.toggle(isVisible);
10686- },
9402+ searchButton.toggleClass('active', isVisible);
106879403 }));
10688- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
9404+}
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- }));
107199405
9406+// MARK: DOM Handlers Start
9407+jQuery(async function () {
107209408 setTimeout(function () {
107219409 $('#groupControlsToggle').trigger('click');
107229410 $('#groupCurrentMemberListToggle .inline-drawer-icon').trigger('click');
@@ -10731,10 +9419,10 @@ jQuery(async function () {
107319419 });
107329420 $('#send_but, #option_regenerate, #option_continue, #mes_continue, #mes_impersonate').on('click', () => {
107339421 if (S_TAPreviouslyFocused) {
107349422 $('#send_textarea').focustrigger('focus');
107359423 }
107369424 });
107379425 $(document).clickon('click', event => {
107389426 if ($(':focus').attr('id') !== 'send_textarea') {
107399427 var validIDs = ['options_button', 'send_but', 'mes_impersonate', 'mes_continue', 'send_textarea', 'option_regenerate', 'option_continue'];
107409428 if (!validIDs.includes($(event.target).attr('id'))) {
@@ -10747,7 +9435,7 @@ jQuery(async function () {
107479435
107489436 /////////////////
107499437
107509438 $('#swipes-checkbox').changeon('change', function () {
107519439 swipes = !!$('#swipes-checkbox').prop('checked');
107529440 if (swipes) {
107539441 //console.log('toggle change calling showswipebtns');
@@ -10764,13 +9452,7 @@ jQuery(async function () {
107649452 $(document).on('click', '.last_mes .swipe_right', swipe_right);
107659453 $(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
107759457 $('#mes_impersonate').on('click', function () {
107769458 $('#option_impersonate').trigger('click');
@@ -10890,11 +9572,51 @@ jQuery(async function () {
108909572 }
108919573 });
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 } = {}) {
108949606 e.stopPropagation();
108959607 chat_file_for_del = $(this).attr('file_name');
108969608 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+ }
108989620 });
108999621
109009622 $('#advanced_div').on('click', function () {
@@ -10943,25 +9665,7 @@ jQuery(async function () {
109439665 }, animation_duration);
109449666
109459667 if (popup_type == 'del_chat') {
10946- //close past chat popup
9668+ 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- }
109659669 }
109669670
109679671 if (dialogueResolve) {
@@ -11133,76 +9837,6 @@ jQuery(async function () {
111339837 }
111349838 });
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
112079841 const button = $('#options_button');
112089842 const menu = $('#options');
@@ -11285,7 +9919,8 @@ jQuery(async function () {
112859919 await doNewChat({ deleteCurrentChat: deleteCurrentChat });
112869920 }
112879921 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 });
112899924 }
112909925 }
112919926
@@ -11424,48 +10059,11 @@ jQuery(async function () {
1142410059 is_delete_mode = false;
1142510060 });
1142610061
1142710062 $('#settings_presetmain_api').changeon('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 () {
1146610063 cancelStatusCheck('Canceled because main api changed');
1146710064 changeMainAPI();
1146810065 saveSettingsDebounced();
10066+ await eventSource.emit(event_types.MAIN_API_CHANGED, { apiId: main_api });
1146910067 });
1147010068
1147110069 ////////////////// OPTIMIZED RANGE SLIDER LISTENERS////////////////
@@ -11542,17 +10140,12 @@ jQuery(async function () {
1154210140 }
1154310141 });
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-
1155310143 //********************
1155410144 //***Message Editor***
1155510145 $(document).on('click', '.mes_edit', async function () {
10146+ if (is_delete_mode) {
10147+ return;
10148+ }
1155610149 if (this_chid !== undefined || selected_group || name2 === neutralCharacterName) {
1155710150 // Previously system messages we're allowed to be edited
1155810151 /*const message = $(this).closest(".mes");
@@ -11896,6 +10489,9 @@ jQuery(async function () {
1189610489 await importCharactersTags(avatarFileNames);
1189710490 selectImportedChar(avatarFileNames[avatarFileNames.length - 1]);
1189810491 }
10492+
10493+ // Clear the file input value to allow re-uploading the same file
10494+ e.target.value = '';
1189910495 });
1190010496
1190110497 $('#export_button').on('click', function () {
@@ -11990,7 +10586,7 @@ jQuery(async function () {
1199010586 select_rm_characters();
1199110587 });
1199210588
1199310589 $('#dupe_button').clickon('click', async function () {
1199410590 await duplicateCharacter();
1199510591 });
1199610592
@@ -12126,9 +10722,17 @@ jQuery(async function () {
1212610722 $('body').append(newElement);
1212710723 newElement.fadeIn(animation_duration);
1212810724 const zoomedAvatarImgElement = $(`.zoomed_avatar[forChar="${charname}"] img`);
1212910725 if (messageElement.attr('is_user') == 'true' || (messageElement.attr('is_system') == 'true' && !isValidCharacter)) { //handle user and system avatars
10726+ //handle user and system avatars
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 {
1213010733 zoomedAvatarImgElement.attr('src', thumbURL);
1213110734 zoomedAvatarImgElement.attr('data-izoomify-url', thumbURL);
10735+ }
1213210736 } else if (messageElement.attr('is_user') == 'false') { //handle char avatars
1213310737 zoomedAvatarImgElement.attr('src', avatarSrc);
1213410738 zoomedAvatarImgElement.attr('data-izoomify-url', avatarSrc);
@@ -12182,18 +10786,18 @@ jQuery(async function () {
1218210786 }
1218310787 });
1218410788
1218510789 $(document).keyupon('keyup', function (e) {
1218610790 if (e.key === 'Escape') {
1218710791 const isEditVisible = $('#curEditTextarea').is(':visible') || $('.reasoning_edit_textarea').length > 0;
1218810792 if (isEditVisible && power_user.auto_save_msg_edits === false) {
1218910793 closeMessageEditor('all');
1219010794 $('#send_textarea').focustrigger('focus');
1219110795 return;
1219210796 }
1219310797 if (isEditVisible && power_user.auto_save_msg_edits === true) {
1219410798 $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).trigger('click');
1219510799 closeMessageEditor('reasoning');
1219610800 $('#send_textarea').focustrigger('focus');
1219710801 return;
1219810802 }
1219910803 if (!this_edit_mes_id && $('#mes_stop').is(':visible')) {
@@ -12210,13 +10814,13 @@ jQuery(async function () {
1221010814 const target = $(targetElement.selectedOptions).attr('id');
1221110815 switch (target) {
1221210816 case 'set_character_world':
1221310817 await openCharacterWorldPopup();
1221410818 break;
1221510819 case 'set_chat_scenario':
1221610820 await setScenarioOverride();
1221710821 break;
1221810822 case 'renameCharButton':
1221910823 await renameCharacter();
1222010824 break;
1222110825 case 'import_character_info':
1222210826 await importEmbeddedWorldInfo();
@@ -12294,6 +10898,9 @@ jQuery(async function () {
1229410898 });
1229510899
1229610900 $(document).on('change', '.range-block-counter input, .neo-range-input', function (e) {
10901+ if (!(e.target instanceof HTMLElement)) {
10902+ return;
10903+ }
1229710904 e.target.focus();
1229810905 e.target.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true }));
1229910906 });
@@ -12314,7 +10921,6 @@ jQuery(async function () {
1231410921 } else {
1231510922 //if value not ok, warn and reset to last known valid value
1231610923 toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);
12317- console.log(valueBeforeManualInput);
1231810924 //newSlider.val(valueBeforeManualInput)
1231910925 $(this).val(valueBeforeManualInput);
1232010926 }
@@ -12324,7 +10930,6 @@ jQuery(async function () {
1232410930
1232510931 $(document).on('keyup', '.range-block-counter input, .neo-range-input', function () {
1232610932 valueBeforeManualInput = $(this).val();
12327- console.log(valueBeforeManualInput);
1232810933 isManualInput = true;
1232910934 });
1233010935
@@ -12342,7 +10947,6 @@ jQuery(async function () {
1234210947 } else {
1234310948 //if value not ok, warn and reset to last known valid value
1234410949 toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`);
12345- console.log(valueBeforeManualInput);
1234610950 $(this).val(valueBeforeManualInput);
1234710951 }
1234810952 }
@@ -12437,13 +11041,6 @@ jQuery(async function () {
1243711041 // Added here to prevent execution before script.js is loaded and get rid of quirky timeouts
1243811042 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-
1244711044 window.addEventListener('beforeunload', (e) => {
1244811045 if (isChatSaving) {
1244911046 e.preventDefault();
public/scripts/PromptManager.js+258 -95
@@ -6,7 +6,7 @@ import { event_types, eventSource, is_send_press, main_api, substituteParams } f
66import { is_group_generating } from './group-chats.js';
77import { Message, MessageCollection, TokenHandler } from './openai.js';
88import { power_user } from './power-user.js';
99import { debounce, waitUntilCondition, escapeHtml, uuidv4 } from './utils.js';
1010import { debounce_timeout } from './constants.js';
1111import { renderTemplateAsync } from './templates.js';
1212import { Popup } from './popup.js';
@@ -29,6 +29,7 @@ function debouncePromise(func, delay) {
2929}
3030
3131const DEFAULT_DEPTH = 4;
32+const DEFAULT_ORDER = 100;
3233
3334/**
3435 * @enum {number}
@@ -77,25 +78,108 @@ const registerPromptManagerMigration = () => {
7778 * Represents a prompt.
7879 */
7980class 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
82165 /**
83166 * Create a new Prompt instance.
84167 *
85168 * @param {Object} [param0] - Object containing the properties of the prompt.
86169 * @param {string} [param0.identifier] - The unique identifier of the prompt.
87170 * @param {string} [param0.role] - The role associated with the prompt.
88171 * @param {string} [param0.content] - The content of the prompt.
89172 * @param {string} [param0.name] - The name of the prompt.
90173 * @param {boolean} [param0.system_prompt] - Indicates if the prompt is a system prompt.
91174 * @param {string|number} [param0.position] - The position of the prompt in the prompt list.
92175 * @param {number} [param0.injection_position] - The insert position of the prompt.
93176 * @param {number} [param0.injection_depth] - The depth of the prompt in the chat.
94177 * @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.
95179 * @param {boolean} [param0.forbid_overrides] - Indicates if the prompt should not be overridden.
96180 * @param {boolean} [param0.extension] - Prompt is added by an extension.
97181 */
98182 constructor({ identifier, role, content, name, system_prompt, position, injection_depth, injection_position, forbid_overrides, extension, injection_order, injection_trigger } = {}) {
99183 this.identifier = identifier;
100184 this.role = role;
101185 this.content = content;
@@ -106,7 +190,8 @@ class Prompt {
106190 this.injection_position = injection_position;
107191 this.forbid_overrides = forbid_overrides;
108192 this.extension = extension ?? false;
109193 this.injection_order = injection_order ?? 100DEFAULT_ORDER;
194+ this.injection_trigger = injection_trigger ?? [];
110195 }
111196}
112197
@@ -114,7 +199,16 @@ class Prompt {
114199 * Representing a collection of prompts.
115200 */
116201export class PromptCollection {
202+ /**
203+ * List of Prompts in the collection.
204+ * @type {Prompt[]}
205+ */
117206 collection = [];
207+
208+ /**
209+ * List of identifiers of prompts that have been overridden.
210+ * @type {string[]}
211+ */
118212 overriddenPrompts = [];
119213
120214 /**
@@ -129,7 +223,7 @@ export class PromptCollection {
129223 /**
130224 * Checks if the provided instances are of the Prompt class.
131225 *
132226 * @param {...anyPrompt} prompts - Instances to check.
133227 * @throws Will throw an error if one or more instances are not of the Prompt class.
134228 */
135229 checkPromptInstance(...prompts) {
@@ -191,6 +285,12 @@ export class PromptCollection {
191285 return this.index(identifier) !== -1;
192286 }
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+ */
194294 override(prompt, position) {
195295 this.set(prompt, position);
196296 this.overriddenPrompts.push(prompt.identifier);
@@ -274,7 +374,7 @@ class PromptManager {
274374 this.tryGenerate = async () => { };
275375
276376 /** Called to persist the configuration, must return a promise */
277377 this.saveServiceSettings = () => { return Promise.resolve(); };
278378
279379 /** Toggle prompt button click */
280380 this.handleToggle = () => { };
@@ -444,32 +544,51 @@ class PromptManager {
444544 break;
445545 }
446546
447547 const nameField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name').value = prompt.name);
448548 const roleField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role').value = 'system');
449549 const promptField = /** @type {HTMLTextAreaElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt').value = prompt.content ?? '');
450550 const injectionPositionField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position').value = prompt.injection_position ?? 0);
451551 const injectionDepthField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth').value = prompt.injection_depth ?? DEFAULT_DEPTH);
452552 const injectionOrderField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_order').value = prompt.injection_order ?? 100);
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
460578 if (isPulledPrompt) {
461579 const sourceName = this.promptSources[promptId];
462- document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_source').textContent = sourceName;
580+ entrySource.textContent = sourceName;
463581 }
464582
465583 if (!this.systemPrompts.includes(promptId)) {
466584 document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position')injectionPositionField.removeAttribute('disabled');
467585 }
468586 };
469587
470588 // Append prompt to selected character
471589 this.handleAppendPrompt = (event) => {
472590 const promptIDappendPromptFooter = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_footer_append_prompt').value);
591+ const promptID = appendPromptFooter.value;
473592 const prompt = this.getPromptById(promptID);
474593
475594 if (prompt) {
@@ -483,7 +602,8 @@ class PromptManager {
483602 this.handleDeletePrompt = async (event) => {
484603 Popup.show.confirm(t`Are you sure you want to delete this prompt?`, null).then((userChoice) => {
485604 if (!userChoice) return;
486605 const promptIDappendPromptFooter = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_footer_append_prompt').value);
606+ const promptID = appendPromptFooter.value;
487607 const prompt = this.getPromptById(promptID);
488608
489609 if (prompt && true === this.isPromptDeletionAllowed(prompt)) {
@@ -566,6 +686,7 @@ class PromptManager {
566686 fileOpener.accept = '.json';
567687
568688 fileOpener.addEventListener('change', (event) => {
689+ if (!(event.target instanceof HTMLInputElement)) return;
569690 const file = event.target.files[0];
570691 if (!file) return;
571692
@@ -575,7 +696,7 @@ class PromptManager {
575696 const fileContent = event.target.result;
576697
577698 try {
578699 const data = JSON.parse(fileContent.toString());
579700 this.import(data);
580701 } catch (err) {
581702 toastr.error(t`An error occurred while importing prompts. More info available in console.`);
@@ -615,7 +736,7 @@ class PromptManager {
615736
616737 // Update edit form if present
617738 // @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
618739 const popupEditFormPrompt = /** @type {HTMLTextAreaElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt'));
619740 if (popupEditFormPrompt.offsetParent) {
620741 popupEditFormPrompt.value = prompt.content;
621742 }
@@ -673,6 +794,7 @@ class PromptManager {
673794
674795 // Trigger re-render when token settings are changed
675796 document.getElementById('openai_max_context').addEventListener('change', (event) => {
797+ if (!(event.target instanceof HTMLInputElement)) return;
676798 this.serviceSettings.openai_max_context = event.target.value;
677799 if (this.activeCharacter) this.renderDebounced();
678800 });
@@ -778,23 +900,33 @@ class PromptManager {
778900
779901 /**
780902 * Update a prompt with the values from the HTML form.
781903 * @param {objectPartial<Prompt>} prompt - The prompt to be updated.
782904 * @returns {void}
783905 */
784906 updatePromptWithPromptEditForm(prompt) {
785907 prompt.nameconst nameField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name').value);
786908 prompt.roleconst roleField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role').value);
787909 prompt.contentconst promptField = /** @type {HTMLTextAreaElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt').value);
788910 prompt.injection_positionconst injectionPositionField = Number/** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position').value);
789911 prompt.injection_depthconst injectionDepthField = Number/** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth').value);
790912 prompt.injection_orderconst injectionOrderField = Number/** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_order').value);
791913 prompt.forbid_overridesconst injectionTriggerField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_forbid_overridesprompt_manager_popup_entry_form_injection_trigger').checked);
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;
792924 }
793925
794926 /**
795927 * Find a prompt by its identifier and update it with the provided object.
796928 * @param {string} identifier - The identifier of the prompt.
797929 * @param {objectPrompt} updatePrompt - An object with properties to be updated in the prompt.
798930 * @returns {void}
799931 */
800932 updatePromptByIdentifier(identifier, updatePrompt) {
@@ -804,7 +936,7 @@ class PromptManager {
804936
805937 /**
806938 * Iterate over an array of prompts, find each one by its identifier, and update them with the provided data.
807939 * @param {objectPrompt[]} prompts - An array of prompt updates.
808940 * @returns {void}
809941 */
810942 updatePrompts(prompts) {
@@ -826,7 +958,7 @@ class PromptManager {
826958
827959 /**
828960 * Add a prompt to the current character's prompt list.
829961 * @param {objectPrompt} prompt - The prompt to be added.
830962 * @param {object} character - The character whose prompt list will be updated.
831963 * @returns {void}
832964 */
@@ -839,7 +971,7 @@ class PromptManager {
839971
840972 /**
841973 * Remove a prompt from the current character's prompt list.
842974 * @param {objectPrompt} prompt - The prompt to be removed.
843975 * @param {object} character - The character whose prompt list will be updated.
844976 * @returns {void}
845977 */
@@ -853,7 +985,7 @@ class PromptManager {
853985
854986 /**
855987 * Create a new prompt and add it to the list of prompts.
856988 * @param {objectPartial<Prompt>} prompt - The prompt to be added.
857989 * @param {string} identifier - The identifier for the new prompt.
858990 * @returns {void}
859991 */
@@ -931,7 +1063,7 @@ class PromptManager {
9311063
9321064 /**
9331065 * Check whether a prompt can be inspected.
9341066 * @param {objectPrompt} prompt - The prompt to check.
9351067 * @returns {boolean} True if the prompt is a marker, false otherwise.
9361068 */
9371069 isPromptInspectionAllowed(prompt) {
@@ -940,7 +1072,7 @@ class PromptManager {
9401072
9411073 /**
9421074 * Check whether a prompt can be deleted. System prompts cannot be deleted.
9431075 * @param {objectPrompt} prompt - The prompt to check.
9441076 * @returns {boolean} True if the prompt can be deleted, false otherwise.
9451077 */
9461078 isPromptDeletionAllowed(prompt) {
@@ -949,7 +1081,7 @@ class PromptManager {
9491081
9501082 /**
9511083 * Check whether a prompt can be edited.
9521084 * @param {objectPrompt} prompt - The prompt to check.
9531085 * @returns {boolean} True if the prompt can be edited, false otherwise.
9541086 */
9551087 isPromptEditAllowed(prompt) {
@@ -966,7 +1098,7 @@ class PromptManager {
9661098
9671099 /**
9681100 * Check whether a prompt can be toggled on or off.
9691101 * @param {objectPrompt} prompt - The prompt to check.
9701102 * @returns {boolean} True if the prompt can be deleted, false otherwise.
9711103 */
9721104 isPromptToggleAllowed(prompt) {
@@ -1062,7 +1194,7 @@ class PromptManager {
10621194
10631195 /**
10641196 * Get the prompts for a specific character. Can be filtered to only include enabled prompts.
10651197 * @returns {objectPrompt[]} The prompts for the character.
10661198 * @param character
10671199 * @param onlyEnabled
10681200 */
@@ -1075,7 +1207,7 @@ class PromptManager {
10751207 /**
10761208 * 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.
10771209 * @param {object|null} character - The character to get the prompt list for.
10781210 * @returns {objectPartial<Prompt>[]} The prompt list for the character, or an empty array.
10791211 */
10801212 getPromptOrderForCharacter(character) {
10811213 return !character ? [] : (this.serviceSettings.prompt_order.find(list => String(list.character_id) === String(character.id))?.order ?? []);
@@ -1083,7 +1215,7 @@ class PromptManager {
10831215
10841216 /**
10851217 * Set the prompts for the manager.
10861218 * @param {objectPartial<Prompt>[]} prompts - The prompts to be set.
10871219 * @returns {void}
10881220 */
10891221 setPrompts(prompts) {
@@ -1125,7 +1257,7 @@ class PromptManager {
11251257 /**
11261258 * Finds and returns a prompt by its identifier.
11271259 * @param {string} identifier - Identifier of the prompt
11281260 * @returns {ObjectPrompt|null} The prompt object, or null if not found
11291261 */
11301262 getPromptById(identifier) {
11311263 return this.serviceSettings.prompts.find(item => item && item.identifier === identifier) ?? null;
@@ -1143,9 +1275,9 @@ class PromptManager {
11431275 /**
11441276 * Enriches a generic object, creating a new prompt object in the process
11451277 *
11461278 * @param {ObjectPartial<Prompt>} prompt - Prompt object
11471279 * @param original
11481280 * @returns {ObjectPrompt} An object with "role" and "content" properties
11491281 */
11501282 preparePrompt(prompt, original = null) {
11511283 const groupMembers = this.getActiveGroupCharacters();
@@ -1184,7 +1316,7 @@ class PromptManager {
11841316
11851317 const debouncedSaveServiceSettings = debouncePromise(() => this.saveServiceSettings(), 300);
11861318
11871319 const textarea = /** @type {HTMLTextAreaElement} */(document.getElementById(textareaIdentifier));
11881320 textarea.addEventListener('blur', () => {
11891321 prompt.content = textarea.value;
11901322 this.updatePromptByIdentifier(identifier, prompt);
@@ -1193,9 +1325,15 @@ class PromptManager {
11931325
11941326 }
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+ */
11961334 updateQuickEdit(identifier, prompt) {
11971335 const elementId = `${identifier}_prompt_quick_edit_textarea`;
11981336 const textarea = /** @type {HTMLTextAreaElement} */(document.getElementById(elementId));
11991337 textarea.value = prompt.content;
12001338
12011339 return elementId;
@@ -1220,30 +1358,35 @@ class PromptManager {
12201358
12211359 /**
12221360 * Loads a given prompt into the edit form fields.
12231361 * @param {ObjectPartial<Prompt>} prompt - Prompt object with properties 'name', 'role', 'content', and 'system_prompt'
12241362 */
12251363 loadPromptIntoEditForm(prompt) {
12261364 const nameField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name'));
12271365 const roleField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role'));
12281366 const promptField = /** @type {HTMLTextAreaElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt'));
12291367 const injectionPositionField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position'));
12301368 const injectionDepthField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth'));
12311369 const injectionOrderField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_order'));
12321370 const injectionDepthBlockinjectionTriggerField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_depth_blockprompt_manager_popup_entry_form_injection_trigger'));
12331371 const injectionOrderBlockinjectionDepthBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_order_blockprompt_manager_depth_block'));
12341372 const forbidOverridesFieldinjectionOrderBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_forbid_overridesprompt_manager_order_block'));
12351373 const forbidOverridesBlockforbidOverridesField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_forbid_overrides_blockprompt_manager_popup_entry_form_forbid_overrides'));
12361374 const entrySourceBlockforbidOverridesBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_source_blockprompt_manager_forbid_overrides_block'));
12371375 const entrySourceentrySourceBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_sourceprompt_manager_popup_entry_source_block'));
1376+ const entrySource = /** @type {HTMLSpanElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_source'));
12381377 const isPulledPrompt = Object.keys(this.promptSources).includes(prompt.identifier);
12391378
12401379 nameField.value = prompt.name ?? '';
12411380 roleField.value = prompt.role || 'system';
12421381 promptField.value = prompt.content ?? '';
12431382 promptField.disabled = prompt.marker ?? false;
12441383 injectionPositionField.value = (prompt.injection_position ?? INJECTION_POSITION.RELATIVE).toString();
12451384 injectionDepthField.value = (prompt.injection_depth ?? DEFAULT_DEPTH).toString();
12461385 injectionOrderField.value = (prompt.injection_order ?? 100DEFAULT_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 }));
12471390 injectionDepthBlock.style.visibility = prompt.injection_position === INJECTION_POSITION.ABSOLUTE ? 'visible' : 'hidden';
12481391 injectionOrderBlock.style.visibility = prompt.injection_position === INJECTION_POSITION.ABSOLUTE ? 'visible' : 'hidden';
12491392 injectionPositionField.removeAttribute('disabled');
@@ -1335,17 +1478,19 @@ class PromptManager {
13351478 const editArea = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_edit');
13361479 editArea.style.display = 'none';
13371480
13381481 const nameField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name'));
13391482 const roleField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role'));
13401483 const promptField = /** @type {HTMLTextAreaElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt'));
13411484 const injectionPositionField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position'));
13421485 const injectionDepthField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth'));
13431486 const injectionDepthBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block'));
13441487 const injectionOrderBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_order_block'));
13451488 const forbidOverridesFieldinjectionOrderField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_forbid_overridesprompt_manager_popup_entry_form_injection_order'));
13461489 const forbidOverridesBlockinjectionTriggerField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_forbid_overrides_blockprompt_manager_popup_entry_form_injection_trigger'));
13471490 const entrySourceBlockforbidOverridesField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_source_blockprompt_manager_popup_entry_form_forbid_overrides'));
13481491 const entrySourceforbidOverridesBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_sourceprompt_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
13501495 nameField.value = '';
13511496 roleField.selectedIndex = 0;
@@ -1353,7 +1498,9 @@ class PromptManager {
13531498 promptField.disabled = false;
13541499 injectionPositionField.selectedIndex = 0;
13551500 injectionPositionField.removeAttribute('disabled');
13561501 injectionDepthField.value = DEFAULT_DEPTH.toString();
1502+ injectionOrderField.value = DEFAULT_ORDER.toString();
1503+ injectionTriggerField.value = '';
13571504 injectionDepthBlock.style.visibility = 'unset';
13581505 injectionOrderBlock.style.visibility = 'unset';
13591506 forbidOverridesBlock.style.visibility = 'unset';
@@ -1373,22 +1520,30 @@ class PromptManager {
13731520
13741521 /**
13751522 * 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'.
13761524 * @returns {PromptCollection} A PromptCollection object
13771525 */
13781526 getPromptCollection(generationType) {
1527+ generationType = String(generationType || 'normal').toLowerCase().trim();
1528+ const promptCollection = new PromptCollection();
13791529 const promptOrder = this.getPromptOrderForCharacter(this.activeCharacter);
13801530
1381- const promptCollection = new PromptCollection();
13821531 promptOrder.forEach(entry => {
1383- if (true === entry.enabled) {
13841532 const prompt = this.getPromptById(entry.identifier);
1385- if (prompt) promptCollection.add(this.preparePrompt(prompt));
1533+ const allowedTrigger = entry.enabled && this.shouldTrigger(prompt, generationType);
1386- } else if (!entry.enabled && entry.identifier === 'main') {
1534+
1535+ if (!prompt) {
1536+ return;
1537+ }
1538+
1539+ if (allowedTrigger) {
1540+ promptCollection.add(this.preparePrompt(prompt));
1541+ } else if (entry.identifier === 'main') {
13871542 // Some extensions require main prompt to be present for relative inserts.
13881543 // So we make a GMO-free vegan replacement.
13891544 const promptreplacementPrompt = structuredClone(this.getPromptById(entry.identifier)prompt);
13901545 promptreplacementPrompt.content = '';
13911546 if (prompt) promptCollection.add(this.preparePrompt(promptreplacementPrompt));
13921547 }
13931548 });
13941549
@@ -1396,6 +1551,18 @@ class PromptManager {
13961551 }
13971552
13981553 /**
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+ /**
13991566 * Setter for messages property
14001567 *
14011568 * @param {import('./openai.js').MessageCollection} messages
@@ -1807,11 +1974,7 @@ class PromptManager {
18071974 * @returns {string} A string representation of an uuid4
18081975 */
18091976 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- });
18151978 }
18161979
18171980 /**
public/scripts/RossAscends-mods.js+13 -15
@@ -4,7 +4,6 @@ import {
44 characters,
55 online_status,
66 main_api,
7- api_server,
87 is_send_press,
98 max_context,
109 saveSettingsDebounced,
@@ -42,6 +41,7 @@ import { debounce_timeout } from './constants.js';
4241import { Popup } from './popup.js';
4342import { accountStorage } from './util/AccountStorage.js';
4443import { getCurrentUserHandle } from './user.js';
44+import { kai_settings } from './kai-settings.js';
4545
4646var RPanelPin = document.getElementById('rm_button_panel_pin');
4747var LPanelPin = document.getElementById('lm_button_panel_pin');
@@ -371,7 +371,7 @@ function RA_autoconnect(PrevApi) {
371371 if (online_status === 'no_connection' && power_user.auto_connect) {
372372 switch (main_api) {
373373 case 'kobold':
374374 if (kai_settings.api_server && isValidUrl(kai_settings.api_server)) {
375375 $('#api_button').trigger('click');
376376 }
377377 break;
@@ -397,8 +397,6 @@ function RA_autoconnect(PrevApi) {
397397 case 'openai':
398398 if (((secret_state[SECRET_KEYS.OPENAI] || oai_settings.reverse_proxy) && oai_settings.chat_completion_source == chat_completion_sources.OPENAI)
399399 || ((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)
402400 || (secret_state[SECRET_KEYS.OPENROUTER] && oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER)
403401 || (secret_state[SECRET_KEYS.AI21] && oai_settings.chat_completion_source == chat_completion_sources.AI21)
404402 || (secret_state[SECRET_KEYS.MAKERSUITE] && oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE)
@@ -434,19 +432,19 @@ function OpenNavPanels() {
434432 //auto-open R nav if locked and previously open
435433 if (accountStorage.getItem('NavLockOn') == 'true' && accountStorage.getItem('NavOpened') == 'true') {
436434 //console.log("RA -- clicking right nav to open");
437435 $('#rightNavDrawerIcon').clicktrigger('click');
438436 }
439437
440438 //auto-open L nav if locked and previously open
441439 if (accountStorage.getItem('LNavLockOn') == 'true' && accountStorage.getItem('LNavOpened') == 'true') {
442440 console.debug('RA -- clicking left nav to open');
443441 $('#leftNavDrawerIcon').clicktrigger('click');
444442 }
445443
446444 //auto-open WI if locked and previously open
447445 if (accountStorage.getItem('WINavLockOn') == 'true' && accountStorage.getItem('WINavOpened') == 'true') {
448446 console.debug('RA -- clicking WI to open');
449447 $('#WIDrawerIcon').clicktrigger('click');
450448 }
451449 }
452450}
@@ -711,7 +709,7 @@ export function initRossMods() {
711709 RA_autoconnect();
712710 }
713711
714712 $('#main_api').changeon('change', function () {
715713 var PrevAPI = main_api;
716714 setTimeout(() => RA_autoconnect(PrevAPI), 100);
717715 });
@@ -834,11 +832,11 @@ export function initRossMods() {
834832 });
835833
836834 var chatbarInFocus = false;
837835 $('#send_textarea').focuson('focus', function () {
838836 chatbarInFocus = true;
839837 });
840838
841839 $('#send_textarea').bluron('blur', function () {
842840 chatbarInFocus = false;
843841 });
844842
@@ -846,8 +844,8 @@ export function initRossMods() {
846844 OpenNavPanels();
847845 }, 300);
848846
849847 $(SelectedCharacterTab).clickon('click', function () { accountStorage.setItem('SelectedNavTab', 'rm_button_selected_ch'); });
850848 $('#rm_button_characters').clickon('click', function () { accountStorage.setItem('SelectedNavTab', 'rm_button_characters'); });
851849
852850 // 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() {
932930 var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');
933931 if (SwipeTargetMesClassParent !== null) {
934932 if (SwipeButR.css('display') === 'flex') {
935933 SwipeButR.clicktrigger('click');
936934 }
937935 }
938936 });
@@ -956,7 +954,7 @@ export function initRossMods() {
956954 var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');
957955 if (SwipeTargetMesClassParent !== null) {
958956 if (SwipeButL.css('display') === 'flex') {
959957 SwipeButL.clicktrigger('click');
960958 }
961959 }
962960 });
@@ -1159,7 +1157,7 @@ export function initRossMods() {
11591157 const lastMes = document.querySelector('.last_mes');
11601158 const editMes = lastMes.querySelector('.mes_block .mes_edit');
11611159 if (editMes !== null) {
11621160 $(editMes).clicktrigger('click');
11631161 return;
11641162 }
11651163 }
public/scripts/backgrounds.js+3 -6
@@ -421,7 +421,7 @@ async function autoBackgroundCommand() {
421421
422422 const list = options.map(option => `- ${option.text}`).join('\n');
423423 const prompt = stringFormat(autoBgPrompt, list);
424424 const reply = await generateQuietPrompt(prompt,{ false,quietPrompt: falseprompt });
425425 const fuse = new Fuse(options, { keys: ['text'] });
426426 const bestMatch = fuse.search(reply, { limit: 1 });
427427
@@ -524,7 +524,7 @@ async function resolveImageUrl(bg, isCustom) {
524524 ? bg
525525 : getThumbnailUrl('bg', bg);
526526
527527 return `url('"${thumbnailUrl}'")`;
528528}
529529
530530/**
@@ -636,12 +636,9 @@ async function uploadBackground(formData) {
636636 return;
637637 }
638638
639- const headers = getRequestHeaders();
640- delete headers['Content-Type'];
641-
642639 const response = await fetch('/api/backgrounds/upload', {
643640 method: 'POST',
644- headers: headers,
641+ headers: getRequestHeaders({ omitContentType: true }),
645642 body: formData,
646643 cache: 'no-cache',
647644 });
public/scripts/bulk-edit.js+6 -2
@@ -1,6 +1,6 @@
11import { characterGroupOverlay } from '../script.js';
22import { BulkEditOverlay, BulkEditOverlayState, CharacterContextMenu } from './BulkEditOverlay.js';
3-
3+import { event_types, eventSource } from './events.js';
44
55let is_bulk_edit = false;
66
@@ -121,4 +121,8 @@ export function initBulkEdit() {
121121 $('#bulkEditButton').on('click', onEditButtonClick);
122122 $('#bulkSelectAllButton').on('click', onSelectAllButtonClick);
123123 $('#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);
124128}
public/scripts/cfg-scale.js+1 -1
@@ -135,7 +135,7 @@ function onCfgMenuItemClick() {
135135 .siblings('.inline-drawer-content')
136136 .css('display') !== 'block') {
137137 $('#floatingPrompt').addClass('resizing');
138138 $('#CFGBlockToggle').clicktrigger('click');
139139 }
140140 } else {
141141 //hide AN if it's already displayed
public/scripts/chat-templates.js+78 -7
@@ -1,3 +1,5 @@
1+import { t } from './i18n.js';
2+
13// 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())"
24// note that chat templates must be trimmed to match the llama.cpp metadata value
35const hash_derivations = {
@@ -83,21 +85,31 @@ const hash_derivations = {
8385 '854b703e44ca06bdb196cc471c728d15dbab61e744fe6cdce980086b61646ed1':
8486 'GLM-4'
8587 ,
86-};
8788
88-const substr_derivations = {
89+ // Kimi K2, ...
89- '<|im_start|>': 'ChatML', // qwen2.5, ...
90+ 'aab20feb9bc6881f941ea649356130ffbc4943b3c2577c0991e1fba90de5a0fc':
91+ 'Moonshot AI'
92+ ,
9093};
9194
95+const 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+
92102const parse_derivation = derivation => (typeof derivation === 'string') ? {
93103 'context': derivation,
94104 'instruct': derivation,
95105} : derivation;
96106
107+const not_found = { context: null, instruct: null };
108+
97109export async function deriveTemplatesFromChatTemplate(chat_template, hash) {
98110 if (chat_template.trim() === '') {
99111 console.log('Missing chat template.');
100112 return nullnot_found;
101113 }
102114
103115 if (hash in hash_derivations) {
@@ -105,12 +117,71 @@ export async function deriveTemplatesFromChatTemplate(chat_template, hash) {
105117 }
106118
107119 // heuristics
108120 for (const [substrderivation, derivationsubstr] of Object.entries(substr_derivations) ) {
109121 if ([substr].flat().every(str => chat_template.includes(substrstr))) {
110122 return parse_derivation(derivation);
111123 }
112124 }
113125
114126 console.warn(`Unknown chat template hash: ${hash} for [${chat_template}]`);
115127 return nullnot_found;
128+}
129+
130+export 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;
116187}
public/scripts/chats.js+191 -17
@@ -43,6 +43,8 @@ import {
4343 extractTextFromOffice,
4444 download,
4545 getFileText,
46+ getFileExtension,
47+ convertTextToBase64,
4648} from './utils.js';
4749import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';
4850import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
@@ -68,7 +70,7 @@ import { accountStorage } from './util/AccountStorage.js';
6870 * @returns {Promise<string>} Converted file text
6971 */
7072
7173const fileSizeLimit = 1024 * 1024 * 100350; // 100350 MB
7274const ATTACHMENT_SOURCE = {
7375 GLOBAL: 'global',
7476 CHARACTER: 'character',
@@ -204,17 +206,16 @@ export async function populateFileAttachment(message, inputId = 'file_form_input
204206 const fileNamePrefix = `${Date.now()}_${slug}`;
205207 const fileBase64 = await getBase64Async(file);
206208 let base64Data = fileBase64.split(',')[1];
209+ const extension = getFileExtension(file);
207210
208211 // If file is image
209212 if (file.type.startsWith('image/')) {
210- const extension = file.type.split('/')[1];
211213 const imageUrl = await saveBase64AsFile(base64Data, name2, fileNamePrefix, extension);
212214 message.extra.image = imageUrl;
213215 message.extra.inline_image = true;
214216 }
215217 // If file is video
216218 else if (file.type.startsWith('video/')) {
217- const extension = file.type.split('/')[1];
218219 const videoUrl = await saveBase64AsFile(base64Data, name2, fileNamePrefix, extension);
219220 message.extra.video = videoUrl;
220221 } else {
@@ -224,7 +225,7 @@ export async function populateFileAttachment(message, inputId = 'file_form_input
224225 try {
225226 const converter = getConverter(file.type);
226227 const fileText = await converter(file);
227228 base64Data = window.btoa(unescape(encodeURIComponentconvertTextToBase64(fileText)));
228229 } catch (error) {
229230 toastr.error(String(error), t`Could not convert file`);
230231 console.error('Could not convert file', error);
@@ -247,6 +248,7 @@ export async function populateFileAttachment(message, inputId = 'file_form_input
247248
248249 } catch (error) {
249250 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`);
250252 } finally {
251253 $('#file_form').trigger('reset');
252254 }
@@ -476,7 +478,7 @@ export async function appendFileContent(message, messageText) {
476478export function encodeStyleTags(text) {
477479 const styleRegex = /<style>(.+?)<\/style>/gims;
478480 return text.replaceAll(styleRegex, (_, match) => {
479481 return `<custom-style>${escapeencodeURIComponent(match)}</custom-style>`;
480482 });
481483}
482484
@@ -497,14 +499,7 @@ export function decodeStyleTags(text, { prefix } = { prefix: '.mes_text ' }) {
497499 for (let i = 0; i < rule.selectors.length; i++) {
498500 const selector = rule.selectors[i];
499501 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;
508503 }
509504 }
510505 }
@@ -513,6 +508,36 @@ export function decodeStyleTags(text, { prefix } = { prefix: '.mes_text ' }) {
513508 }
514509 }
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+
516541 function sanitizeRuleSet(ruleSet) {
517542 if (Array.isArray(ruleSet.selectors) || Array.isArray(ruleSet.declarations)) {
518543 sanitizeRule(ruleSet);
@@ -529,7 +554,7 @@ export function decodeStyleTags(text, { prefix } = { prefix: '.mes_text ' }) {
529554
530555 return text.replaceAll(styleDecodeRegex, (_, style) => {
531556 try {
532557 let styleCleaned = unescapedecodeURIComponent(style).replaceAll(/<br\/>/g, '');
533558 const ast = css.parse(styleCleaned);
534559 const sheet = ast?.stylesheet;
535560 if (sheet) {
@@ -893,6 +918,27 @@ async function deleteMessageImage() {
893918 await saveChatConditional();
894919}
895920
921+async 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+
896942/**
897943 * Deletes file from the server.
898944 * @param {string} url Path to the file on the server
@@ -1475,14 +1521,14 @@ export async function uploadFileAttachmentToServer(file, target) {
14751521 try {
14761522 const converter = getConverter(file.type);
14771523 const fileText = await converter(file);
14781524 base64Data = window.btoa(unescape(encodeURIComponentconvertTextToBase64(fileText)));
14791525 } catch (error) {
14801526 toastr.error(String(error), t`Could not convert file`);
14811527 console.error('Could not convert file', error);
14821528 }
14831529 } else {
14841530 const fileText = await file.text();
14851531 base64Data = window.btoa(unescape(encodeURIComponentconvertTextToBase64(fileText)));
14861532 }
14871533
14881534 const fileUrl = await uploadFileAttachment(uniqueFileName, base64Data);
@@ -1670,6 +1716,132 @@ export function registerFileConverter(mimeType, converter) {
16701716 converters[mimeType] = converter;
16711717}
16721718
1719+export 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+
16731845export function initChatUtilities() {
16741846 $(document).on('click', '.mes_hide', async function () {
16751847 const messageBlock = $(this).closest('.mes');
@@ -1849,13 +2021,15 @@ export function initChatUtilities() {
18492021 reloadCurrentChat();
18502022 });
18512023
18522024 $('#creators_note_styles_button').on('click', function (e) {
2025+ e.stopPropagation();
18532026 openGlobalStylesPreferenceDialog();
18542027 });
18552028
18562029 $(document).on('click', '.mes_img', expandMessageImage);
18572030 $(document).on('click', '.mes_img_enlarge', expandAndZoomMessageImage);
18582031 $(document).on('click', '.mes_img_delete', deleteMessageImage);
2032+ $(document).on('click', '.mes_video_delete', deleteMessageVideo);
18592033
18602034 $('#file_form_input').on('change', async () => {
18612035 const fileInput = document.getElementById('file_form_input');
public/scripts/constants.js+18 -0
@@ -22,3 +22,21 @@ export const debounce_timeout = {
2222 * which is needed to preserve world info timed effects.
2323 */
2424export 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+ */
30+export 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+ */
35+export 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 @@
11import { getPresetManager } from './preset-manager.js';
22import { extractJsonFromData, extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../script.js';
33import { getTextGenServer } from './textgen-settings.js';
44import { extractReasoningFromData } from './reasoning.js';
55import { formatInstructModeChat, formatInstructModePrompt, getInstructStoppingSequences, names_behavior_types } from './instruct-mode.js';
@@ -467,7 +467,7 @@ export class ChatCompletionService {
467467 return json;
468468 }
469469
470470 returnconst result = {
471471 content: extractMessageFromData(json, this.TYPE),
472472 reasoning: extractReasoningFromData(json, {
473473 mainApi: this.TYPE,
@@ -475,6 +475,11 @@ export class ChatCompletionService {
475475 ignoreShowThoughts: true,
476476 }),
477477 };
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;
478483 }
479484
480485 if (!response.ok) {
public/scripts/data-maid.js+21 -9
@@ -1,4 +1,5 @@
11import { getRequestHeaders } from '../script.js';
2+import { VIDEO_EXTENSIONS } from './constants.js';
23import { t } from './i18n.js';
34import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';
45import { renderTemplateAsync } from './templates.js';
@@ -44,6 +45,10 @@ class DataMaidDialog {
4445 name: t`Background Thumbnails`,
4546 description: t`Thumbnails for missing or deleted backgrounds.`,
4647 },
48+ personaThumbnails: {
49+ name: t`Persona Thumbnails`,
50+ description: t`Thumbnails for missing or deleted personas.`,
51+ },
4752 chatBackups: {
4853 name: t`Chat Backups`,
4954 description: t`Automatically generated chat backups.`,
@@ -217,8 +222,9 @@ class DataMaidDialog {
217222 button.addEventListener('click', async () => {
218223 const item = button.closest('.dataMaidItem');
219224 const hash = item?.getAttribute('data-hash');
225+ const itemName = items.find(i => i.hash === hash)?.name;
220226 if (hash) {
221227 await this.view(prop, hash, itemName);
222228 }
223229 });
224230 });
@@ -304,13 +310,14 @@ class DataMaidDialog {
304310 * Opens the item view for a specific hash.
305311 * @param {string} prop Property name for the category
306312 * @param {string} hash Item hash to view
313+ * @param {string} name Name of the item to view
307314 * @private
308315 */
309316 async view(prop, hash, name) {
310317 const url = this.getViewUrl(hash);
311318 const isImage = ['images', 'avatarThumbnails', 'backgroundThumbnails'].includes(prop);
312319 const element = isImage
313320 ? await this.getViewElement(url, name)
314321 : await this.getTextViewElement(url);
315322 await callGenericPopup(element, POPUP_TYPE.DISPLAY, '', { large: true, wide: true });
316323 }
@@ -341,16 +348,21 @@ class DataMaidDialog {
341348 }
342349
343350 /**
344351 * Gets ana imagemedia element for viewing images or videos.
345352 * @param {string} url View URL
353+ * @param {string} name Name of the file
346354 * @returns {Promise<HTMLElement>} Image element
347355 * @private
348356 */
349357 async getViewElement(url, name) {
350358 const imgisVideo = documentVIDEO_EXTENSIONS.createElementincludes(name.split('img.').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;
354366 }
355367
356368 /**
public/scripts/events.js+92 -0
@@ -0,0 +1,92 @@
1+import { EventEmitter } from '../lib/eventemitter.js';
2+
3+export 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+
92+export 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';
77import { getRequestHeaders, processDroppedFiles, eventSource, event_types } from '../../../script.js';
88import { deleteExtension, extensionNames, getContext, installExtension, renderExtensionTemplateAsync } from '../../extensions.js';
99import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
1010import { executeSlashCommandsexecuteSlashCommandsWithOptions } from '../../slash-commands.js';
1111import { accountStorage } from '../../util/AccountStorage.js';
1212import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js';
1313import { t } from '../../i18n.js';
@@ -142,7 +142,7 @@ async function downloadAssetsList(url) {
142142 const assetDelete = async function () {
143143 if (assetType === 'character') {
144144 toastr.error('Go to the characters menu to delete a character.', 'Character deletion not supported');
145145 await executeSlashCommandsexecuteSlashCommandsWithOptions(`/go ${asset['id']}`);
146146 return;
147147 }
148148 element.off('click');
public/scripts/extensions/caption/index.js+12 -3
@@ -1,4 +1,4 @@
11import { ensureImageFormatSupported, getBase64Async, getFileExtension, isTrueBoolean, saveBase64AsFile } from '../../utils.js';
22import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js';
33import { appendMediaToMessage, eventSource, event_types, getRequestHeaders, saveChatConditional, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js';
44import { getMessageTimeStamp } from '../../RossAscends-mods.js';
@@ -53,6 +53,10 @@ function migrateSettings() {
5353 if (!extension_settings.caption.template) {
5454 extension_settings.caption.template = TEMPLATE_DEFAULT;
5555 }
56+
57+ if (!extension_settings.caption.show_in_chat) {
58+ extension_settings.caption.show_in_chat = false;
59+ }
5660}
5761
5862/**
@@ -167,6 +171,7 @@ async function sendCaptionedMessage(caption, image) {
167171 extra: {
168172 image: image,
169173 title: messageText,
174+ inline_image: !!extension_settings.caption.show_in_chat,
170175 },
171176 };
172177 context.chat.push(message);
@@ -327,11 +332,11 @@ async function getCaptionForFile(file, prompt, quiet) {
327332 setSpinnerIcon();
328333 const context = getContext();
329334 const fileData = await getBase64Async(await ensureImageFormatSupported(file));
330- const base64Format = fileData.split(',')[0].split(';')[0].split('/')[1];
335+ const extension = getFileExtension(file);
331336 const base64Data = fileData.split(',')[1];
332337 const { caption } = await doCaptionRequest(base64Data, fileData, prompt);
333338 if (!quiet) {
334339 const imagePath = await saveBase64AsFile(base64Data, context.name2, '', base64Formatextension);
335340 await sendCaptionedMessage(caption, imagePath);
336341 }
337342 return caption;
@@ -596,6 +601,10 @@ jQuery(async function () {
596601 extension_settings.caption.alt_endpoint_enabled = !!$('#caption_altEndpoint_enabled').prop('checked');
597602 saveSettingsDebounced();
598603 });
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
600609 const onMessageEvent = async (index) => {
601610 if (!extension_settings.caption.auto_mode) {
public/scripts/extensions/caption/settings.html+10 -4
@@ -48,6 +48,7 @@
4848 <option data-type="mistral" value="pixtral-large-2411">pixtral-large-2411</option>
4949 <option data-type="mistral" value="mistral-large-pixtral-2411">mistral-large-pixtral-2411</option>
5050 <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>
5152 <option data-type="mistral" value="mistral-small-latest">mistral-small-latest</option>
5253 <option data-type="mistral" value="mistral-medium-latest">mistral-medium-latest</option>
5354 <option data-type="mistral" value="mistral-medium-2505">mistral-medium-2505</option>
@@ -84,7 +85,6 @@
8485 <option data-type="anthropic" value="claude-3-5-haiku-latest">claude-3-5-haiku-latest</option>
8586 <option data-type="anthropic" value="claude-3-5-haiku-20241022">claude-3-5-haiku-20241022</option>
8687 <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>
8888 <option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
8989 <option data-type="google" value="gemini-2.5-pro">gemini-2.5-pro</option>
9090 <option data-type="google" value="gemini-2.5-pro-preview-06-05">gemini-2.5-pro-preview-06-05</option>
@@ -189,6 +189,7 @@
189189 <option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option>
190190 <option data-type="vllm" value="vllm_current" data-i18n="currently_selected">[Currently selected]</option>
191191 <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>
192193 <option data-type="xai" value="grok-2-vision-1212">grok-2-vision-1212</option>
193194 <option data-type="xai" value="grok-vision-beta">grok-vision-beta</option>
194195 <option data-type="pollinations" value="openai">openai</option>
@@ -228,23 +229,28 @@
228229 </div>
229230 <div id="caption_prompt_block">
230231 <label for="caption_prompt" data-i18n="Caption Prompt">Caption Prompt</label>
231232 <textarea id="caption_prompt" class="text_pole textarea_compact autoSetHeight" rows="1" placeholder="&lt; Use default &gt;">{{PROMPT_DEFAULT}}</textarea>
232233 <label class="checkbox_label margin-bot-10px" for="caption_prompt_ask" title="Ask for a custom prompt every time an image is captioned.">
233234 <input id="caption_prompt_ask" type="checkbox" class="checkbox">
234235 <span data-i18n="Ask every time">Ask every time</span>
235236 </label>
236237 </div>
237238 <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>
238239 <textarea id="caption_template" class="text_pole textarea_compact autoSetHeight" rows="2" placeholder="&lt; Use default &gt;">{{TEMPLATE_DEFAULT}}</textarea>
239240 <label class="checkbox_label" for="caption_auto_mode">
240241 <input id="caption_auto_mode" type="checkbox" class="checkbox">
241242 <span data-i18n="Automatically caption images">Automatically caption images</span>
242243 <i class="fa-solid fa-info-circle" title="Automatically caption images when they are pasted into the chat or attached to messages."></i>
243244 </label>
244245 <label class="checkbox_label margin-bot-10px" for="caption_refine_mode">
245246 <input id="caption_refine_mode" type="checkbox" class="checkbox">
246247 <span data-i18n="Edit captions before saving">Edit captions before saving</span>
247248 </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>
248254 </div>
249255 </div>
250256</div>
public/scripts/extensions/expressions/index.js+30 -15
@@ -1,6 +1,6 @@
11import { Fuse } from '../../../lib.js';
22
33import { characters, eventSource, event_types, generateQuietPrompt, generateRaw, getRequestHeaders, main_api, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types, this_chid } from '../../../script.js';
44import { dragElement, isMobile } from '../../RossAscends-mods.js';
55import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
66import { loadMovingUIState, performFuzzySearch, power_user } from '../../power-user.js';
@@ -1056,10 +1056,10 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin
10561056 inApiCall = true;
10571057 switch (extension_settings.expressions.promptType) {
10581058 case PROMPT_TYPE.raw:
10591059 emotionResponse = await generateRaw(text,{ main_api,prompt: falsetext, false,systemPrompt: prompt });
10601060 break;
10611061 case PROMPT_TYPE.full:
10621062 emotionResponse = await generateQuietPrompt(prompt,{ false,quietPrompt: falseprompt });
10631063 break;
10641064 }
10651065 } finally {
@@ -1331,7 +1331,7 @@ async function renderFallbackExpressionPicker() {
13311331 defaultPicker.empty();
13321332
13331333
13341334 addOption(OPTION_NO_FALLBACK, '[ No fallback ]', !extension_settings.expressions.fallback_expression && !extension_settings.expressions.showDefault);
13351335 addOption(OPTION_EMOJI_FALLBACK, '[ Default emojis ]', !!extension_settings.expressions.showDefault);
13361336
13371337 for (const expression of expressions) {
@@ -1758,27 +1758,38 @@ async function onExpressionFallbackChanged() {
17581758 saveSettingsDebounced();
17591759}
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+ */
17611767async function handleFileUpload(url, formData) {
17621768 try {
17631769 const dataresult = await jQuery.ajaxfetch(url, {
17641770 typemethod: 'POST',
1765- url: url,
1771+ headers: getRequestHeaders({ omitContentType: true }),
17661772 databody: formData,
1767- beforeSend: function () { },
1773+ cache: 'no-cache',
1768- cache: false,
1769- contentType: false,
1770- processData: false,
17711774 });
17721775
1776+ if (!result.ok) {
1777+ throw new Error(`Upload failed with status ${result.status}`);
1778+ }
1779+
1780+ const data = await result.json();
1781+
17731782 // Refresh sprites list
17741783 const name = formData.get('name').toString();
17751784 delete spriteCache[name];
17761785 await fetchImagesNoCache();
17771786 await validateImages(name);
17781787
17791788 return data ?? {};
17801789 } catch (error) {
1790+ console.error('Error uploading image:', error);
17811791 toastr.error('Failed to upload image');
1792+ return {};
17821793 }
17831794}
17841795
@@ -1996,7 +2007,11 @@ async function onClickExpressionUploadPackButton() {
19962007 const uploadToast = toastr.info('Please wait...', 'Upload is processing', { timeOut: 0, extendedTimeOut: 0 });
19972008 const { count } = await handleFileUpload('/api/sprites/upload-zip', formData);
19982009 toastr.clear(uploadToast);
2010+
2011+ // Only show success message if at least one image was uploaded
2012+ if (count) {
19992013 toastr.success(`Uploaded ${count} image(s) for ${name}`);
2014+ }
20002015
20012016 // Reset the input
20022017 e.target.form.reset();
@@ -2116,7 +2131,7 @@ function migrateSettings() {
21162131 saveSettingsDebounced();
21172132 }
21182133
21192134 if (extension_settings.expressions.showDefault && extension_settings.expressions.fallback_expression !== undefined) {
21202135 extension_settings.expressions.showDefault = false;
21212136 saveSettingsDebounced();
21222137 }
public/scripts/extensions/gallery/index.js+9 -23
@@ -8,7 +8,7 @@ import {
88 animation_easing,
99} from '../../../script.js';
1010import { groups, selected_group } from '../../group-chats.js';
1111import { loadFileToDocument, delay, getBase64Async, getSanitizedFilename, saveBase64AsFile, getFileExtension } from '../../utils.js';
1212import { loadMovingUIState } from '../../power-user.js';
1313import { dragElement } from '../../RossAscends-mods.js';
1414import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
@@ -324,7 +324,6 @@ async function showCharGallery(deleteModeState = false) {
324324 await delay(100);
325325 await initGallery(items, url);
326326 } catch (err) {
327- console.trace();
328327 console.error(err);
329328 }
330329}
@@ -341,27 +340,12 @@ async function showCharGallery(deleteModeState = false) {
341340async function uploadFile(file, url) {
342341 try {
343342 // Convert the file to a base64 string
344343 const base64DatafileBase64 = 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 payload
348+ 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}`);
365349 } catch (error) {
366350 console.error('There was an issue uploading the file:', error);
367351
@@ -620,6 +604,9 @@ function makeDragImg(id, url) {
620604 }
621605 draggableElem.id = uniqueId;
622606
607+ // Add the galleryImageDraggable to have unique class
608+ draggableElem.classList.add('galleryImageDraggable');
609+
623610 // Ensure that the newly added element is displayed as block
624611 draggableElem.style.display = 'block';
625612 //and has no padding unlike other non-zoomed-avatar draggables
@@ -749,7 +736,6 @@ async function listGalleryCommand(args) {
749736 return JSON.stringify(items.map(it => it.src));
750737
751738 } catch (err) {
752- console.trace();
753739 console.error(err);
754740 }
755741 return JSON.stringify([]);
public/scripts/extensions/memory/index.js+15 -3
@@ -506,7 +506,7 @@ async function summarizeCallback(args, text) {
506506 case summary_sources.extras:
507507 return await callExtrasSummarizeAPI(text);
508508 case summary_sources.main:
509509 return removeReasoningFromString(await generateRaw(text,{ '',prompt: falsetext, false,systemPrompt: prompt, responseLength: extension_settings.memory.overrideResponseLength }));
510510 case summary_sources.webllm: {
511511 const messages = [{ role: 'system', content: prompt }, { role: 'user', content: text }].filter(m => m.content);
512512 const params = extension_settings.memory.overrideResponseLength > 0 ? { max_tokens: extension_settings.memory.overrideResponseLength } : {};
@@ -677,7 +677,13 @@ async function summarizeChatMain(context, force, skipWIAN) {
677677 if (prompt_builders.DEFAULT === extension_settings.memory.prompt_builder) {
678678 try {
679679 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);
681687 } finally {
682688 inApiCall = false;
683689 }
@@ -701,7 +707,13 @@ async function summarizeChatMain(context, force, skipWIAN) {
701707 return null;
702708 }
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);
705717 summary = removeReasoningFromString(rawSummary);
706718 index = lastUsedIndex;
707719 } finally {
public/scripts/extensions/quick-reply/src/QuickReplySettings.js+3 -3
@@ -1,5 +1,5 @@
11import { chat_metadata, saveChatDebounced, saveSettingsDebounced } from '../../../../script.js';
22import { extension_settings, saveMetadataDebounced } from '../../../extensions.js';
33import { QuickReplyConfig } from './QuickReplyConfig.js';
44
55export class QuickReplySettings {
@@ -62,7 +62,7 @@ export class QuickReplySettings {
6262 saveSettingsDebounced();
6363 if (this.chatConfig) {
6464 chat_metadata.quickReply = this.chatConfig.toJSON();
6565 saveChatDebouncedsaveMetadataDebounced();
6666 }
6767 if (this.onSave) {
6868 this.onSave();
public/scripts/extensions/regex/index.js+5 -0
@@ -375,6 +375,11 @@ function migrateSettings() {
375375 performSave = true;
376376 }
377377
378+ if (!Array.isArray(script.placement)) {
379+ script.placement = [];
380+ performSave = true;
381+ }
382+
378383 if (script.placement.includes(regex_placement.MD_DISPLAY)) {
379384 script.placement = script.placement.length === 1 ?
380385 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) {
3838 const isVllm = extension_settings.caption.multimodal_api === 'vllm';
3939 const base64Bytes = base64Img.length * 0.75;
4040 const compressionLimit = 2 * 1024 * 1024;
41+ const safeMimeTypes = ['image/jpeg', 'image/png', 'image/webp'];
42+ const mimeType = base64Img?.split(';')?.[0]?.split(':')?.[1];
4143 const thumbnailNeeded = ['google', 'openrouter', 'mistral', 'groq', 'vertexai'].includes(extension_settings.caption.multimodal_api);
4244 if ((thumbnailNeeded && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) {
4345 const maxSide = 10242048;
4446 base64Img = await createThumbnail(base64Img, maxSide, maxSide, 'image/jpeg');
47+ } else if (!safeMimeTypes.includes(mimeType)) {
48+ base64Img = await createThumbnail(base64Img, null, null);
4549 }
4650
4751 const proxyUrl = useReverseProxy ? oai_settings.reverse_proxy : '';
@@ -467,7 +471,7 @@ export class ConnectionManagerRequestService {
467471
468472 /**
469473 * @param {import('./connection-manager/index.js').ConnectionProfile?} [profile]
470474 * @return {import('../../scriptslash-commands.js').ConnectAPIMap}
471475 * @throws {Error}
472476 */
473477 static validateProfile(profile) {
public/scripts/extensions/stable-diffusion/index.js+27 -11
@@ -51,7 +51,7 @@ import {
5151 SlashCommandArgument,
5252 SlashCommandNamedArgument,
5353} from '../../slash-commands/SlashCommandArgument.js';
5454import { debounce_timeout, VIDEO_EXTENSIONS } from '../../constants.js';
5555import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
5656import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js';
5757import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
@@ -339,6 +339,7 @@ const defaultSettings = {
339339};
340340
341341const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);
342+const isVideo = (/** @type {string} */ format) => VIDEO_EXTENSIONS.includes(String(format || '').trim().toLowerCase());
342343
343344/**
344345 * Generate interceptor for interactive mode triggers.
@@ -2476,14 +2477,14 @@ async function generatePicture(initiator, args, trigger, message, callback) {
24762477
24772478 if (generationType === generationMode.BACKGROUND) {
24782479 const callbackOriginal = callback;
24792480 callback = async function (prompt, imagePath, generationType, _negativePromptPrefix, _initiator, prefixedPrompt, format) {
24802481 const imgUrl = `url("${encodeURI(imagePath)}")`;
24812482 await eventSource.emit(event_types.FORCE_SET_BACKGROUND, { url: imgUrl, path: imagePath });
24822483
24832484 if (typeof callbackOriginal === 'function') {
24842485 await callbackOriginal(prompt, imagePath, generationType, negativePromptPrefix, initiator, prefixedPrompt, format);
24852486 } else {
24862487 await sendMessage(prompt, imagePath, generationType, negativePromptPrefix, initiator, prefixedPrompt, format);
24872488 }
24882489 };
24892490 }
@@ -2725,7 +2726,7 @@ function getUserAvatarUrl() {
27252726 * @returns {Promise<string>} - A promise that resolves when the prompt generation completes.
27262727 */
27272728async function generatePrompt(quietPrompt) {
27282729 const reply = await generateQuietPrompt(quietPrompt,{ false,quietPrompt false});
27292730 const processedReply = processReply(reply);
27302731
27312732 if (!processedReply) {
@@ -2838,8 +2839,8 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
28382839 const filename = `${characterName}_${humanizedDateTime()}`;
28392840 const base64Image = await saveBase64AsFile(result.data, characterName, filename, result.format);
28402841 callback
28412842 ? await callback(prompt, base64Image, generationType, additionalNegativePrefix, initiator, prefixedPrompt, result.format)
28422843 : await sendMessage(prompt, base64Image, generationType, additionalNegativePrefix, initiator, prefixedPrompt, result.format);
28432844 return base64Image;
28442845}
28452846
@@ -3524,7 +3525,8 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
35243525 const text = await promptResult.text();
35253526 throw new Error(text);
35263527 }
35273528 returnconst { format: 'png', data: } = await promptResult.textjson() };
3529+ return { format, data };
35283530}
35293531
35303532
@@ -3864,8 +3866,9 @@ async function onComfyDeleteWorkflowClick() {
38643866 * @param {string} additionalNegativePrefix Additional negative prompt used for the image generation
38653867 * @param {string} initiator The initiator of the image generation
38663868 * @param {string} prefixedPrompt Prompt with an attached specific prefix
3869+ * @param {string} format Format of the image (e.g., 'png', 'jpg')
38673870 */
38683871async function sendMessage(prompt, image, generationType, additionalNegativePrefix, initiator, prefixedPrompt, format) {
38693872 const context = getContext();
38703873 const name = context.groupId ? systemUserName : context.name2;
38713874 const template = extension_settings.sd.prompts[generationMode.MESSAGE] || '{{prompt}}';
@@ -3885,6 +3888,12 @@ async function sendMessage(prompt, image, generationType, additionalNegativePref
38853888 image_swipes: [image],
38863889 },
38873890 };
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+ }
38883897 context.chat.push(message);
38893898 const messageId = context.chat.length - 1;
38903899 await eventSource.emit(event_types.MESSAGE_RECEIVED, messageId, 'extension');
@@ -4068,7 +4077,7 @@ async function sdMessageButton(e) {
40684077 }
40694078 }
40704079
40714080 function saveGeneratedImage(prompt, image, generationType, negative, _initiator, _prefixedPrompt, format) {
40724081 // Some message sources may not create the extra object
40734082 if (typeof message.extra !== 'object' || message.extra === null) {
40744083 message.extra = {};
@@ -4085,17 +4094,24 @@ async function sdMessageButton(e) {
40854094 swipes.push(message.extra.image);
40864095 }
40874096
4097+ const isVideoFormat = isVideo(format);
4098+
4099+ if (isVideoFormat) {
4100+ message.extra.video = image;
4101+ } else {
40884102 swipes.push(image);
40894103
40904104 // If already contains an image and it's not inline - leave it as is
40914105 message.extra.inline_image = !(message.extra.image && !message.extra.inline_image);
40924106 message.extra.image = image;
4107+ }
4108+
40934109 message.extra.title = prompt;
40944110 message.extra.generationType = generationType;
40954111 message.extra.negative = negative;
40964112 appendMediaToMessage(message, $mes);
40974113
40984114 return context.saveChat();
40994115 }
41004116}
41014117
public/scripts/extensions/token-counter/index.js+0 -13
@@ -9,19 +9,6 @@ import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
99import { renderExtensionTemplateAsync } from '../../extensions.js';
1010import { t } from '../../i18n.js';
1111
12-function 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-
2512async function doTokenCounter() {
2613 const { tokenizerName, tokenizerId } = getFriendlyTokenizerName(main_api);
2714 const html = await renderExtensionTemplateAsync('token-counter', 'window', { tokenizerName });
public/scripts/extensions/translate/index.html+8 -2
@@ -28,8 +28,14 @@
2828 <option value="free">Free</option>
2929 <option value="pro">Pro</option>
3030 </select>
3131 <div id="translate_key_button" class="menu_button fa-solid fa-keymenu_button_icon margin0 manage-api-keys"></div>
3232 <div id="translate_url_button"i class="menu_button fa-solid fa-link margin0key"></divi>
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>
3339 </div>
3440 <label data-i18n="ext_translate_target_lang" for="translation_target_language">Target Language</label>
3541 <select id="translation_target_language" name="target_language"></select>
public/scripts/extensions/translate/index.js+11 -58
@@ -10,9 +10,9 @@ import {
1010 updateMessageBlock,
1111} from '../../../script.js';
1212import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js';
1313import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from '../../popup.js';
1414import { updateReasoningUI } from '../../reasoning.js';
1515import { findSecret, secret_state, writeSecret } from '../../secrets.js';
1616import { SlashCommand } from '../../slash-commands/SlashCommand.js';
1717import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
1818import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
@@ -152,9 +152,9 @@ const LOCAL_URL = ['libre', 'oneringtranslator', 'deeplx', 'lingva'];
152152function showKeysButton() {
153153 const providerRequiresKey = KEY_REQUIRED.includes(extension_settings.translate.provider);
154154 const providerOptionalUrl = LOCAL_URL.includes(extension_settings.translate.provider);
155155 $('#translate_key_button').toggle(providerRequiresKey).data('key', extension_settings.translate.provider);
156156 $('#translate_key_button').toggleClass('success', Boolean(secret_state[extension_settings.translate.provider]));
157157 $('#translate_url_button').toggle(providerOptionalUrl).data('key', extension_settings.translate.provider + '_url');
158158 $('#translate_url_button').toggleClass('success', Boolean(secret_state[extension_settings.translate.provider + '_url']));
159159 $('#deepl_api_endpoint').toggle(extension_settings.translate.provider === 'deepl');
160160}
@@ -749,63 +749,16 @@ jQuery(async () => {
749749 saveSettingsDebounced();
750750 });
751751 $(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) => {
755+ if (key === extension_settings.translate.provider) {
756+ $('#translate_key_button').toggleClass('success', !!secret_state[extension_settings.translate.provider]);
769757 }
770-
758+ if (key === `${extension_settings.translate.provider}_url`) {
771- await writeSecret(extension_settings.translate.provider, key);
759+ $('#translate_url_button').toggleClass('success', !!secret_state[`${extension_settings.translate.provider}_url`]);
772- toastr.success('API Key saved');
773- $('#translate_key_button').addClass('success');
774- });
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- });
800-
801- if (!url) {
802- return;
803760 }
804-
761+ });
805- await writeSecret(secretKey, url);
806-
807- toastr.success('API URL saved');
808- $('#translate_url_button').addClass('success');
809762 });
810763
811764 loadSettings();
public/scripts/extensions/tts/alltalk.js+9 -9
@@ -296,20 +296,20 @@ class AllTalkTtsProvider {
296296 const ttsNarrateDialoguesCheckbox = document.getElementById('tts_narrate_dialogues');
297297 if (this.settings.narrator_enabled) {
298298 ttsPassAsterisksCheckbox.checked = false;
299299 $('#tts_pass_asterisks').clicktrigger('click');
300300 $('#tts_pass_asterisks').trigger('change');
301301 }
302302 if (!this.settings.narrator_enabled) {
303303 ttsPassAsterisksCheckbox.checked = true;
304304 $('#tts_pass_asterisks').clicktrigger('click');
305305 $('#tts_pass_asterisks').trigger('change');
306306 }
307307 if (this.settings.narrator_enabled) {
308308 ttsNarrateQuotedCheckbox.checked = true;
309309 ttsNarrateDialoguesCheckbox.checked = true;
310310 $('#tts_narrate_quoted').clicktrigger('click');
311311 $('#tts_narrate_quoted').trigger('change');
312312 $('#tts_narrate_dialogues').clicktrigger('click');
313313 $('#tts_narrate_dialogues').trigger('change');
314314 }
315315 atNarratorSelect.value = this.settings.narrator_enabled.toString();
@@ -783,21 +783,21 @@ class AllTalkTtsProvider {
783783
784784 if (narratorOption === 'true') {
785785 ttsPassAsterisksCheckbox.checked = false;
786786 $('#tts_pass_asterisks').clicktrigger('click');
787787 $('#tts_pass_asterisks').trigger('change');
788788 ttsNarrateQuotedCheckbox.checked = true;
789789 ttsNarrateDialoguesCheckbox.checked = true;
790790 $('#tts_narrate_quoted').clicktrigger('click');
791791 $('#tts_narrate_quoted').trigger('change');
792792 $('#tts_narrate_dialogues').clicktrigger('click');
793793 $('#tts_narrate_dialogues').trigger('change');
794794 } else if (narratorOption === 'silent') {
795795 ttsPassAsterisksCheckbox.checked = false;
796796 $('#tts_pass_asterisks').clicktrigger('click');
797797 $('#tts_pass_asterisks').trigger('change');
798798 } else {
799799 ttsPassAsterisksCheckbox.checked = true;
800800 $('#tts_pass_asterisks').clicktrigger('click');
801801 $('#tts_pass_asterisks').trigger('change');
802802 }
803803
public/scripts/extensions/tts/azure.js+19 -31
@@ -1,6 +1,5 @@
11import { event_types, eventSource, getRequestHeaders } from '../../../script.js';
22import { POPUP_RESULT, POPUP_TYPESECRET_KEYS, callGenericPopupsecret_state } from '../../popupsecrets.js';
3-import { SECRET_KEYS, findSecret, secret_state, writeSecret } from '../../secrets.js';
43import { getPreviewString, saveTtsProviderSettings } from './index.js';
54export { AzureTtsProvider };
65
@@ -26,7 +25,7 @@ class AzureTtsProvider {
2625 <h4 for="azure_tts_key" class="flex1 margin0">
2726 <a href="https://portal.azure.com/" target="_blank">Azure TTS Key</a>
2827 </h4>
2928 <div id="azure_tts_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_azure_tts">
3029 <i class="fa-solid fa-key"></i>
3130 <span>Click to set</span>
3231 </div>
@@ -39,6 +38,20 @@ class AzureTtsProvider {
3938 return html;
4039 }
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+
4255 onSettingsChange() {
4356 // Update dynamically
4457 this.settings.region = String($('#azure_tts_region').val());
@@ -65,34 +78,9 @@ class AzureTtsProvider {
6578 }
6679
6780 $('#azure_tts_region').val(this.settings.region).on('input', () => this.onSettingsChange());
68- $('#azure_tts_key').toggleClass('success', secret_state[SECRET_KEYS.AZURE_TTS]);
69- $('#azure_tts_key').on('click', async () => {
70- const popupText = 'Azure TTS API Key';
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, '');
8081 $('#azure_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.AZURE_TTS]);
81- toastr.success('API Key removed');
82+ [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
82- await this.onRefreshClick();
83+ eventSource.on(event, this.handler);
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();
9684 });
9785
9886 try {
public/scripts/extensions/tts/elevenlabs.js+26 -5
@@ -16,8 +16,9 @@ class ElevenLabsTtsProvider {
1616 similarity_boost: 0.75,
1717 style_exaggeration: 0.00,
1818 speaker_boost: true,
19+ speed: 1.0,
1920 apiKey: '',
2021 model: 'eleven_monolingual_v1eleven_turbo_v2_5',
2122 voiceMap: {},
2223 };
2324
@@ -28,16 +29,23 @@ class ElevenLabsTtsProvider {
2829 <input id="elevenlabs_tts_api_key" type="text" class="text_pole" placeholder="<API Key>"/>
2930 <label for="elevenlabs_tts_model">Model</label>
3031 <select id="elevenlabs_tts_model" class="text_pole">
3132 <option value="eleven_monolingual_v1eleven_v3">EnglishEleven v1v3</option>
3233 <option value="eleven_multilingual_v1eleven_ttv_v3">MultilingualEleven v1ttv v3</option>
3334 <option value="eleven_multilingual_v2">Multilingual v2</option>
3435 <option value="eleven_turbo_v2eleven_flash_v2_5">TurboEleven 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>
3541 </select>
3642 <input id="eleven_labs_connect" class="menu_button" type="button" value="Connect" />
3743 <label for="elevenlabs_tts_stability">Stability: <span id="elevenlabs_tts_stability_output"></span></label>
3844 <input id="elevenlabs_tts_stability" type="range" value="${this.defaultSettings.stability}" min="0" max="1" step="0.01" />
3945 <label for="elevenlabs_tts_similarity_boost">Similarity Boost: <span id="elevenlabs_tts_similarity_boost_output"></span></label>
4046 <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" />
4149 <div id="elevenlabs_tts_v2_options" style="display: none;">
4250 <label for="elevenlabs_tts_style_exaggeration">Style Exaggeration: <span id="elevenlabs_tts_style_exaggeration_output"></span></label>
4351 <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 {
6573 }
6674
6775 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);
6984 }
7085
7186 onSettingsChange() {
@@ -74,10 +89,12 @@ class ElevenLabsTtsProvider {
7489 this.settings.similarity_boost = $('#elevenlabs_tts_similarity_boost').val();
7590 this.settings.style_exaggeration = $('#elevenlabs_tts_style_exaggeration').val();
7691 this.settings.speaker_boost = $('#elevenlabs_tts_speaker_boost').is(':checked');
92+ this.settings.speed = $('#elevenlabs_tts_speed').val();
7793 this.settings.model = $('#elevenlabs_tts_model').find(':selected').val();
7894 $('#elevenlabs_tts_stability_output').text(Math.round(this.settings.stability * 100) + '%');
7995 $('#elevenlabs_tts_similarity_boost_output').text(Math.round(this.settings.similarity_boost * 100) + '%');
8096 $('#elevenlabs_tts_style_exaggeration_output').text(Math.round(this.settings.style_exaggeration * 100) + '%');
97+ $('#elevenlabs_tts_speed_output').text(this.settings.speed + 'x');
8198 $('#elevenlabs_tts_v2_options').toggle(this.shouldInvolveExtendedSettings());
8299 saveTtsProviderSettings();
83100 }
@@ -109,6 +126,7 @@ class ElevenLabsTtsProvider {
109126 $('#elevenlabs_tts_similarity_boost').val(this.settings.similarity_boost);
110127 $('#elevenlabs_tts_style_exaggeration').val(this.settings.style_exaggeration);
111128 $('#elevenlabs_tts_speaker_boost').prop('checked', this.settings.speaker_boost);
129+ $('#elevenlabs_tts_speed').val(this.settings.speed);
112130 $('#elevenlabs_tts_api_key').val(this.settings.apiKey);
113131 $('#elevenlabs_tts_model').val(this.settings.model);
114132 $('#eleven_labs_connect').on('click', () => { this.onConnectClick(); });
@@ -116,10 +134,12 @@ class ElevenLabsTtsProvider {
116134 $('#elevenlabs_tts_stability').on('input', this.onSettingsChange.bind(this));
117135 $('#elevenlabs_tts_style_exaggeration').on('input', this.onSettingsChange.bind(this));
118136 $('#elevenlabs_tts_speaker_boost').on('change', this.onSettingsChange.bind(this));
137+ $('#elevenlabs_tts_speed').on('input', this.onSettingsChange.bind(this));
119138 $('#elevenlabs_tts_model').on('change', this.onSettingsChange.bind(this));
120139 $('#elevenlabs_tts_stability_output').text(Math.round(this.settings.stability * 100) + '%');
121140 $('#elevenlabs_tts_similarity_boost_output').text(Math.round(this.settings.similarity_boost * 100) + '%');
122141 $('#elevenlabs_tts_style_exaggeration_output').text(Math.round(this.settings.style_exaggeration * 100) + '%');
142+ $('#elevenlabs_tts_speed_output').text(this.settings.speed + 'x');
123143 $('#elevenlabs_tts_v2_options').toggle(this.shouldInvolveExtendedSettings());
124144 try {
125145 await this.checkReady();
@@ -308,6 +328,7 @@ class ElevenLabsTtsProvider {
308328 voice_settings: {
309329 stability: Number(this.settings.stability),
310330 similarity_boost: Number(this.settings.similarity_boost),
331+ speed: Number(this.settings.speed),
311332 },
312333 };
313334 if (this.shouldInvolveExtendedSettings()) {
public/scripts/extensions/tts/index.js+3 -1
@@ -31,6 +31,7 @@ import { GoogleNativeTtsProvider } from './google-native.js';
3131import { ChatterboxTtsProvider } from './chatterbox.js';
3232import { KokoroTtsProvider } from './kokoro.js';
3333import { TtsWebuiProvider } from './tts-webui.js';
34+import { PollinationsTtsProvider } from './pollinations.js';
3435
3536const UPDATE_INTERVAL = 1000;
3637const wrapper = new ModuleWorkerWrapper(moduleWorker);
@@ -129,6 +130,7 @@ const ttsProviders = {
129130 Novel: NovelTtsProvider,
130131 OpenAI: OpenAITtsProvider,
131132 'OpenAI Compatible': OpenAICompatibleTtsProvider,
133+ Pollinations: PollinationsTtsProvider,
132134 SBVits2: SBVits2TtsProvider,
133135 Silero: SileroTtsProvider,
134136 SpeechT5: SpeechT5TtsProvider,
@@ -866,7 +868,7 @@ async function onMessageEvent(messageId, lastCharIndex) {
866868
867869 console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`);
868870
869871 if (extension_settings.tts.periodic_auto_generation && isStreamingEnabled()) {
870872 ttsJobQueue.push(message);
871873 } else {
872874 processAndQueueTtsMessage(message);
public/scripts/extensions/tts/novel.js+17 -11
@@ -132,9 +132,14 @@ class NovelTtsProvider {
132132 return { name: voiceName, voice_id: voiceName, lang: 'en-US', preview_url: false };
133133 }
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);
138143 }
139144
140145 //###########//
@@ -172,16 +177,17 @@ class NovelTtsProvider {
172177 this.audioElement.currentTime = 0;
173178
174179 const text = getPreviewString('en-US');
175180 for await (const response = awaitof this.fetchTtsGenerationgenerateTts(text, id);) {
176- if (!response.ok) {
177- throw new Error(`HTTP ${response.status}`);
178- }
179-
180181 const audio = await response.blob();
181182 const url = URL.createObjectURL(audio);
182- this.audioElement.src = url;
183+ await new Promise(resolve => {
183- this.audioElement.play();
184+ const audioElement = new Audio();
184- this.audioElement.onended = () => URL.revokeObjectURL(url);
185+ audioElement.src = url;
186+ audioElement.play();
187+ audioElement.onended = () => resolve();
188+ });
189+ URL.revokeObjectURL(url);
190+ }
185191 }
186192
187193 async* fetchTtsGeneration(inputText, voiceId) {
public/scripts/extensions/tts/openai-compatible.js+17 -22
@@ -1,6 +1,5 @@
11import { event_types, eventSource, getRequestHeaders } from '../../../script.js';
22import { callGenericPopup, POPUP_RESULTSECRET_KEYS, POPUP_TYPEsecret_state } from '../../popupsecrets.js';
3-import { findSecret, SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
43import { getPreviewString, saveTtsProviderSettings } from './index.js';
54
65export { OpenAICompatibleTtsProvider };
@@ -27,7 +26,7 @@ class OpenAICompatibleTtsProvider {
2726 <div class="flex1">
2827 <input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="500" value="${this.defaultSettings.provider_endpoint}"/>
2928 </div>
3029 <div id="openai_compatible_tts_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_custom_openai_tts">
3130 <i class="fa-solid fa-key"></i>
3231 <span>API Key</span>
3332 </div>
@@ -41,6 +40,20 @@ class OpenAICompatibleTtsProvider {
4140 return html;
4241 }
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+
4457 async loadSettings(settings) {
4558 // Populate Provider UI given input settings
4659 if (Object.keys(settings).length == 0) {
@@ -74,34 +87,9 @@ class OpenAICompatibleTtsProvider {
7487
7588 $('#openai_compatible_tts_speed_output').text(this.settings.speed);
7689
77- $('#openai_compatible_tts_key').toggleClass('success', secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS]);
78- $('#openai_compatible_tts_key').on('click', async () => {
79- const popupText = 'OpenAI-compatible TTS API Key';
80- const savedKey = secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS] ? await findSecret(SECRET_KEYS.CUSTOM_OPENAI_TTS) : '';
81-
82- const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedKey, {
83- customButtons: [{
84- text: 'Remove Key',
85- appendAtEnd: true,
86- result: POPUP_RESULT.NEGATIVE,
87- action: async () => {
88- await writeSecret(SECRET_KEYS.CUSTOM_OPENAI_TTS, '');
8990 $('#openai_compatible_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS]);
90- toastr.success('API Key removed');
91- await this.onRefreshClick();
92- },
93- }],
94- });
95-
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