Merge branch 'SillyTavern:staging' into staging

17fb9815b3e3bc27f8000ce5da622d5744ae7188

Yokayo <52032299+Yokayo@users.noreply.github.com>

Signed
28 files changed, +1795 -146Showing whitespace changes
.github/readme-de_de.md+383 -0
@@ -0,0 +1,383 @@
1> [!IMPORTANT]
2> Die hier veröffentlichten Informationen sind möglicherweise veraltet oder unvollständig. Für aktuelle Informationen nutzen Sie bitte die englische Version.
3> Letztes Update dieser README: 28.9.2024
4
5<a name="readme-top"></a>
6
7![][cover]
8
9<div align="center">
10
11[English](readme.md) | German | [中文](readme-zh_cn.md) | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)
12
13[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
14[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)
15[![GitHub Issues](https://img.shields.io/github/issues/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/issues)
16[![GitHub Pull Requests](https://img.shields.io/github/issues-pr/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/pulls)
17
18</div>
19
20---
21
22SillyTavern bietet eine einheitliche Benutzeroberfläche für viele LLM-APIs (KoboldAI/CPP, Horde, NovelAI, Ooba, Tabby, OpenAI, OpenRouter, Claude, Mistral und mehr), ein mobilfreundliches Layout, einen Visual-Novel-Modus, die Integration von Automatic1111 & ComfyUI API zur Bilderzeugung, TTS, WorldInfo (Lorebooks), anpassbare UI, automatische Übersetzung, mehr Eingabeaufforderungsoptionen, als du jemals wolltest oder brauchst, und unendliches Wachstumspotenzial durch Drittanbietererweiterungen.
23
24Wir haben eine [Dokumentationswebsite](https://docs.sillytavern.app/), um die meisten deiner Fragen zu beantworten und dir den Einstieg zu erleichtern.
25
26## Was ist SillyTavern?
27
28SillyTavern (oder ST abgekürtz) ist eine lokal installierte Benutzeroberfläche, die es dir ermöglicht, mit Textgenerations-LLMs, Bildgenerierungsmaschinen und TTS-Sprachmodellen zu interagieren.
29
30Angefangen im Februar 2023 als Fork von TavernAI 1.2.8 hat SillyTavern nun über 100 Mitwirkende und 2 Jahre unabhängiger Entwicklung hinter sich und dient weiterhin als führende Software für versierte KI-Hobbyisten.
31
32## Unsere Vision
33
341. Wir möchten die Nutzer mit so viel Nutzen und Kontrolle über ihre LLM-Prompts wie möglich ausstatten. Die steile Lernkurve ist Teil des Spaßes!
352. Wir bieten weder Online- oder gehosteten Dienste an, noch verfolgen wir programmgesteuert Benutzerdaten.
363. SillyTavern ist ein Herzensprojekt, das von einer engagierten Community von LLM-Enthusiasten unterstützt wird, und wird immer kostenlos und Open Source sein.
37
38## Branches
39
40SillyTavern wird mit einem Branchsystem entwickelt, um ein reibungsloses Erlebnis für alle Nutzer zu gewährleisten.
41
42* `release` -🌟 **Empfohlen für die meisten Nutzer.** Dies ist der stabilste und empfohlene Branch, der nur aktualisiert wird, wenn wichtige Versionen veröffentlicht werden. Er ist für die Mehrheit der Nutzer geeignet. Typischerweise einmal im Monat aktualisiert.
43* `staging` - ⚠️ **Nicht für den gelegentlichen Gebrauch empfohlen.** Dieser Branch enthält die neuesten Funktionen, kann jedoch jederzeit instabil sein. Nur für Power-User und Enthusiasten. Mehrmals täglich aktualisiert.
44
45Wenn du nicht vertraut mit der Verwendung der git CLI bist oder nicht verstehst, was ein Branch ist, mach dir keine Sorgen! Der Release-Branch ist immer die bevorzugte Option für dich.
46
47## Was brauche ich zusätzlich zu SillyTavern?
48
49Da SillyTavern nur eine Benutzeroberfläche ist, benötigst du Zugriff auf ein LLM-Backend, um Inferenz bereitzustellen. Du kannst AI Horde für sofortiges Chatten ohne weitere Einrichtung verwenden. Darüber hinaus unterstützen wir viele andere lokale und cloudbasierte LLM-Backends: OpenAI-kompatible API, KoboldAI, Tabby und viele mehr. Du kannst mehr über unsere unterstützten APIs in [der FAQ](https://docs.sillytavern.app/usage/api-connections/) lesen.
50
51### Brauche ich einen leistungsstarken PC, um SillyTavern auszuführen?
52
53Die Hardwareanforderungen sind minimal: Es läuft auf allem, was NodeJS 18 oder höher ausführen kann. Wenn du LLM-Inferenz auf deinem lokalen Rechner durchführen möchtest, empfehlen wir eine NVIDIA-Grafikkarte der 3000er-Serie mit mindestens 6 GB VRAM. Überprüfe die Dokumentation deines Backends für weitere Einzelheiten.
54
55### Vorgeschlagene Backends (keine Partnerschaft oder Werbebeziehung)
56
57* [AI Horde](https://aihorde.net/) - verwende Modelle, die von Freiwilligen gehostet werden. Erfordert keine weitere Einrichtung
58* [KoboldCpp](https://github.com/LostRuins/koboldcpp) - ein Favorit der Community, um GGUF-Modelle lokal auszuführen
59* [tabbyAPI](https://github.com/theroyallab/tabbyAPI) - eine beliebte, portable, speicherplatzoptimierte und lokal gehostete exl2 Inferenz-API
60* [OpenRouter](https://openrouter.ai) - eine einzige API für viele Cloud-Anbieter (OpenAI, Claude, Meta Llama usw.) sowie beliebte Community-Modelle.
61
62## Fragen oder Vorschläge?
63
64### Discord-Server
65
66| [![][discord-shield-badge]][discord-link] | [Tritt unserer Discord-Community bei!](https://discord.gg/sillytavern) Erhalte Unterstützung, teile deine Lieblingscharaktere und Prompts. |
67| :---------------------------------------- | :----------------------------------------------------------------------------------------------------------------- |
68
69Oder nimm direkt Kontakt mit den Entwicklern auf:
70
71* Discord: cohee, rossascends, wolfsblvt
72* 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/)
73* [Erstelle ein GitHub-Issue](https://github.com/SillyTavern/SillyTavern/issues)
74
75### Ich mag dieses Projekt! Wie kann ich beitragen?
76
771. Sende Pull-Requests. Lerne, wie du beitragen kannst: [CONTRIBUTING.md](../CONTRIBUTING.md)
782. Sende Feature Requests und Issues unter Verwendung der bereitgestellten Vorlagen.
793. Lies diese gesamte README-Datei und überprüfe zuerst die Dokumentationswebsite, um doppelte Issues zu vermeiden.
80
81## Screenshots
82
83<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">
84<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">
85
86## Charakterkarten
87
88SillyTavern basiert auf dem Konzept der "Charakterkarten". Eine Charakterkarte ist eine Sammlung von Prompts, die das Verhalten des LLM festlegen und erforderlich sind, um persistente Gespräche in SillyTavern zu führen. Sie funktionieren ähnlich wie ChatGPT's GPTs oder Poe's Bots. Der Inhalt einer Charakterkarte kann alles sein: ein abstraktes Szenario, ein Assistent, der für eine bestimmte Aufgabe maßgeschneidert ist, eine berühmte Persönlichkeit oder ein fiktiver Charakter.
89
90Das Namensfeld ist der einzige erforderliche Eingabewert für die Charakterkarte. Um ein neutrales Gespräch mit dem LLM zu beginnen, erstelle eine neue Karte, die einfach "Assistent" genannt wird, und lasse die restlichen Felder leer. Für einen thematischeren Chat kannst du dem LLM verschiedene Hintergrundinformationen, Verhaltensweisen und Schreibmuster sowie ein Szenario geben, um das Gespräch zu beginnen.
91
92Um ein schnelles Gespräch zu führen, ohne eine Charakterkarte auszuwählen, oder um einfach die LLM-Verbindung zu testen, gib einfach dein Prompt in die Eingabezeile auf dem Willkommensbildschirm ein, nachdem du SillyTavern geöffnet hast. Bitte beachte, dass solche Chats vorübergehend sind und nicht gespeichert werden.
93
94Um eine allgemeine Vorstellung davon zu bekommen, wie man Charakterkarten definiert, sieh dir die mitgelieferte Charakterkarte (Seraphina) an oder lade ausgewählte von der Community erstellte Karten im Menü "Erweiterungen & Assets herunterladen" herunter.
95
96## Wichtigste Features
97
98* Erweiterte Text-Generierungs-Einstellungen mit vielen von der Community erstellten mitgelieferten Einstellungen (Presets)
99* Unterstützung für World Info: Erstelle reichhaltige Lore oder reduziere die Tokens in deiner Charakterkarte
100* Gruppenchats: Multi-Bot-Räume für Charaktere, die mit dir und/oder untereinander sprechen
101* Reichhaltige UI-Anpassungsoptionen: Themes zur Farbenwahl, Hintergrundbilder, benutzerdefiniertes CSS und mehr
102* Benutzer-Personas: Lass die KI ein wenig über dich wissen, um die Immersion zu erhöhen
103* Eingebaute RAG-Unterstützung: Füge Dokumente zu deinen Chats hinzu, auf die die KI verweisen kann
104* Umfangreiches "Chat-Befehle"-System und eigene [Scripting-Engine](https://docs.sillytavern.app/usage/st-script/)
105
106## Erweiterungen
107
108SillyTavern unterstützt Erweiterungen.
109
110* Emotionale Ausdrucksformen von Charakteren (Sprites)
111* Automatische Zusammenfassung des Chatverlaufs
112* Automatische UI- und Chat-Übersetzung
113* Bildgenerierung mit Stable Diffusion/FLUX/DALL-E
114* Text-to-Speech für KI-Antwortnachrichten (über ElevenLabs, Silero oder die TTS-Funktion des Betriebssystems)
115* Websuchfunktionen zum Hinzufügen zusätzlicher realer Kontexte zu deinen Eingabeaufforderungen
116* Viele weitere sind im Menü "Erweiterungen & Assets herunterladen" verfügbar.
117
118Tutorials zur Nutzung findest du in der [Dokumentation](https://docs.sillytavern.app/).
119
120# ⌛ Installation
121
122> \[!WARNING]
123>
124> * INSTALLIERE NICHT IN EINEM VON WINDOWS KONTROLLIERTEN ORDNER (Programme, System32 usw.).
125> * FÜHRE START.BAT NICHT MIT ADMIN-BERECHTIGUNGEN AUS.
126> * DIE INSTALLATION AUF WINDOWS 7 IST UNMÖGLICH, DA ES NODEJS 18.16 NICHT AUSFÜHREN KANN.
127
128## 🪟 Windows
129
130### Installation über Git
131
1321. Installiere [NodeJS](https://nodejs.org/en) (die neueste LTS-Version wird empfohlen).
1332. Installiere [Git für Windows](https://gitforwindows.org/).
1343. Öffne den Windows-Explorer (`Win+E`).
1354. Gehe zu oder erstelle einen Ordner, der nicht von Windows kontrolliert oder überwacht wird. (z.B.: C:\MySpecialFolder\)
1365. Öffne ein Eingabeaufforderungsfenster in diesem Ordner, indem du in die 'Adressleiste' oben klickst, `cmd` eingibst und Enter drückst.
1376. Sobald das schwarze Fenster (Eingabeaufforderung) erscheint, gib EINE der folgenden Optionen ein und drücke Enter:
138
139* für den Release-Branch: `git clone https://github.com/SillyTavern/SillyTavern -b release`
140* für den Staging-Branch: `git clone https://github.com/SillyTavern/SillyTavern -b staging`
141
1427. Sobald alles geklont ist, doppelklicke auf `Start.bat`, damit NodeJS seine Anforderungen installiert.
1438. Der Server wird dann gestartet, und SillyTavern wird in deinem Browser geöffnet.
144
145### Installation über GitHub Desktop
146
147(Dies ermöglicht die Nutzung von git **nur** in GitHub Desktop. Wenn du `git` auch in der Eingabeaufforderung verwenden möchtest, musst du auch [Git für Windows](https://gitforwindows.org/) installieren.)
148
1491. Installiere [NodeJS](https://nodejs.org/en) (die neueste LTS-Version wird empfohlen).
1502. Installiere [GitHub Desktop](https://central.github.com/deployments/desktop/desktop/latest/win32).
1513. Klicke nach der Installation von GitHub Desktop auf `Ein Repository aus dem Internet klonen....` (Hinweis: Du **musst kein** GitHub-Konto für diesen Schritt erstellen).
1524. Klicke im Menü auf den Tab URL, gib diese URL ein `https://github.com/SillyTavern/SillyTavern` und klicke auf Klonen. Du kannst den lokalen Pfad ändern, um festzulegen, wohin SillyTavern heruntergeladen werden soll.
1535. Um SillyTavern zu öffnen, durchsuche mit dem Windows-Explorer den Ordner, in den du das Repository geklont hast. Standardmäßig wird das Repository hier hin geklont: `C:\Users\[Dein Windows-Benutzername]\Documents\GitHub\SillyTavern`.
1546. Doppelklicke auf die Datei `start.bat`. (Hinweis: Der Teil `.bat` des Dateinamens könnte von deinem Betriebssystem verborgen sein. In diesem Fall sieht es aus wie eine Datei namens "`Start`". Dies ist die Datei, auf die du doppelklickst, um SillyTavern auszuführen.)
1557. Nach dem Doppelklicken sollte ein großes schwarzes Konsolenfenster erscheinen, und SillyTavern beginnt, das zu installieren, was es zum Betrieb benötigt.
1568. Nach dem Installationsprozess sollte das Konsolenfenster so aussehen, und ein SillyTavern-Tab sollte in deinem Browser geöffnet sein.
1579. Verbinde dich mit einer der [unterstützten APIs](https://docs.sillytavern.app/usage/api-connections/) und beginne zu chatten!
158
159## 🐧 Linux & 🍎 MacOS
160
161Für MacOS/Linux werden all diese Schritte in einem Terminal durchgeführt.
162
1631. Installiere git und nodeJS (die Methode zur Durchführung hängt von deinem Betriebssystem ab).
1642. Klone das Repository.
165
166* für den Release-Branch: `git clone https://github.com/SillyTavern/SillyTavern -b release`
167* für den Staging-Branch: `git clone https://github.com/SillyTavern/SillyTavern -b staging`
168
1693. `cd SillyTavern`, um in den Installationsordner zu navigieren.
1704. Führe das Skript `start.sh` mit einem dieser Befehle aus:
171
172* `./start.sh`
173* `bash start.sh`
174
175## ⚡ Installation über SillyTavern Launcher
176
177Der SillyTavern Launcher ist ein Installationsassistent, der dir bei der Einrichtung mit vielen Optionen helfen wird, einschließlich der Installation eines Backends für lokale Inferenz.
178
179### Für Windows-Nutzer
180
1811. Drücke auf deiner Tastatur **`WINDOWS + R`**, um das Ausführen-Dialogfeld zu öffnen. Führe dann den folgenden Befehl aus, um git zu installieren:
182
183```shell
184cmd /c winget install -e --id Git.Git
185```
186
1872. Drücke auf deiner Tastatur **`WINDOWS + E`**, um den Datei-Explorer zu öffnen, und navigiere dann zu dem Ordner, in dem du den Launcher installieren möchtest. Gib im gewünschten Ordner in die Adressleiste `cmd` ein und drücke Enter. Führe dann den folgenden Befehl aus:
188
189```shell
190git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher && start installer.bat
191```
192
193### Für Linux-Nutzer
194
1951. Öffne dein bevorzugtes Terminal und installiere git.
1962. Klone den SillyTavern-Launcher mit:
197
198```shell
199git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
200```
201
2023. Starte die installer.sh mit:
203
204```shell
205chmod +x install.sh && ./install.sh
206```
207
2084. Nach der Installation starte die launcher.sh mit:
209
210```shell
211chmod +x launcher.sh && ./launcher.sh
212```
213
214### Für Mac-Nutzer
215
2161. Öffne ein Terminal und installiere brew mit:
217
218```shell
219/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
220```
221
2222. Installiere git mit:
223
224```shell
225brew install git
226```
227
2283. Klone den SillyTavern-Launcher mit:
229
230```shell
231git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
232```
233
2344. Starte die installer.sh mit:
235
236```shell
237chmod +x install.sh && ./install.sh
238```
239
2405. Nach der Installation starte die launcher.sh mit:
241
242```shell
243chmod +x launcher.sh && ./launcher.sh
244```
245
246## 🐋 Installation über Docker
247
248Diese Anweisungen setzen voraus, dass du Docker installiert hast, auf deine Befehlszeile für die Installation von Containern zugreifen kannst und mit deren allgemeiner Funktionsweise vertraut bist.
249
250### Image selbst bauen
251
252Wir haben einen umfassenden Leitfaden zur Nutzung von SillyTavern in Docker [hier](http://docs.sillytavern.app/installation/docker/) der die Installationen auf Windows, macOS und Linux abdeckt! Lies ihn, wenn du das Image selbst bauen möchtest.
253
254### Verwendung der GitHub Container Registry (am einfachsten)
255
256Du benötigst zwei zwingende Verzeichniszuordnungen und eine Portzuordnung, um SillyTavern funktionsfähig zu machen. Ersetze in dem Befehl deine Auswahl an den folgenden Stellen:
257
258#### Container-Variablen
259
260##### Volumen-Zuordnungen
261
262* [config] - Das Verzeichnis, in dem die SillyTavern-Konfigurationsdateien auf deinem Host-Computer gespeichert werden
263* [data] - Das Verzeichnis, in dem die Benutzerdaten von SillyTavern (einschließlich Charaktere) auf deinem Host-Computer gespeichert werden
264* [plugins] - (optional) Das Verzeichnis, in dem die SillyTavern-Server-Plugins auf deinem Host-Computer gespeichert werden
265
266##### Port-Zuordnungen
267
268* [PublicPort] - Der Port, über den der Datenverkehr ausgegeben werden soll. Dies ist zwingend erforderlich, da du auf die Instanz von außerhalb des virtuellen Maschinencontainers zugreifst. EXPOSIERE DIES NICHT IM INTERNET, OHNE EINEN GETRENNTEN SERVICE FÜR DIE SICHERHEIT ZU IMPLEMENTIEREN.
269
270##### Zusätzliche Einstellungen
271
272* [DockerNet] - Das Docker-Netzwerk, mit dem der Container erstellt werden soll. Wenn du nicht weißt, was das ist, sieh dir die [offizielle Docker-Dokumentation](https://docs.docker.com/reference/cli/docker/network/) an.
273* [version] - Auf der rechten Seite dieser GitHub-Seite siehst du "Packages". Wähle das Paket "sillytavern" und du siehst die Imageversionen. Das Image-Tag "latest" hält dich auf dem Laufenden mit dem aktuellen Release. Du kannst auch "staging" und "release" Tags nutzen, die auf die nightly images der jeweiligen Branche verweisen, aber das könnte unangemessen sein, wenn du Erweiterungen verwendest, die möglicherweise kaputt sind und Zeit benötigen, um aktualisiert zu werden.
274
275#### Installationsbefehl
276
2771. Öffne deine Befehlszeile.
2782. Führe den folgenden Befehl aus:
279
280`docker create --name='sillytavern' --net='[DockerNet]' -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'`
281
282> Beachte, dass 8000 ein Standard-Listening-Port ist. Vergiss nicht, einen geeigneten Port zu verwenden, wenn du ihn in der Konfiguration änderst.
283
284## 📱 Installation über Termux auf Android OS
285
286> \[!HINWEIS]
287> **SillyTavern kann nativ auf Android-Geräten über Termux ausgeführt werden, aber wir bieten keine offizielle Unterstützung für diesen Anwendungsfall.**
288>
289> **Bitte beziehe dich auf diesen Leitfaden von ArroganceComplex#2659:**
290>
291> * <https://rentry.org/STAI-Termux>
292
293**Nicht unterstützte Plattform: android arm LEtime-web.** 32-Bit-Android benötigt eine externe Abhängigkeit, die nicht mit npm installiert werden kann. Verwende den folgenden Befehl, um sie zu installieren: `pkg install esbuild`. Führe dann die üblichen Installationsschritte aus.
294
295## API-Schlüsselverwaltung
296
297SillyTavern speichert deine API-Schlüssel in einer Datei `secrets.json` im Benutzerdatenverzeichnis (`/data/default-user/secrets.json` ist der Standardpfad).
298
299Standardmäßig sind API-Schlüssel nach dem Speichern und Aktualisieren der Seite nicht mehr über die Benutzeroberfläche sichtbar.
300
301Um die Ansicht deiner Schlüssel zu aktivieren:
302
3031. Setze den Wert von `allowKeysExposure` auf `true` in der Datei `config.yaml`.
3042. Starte den SillyTavern-Server neu.
3053. Klicke auf den Link 'Verborgene API-Schlüssel anzeigen' in der unteren rechten Ecke des API-Verbindungsfeldes.
306
307## Befehlszeilenargumente
308
309Sie können Befehlszeilenargumente an den Start des SillyTavern-Servers übergeben, um einige Einstellungen in `config.yaml` zu überschreiben.
310
311### Beispiele
312
313```shell
314node server.js --port 8000 --listen false
315# oder
316npm run start -- --port 8000 --listen false
317# oder (nur Windows)
318Start.bat --port 8000 --listen false
319```
320
321### Unterstützte Argumente
322
323| Option | Beschreibung | Typ |
324|-------------------------|----------------------------------------------------------------------------------------------------|----------|
325| `--version` | Versionsnummer anzeigen | boolean |
326| `--enableIPv6` | Aktiviert IPv6. | boolean |
327| `--enableIPv4` | Aktiviert IPv4. | boolean |
328| `--port` | Legt den Port fest, unter dem SillyTavern ausgeführt wird. Wenn nicht angegeben, wird auf YAML-Konfiguration „Port“ zurückgegriffen. | number |
329| „--dnsPreferIPv6“ | Bevorzugt IPv6 für DNS. Wenn nicht angegeben, wird auf YAML-Konfiguration „preferIPv6“ zurückgegriffen. | boolean |
330| „--autorun“ | Startet SillyTavern automatisch im Browser. Wenn nicht angegeben, wird auf YAML-Konfiguration „autorun“ zurückgegriffen.| boolean |
331| „--autorunHostname“ | Der Autorun-Hostname, am besten auf „auto“ belassen. | string |
332| „--autorunPortOverride“ | Überschreibt den Port für Autorun. | string |
333| „--listen“ | SillyTavern lauscht auf allen Netzwerkschnittstellen. Wenn nicht angegeben, wird auf YAML-Konfiguration „listen“ zurückgegriffen.| boolean |
334| „--corsProxy“ | Aktiviert CORS-Proxy. Wenn nicht angegeben, wird auf YAML-Konfiguration „enableCorsProxy“ zurückgegriffen. | boolean |
335| `--disableCsrf` | Deaktiviert CSRF-Schutz | boolean |
336| `--ssl` | Aktiviert SSL | boolean |
337| `--certPath` | Pfad zu Ihrer Zertifikatsdatei. | string |
338| `--keyPath` | Pfad zu Ihrer privaten Schlüsseldatei. | string |
339| `--whitelist` | Aktiviert den Whitelist-Modus | boolean |
340| `--dataRoot` | Stammverzeichnis für Datenspeicherung | string |
341| `--avoidLocalhost` | Vermeidet die Verwendung von „localhost“ für Autorun im Auto-Modus. | boolean |
342| `--basicAuthMode` | Aktiviert die grundlegende Authentifizierung | boolean |
343| `--requestProxyEnabled` | Aktiviert die Verwendung eines Proxys für ausgehende Anfragen | boolean |
344| `--requestProxyUrl` | Proxy-URL anfordern (HTTP- oder SOCKS-Protokolle) | string |
345| `--requestProxyBypass` | Proxy-Bypass-Liste anfordern (durch Leerzeichen getrennte Liste von Hosts) | Array |
346
347## Remoteverbindungen
348
349Dies ist in den meisten Fällen für Personen gedacht, die SillyTavern auf ihren Mobiltelefonen verwenden möchten, während ihr PC den ST-Server im selben WLAN-Netzwerk betreibt. Es kann jedoch auch verwendet werden, um Remoteverbindungen von überall her zu ermöglichen.
350
351Lies die ausführliche Anleitung zum Einrichten von Remoteverbindungen in den [Docs](https://docs.sillytavern.app/usage/remoteconnections/).
352
353Möglicherweise möchtest du SillyTavern-Benutzerprofile auch mit (optionalem) Kennwortschutz konfigurieren: [Benutzer](https://docs.sillytavern.app/installation/st-1.12.0-migration-guide/#users).
354
355## Leistungsprobleme?
356
3571. Deaktiviere den Unschärfeeffekt und aktiviere "Verringerte Bewegung" im Bedienfeld "Benutzereinstellungen" (UI-Design schaltet Kategorie um).
3582. Wenn du Response Streaming verwendest, stelle die Streaming-FPS auf einen niedrigeren Wert ein (10-15 FPS werden empfohlen).
3593. Stelle sicher, dass der Browser die GPU-Beschleunigung zum Rendern verwenden kann.
360
361## Lizenz und Danksagungen
362
363**Dieses Programm wird in der Hoffnung verbreitet, dass es nützlich ist,
364aber OHNE JEGLICHE GARANTIE; nicht einmal die stillschweigende Garantie der
365MARKTFÄHIGKEIT oder EIGNUNG FÜR EINEN BESTIMMTEN ZWECK. Siehe die
366GNU Affero General Public License für weitere Details.**
367
368* [TavernAI](https://github.com/TavernAI/TavernAI) 1.2.8 von Humi: MIT-Lizenz
369* Teile von CncAnons TavernAITurbo-Mod werden mit Genehmigung verwendet
370* Visual Novel-Modus inspiriert von der Arbeit von PepperTaco (<https://github.com/peppertaco/Tavern/>)
371* Noto Sans-Schriftart von Google (OFL-Lizenz)
372* Symboldesign von Font Awesome <https://fontawesome.com> (Symbole: CC BY 4.0, Schriftarten: SIL OFL 1.1, Code: MIT-Lizenz)
373* Standardinhalt von @OtisAlejandro (Seraphina-Charakter und Lorebook) und @kallmeflocc (10.000 Discord-Benutzer-Feierhintergrund)
374* Docker-Anleitung von [@mrguymiah](https://github.com/mrguymiah) und [@Bronya-Rand](https://github.com/Bronya-Rand)
375
376## Top Contributors
377
378[![Contributors](https://contrib.rocks/image?repo=SillyTavern/SillyTavern)](https://github.com/SillyTavern/SillyTavern/graphs/contributors)
379
380<!-- LINK GROUP -->
381[cover]: https://github.com/user-attachments/assets/01a6ae9a-16aa-45f2-8bff-32b5dc587e44
382[discord-link]: https://discord.gg/sillytavern
383[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-ja_jp.md+1 -1
@@ -5,7 +5,7 @@
55
6<div align="center">6<div align="center">
77
8[English](readme.md) | [中文](readme-zh_cn.md) | 日本語 | [Русский](readme-ru_ru.md)8[English](readme.md) | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | 日本語 | [Русский](readme-ru_ru.md)
99
10[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)10[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
11[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)11[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)
.github/readme-ru_ru.md+1 -1
@@ -7,7 +7,7 @@
77
8<div align="center">8<div align="center">
99
10[English](readme.md) | [中文](readme-zh_cn.md) | [日本語](readme-ja_jp.md) | Русский10[English](readme.md) | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | [日本語](readme-ja_jp.md) | Русский
1111
12[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)12[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
13[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)13[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)
.github/readme-zh_cn.md+1 -1
@@ -5,7 +5,7 @@
55
6<div align="center">6<div align="center">
77
8[English](readme.md) | 中文 | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)8[English](readme.md) | [German](readme-de_de.md) | 中文 | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)
99
10[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)10[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
11[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)11[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)
.github/readme.md+1 -1
@@ -4,7 +4,7 @@
44
5<div align="center">5<div align="center">
66
7English | [中文](readme-zh_cn.md) | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)7English | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)
88
9[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)9[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
10[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)10[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)
public/css/popup.css+2 -1
@@ -1,3 +1,4 @@
1@import url('/lib/dialog-polyfill.css');
1@import url('./popup-safari-fix.css');2@import url('./popup-safari-fix.css');
23
3dialog {4dialog {
@@ -42,7 +43,7 @@ dialog {
42 display: flex;43 display: flex;
43 flex-direction: column;44 flex-direction: column;
44 overflow: hidden;45 overflow: hidden;
45 width: 100%;46 width: min(100%, 100vw);
46 height: 100%;47 height: 100%;
47 padding: 1px;48 padding: 1px;
48}49}
public/index.html+36 -18
@@ -1709,9 +1709,9 @@
1709 <div class="">1709 <div class="">
1710 <div class="inline-drawer wide100p flexFlowColumn">1710 <div class="inline-drawer wide100p flexFlowColumn">
1711 <div class="inline-drawer-toggle inline-drawer-header">1711 <div class="inline-drawer-toggle inline-drawer-header">
1712 <div class="flex-container alignItemsCenter flexNoGap">1712 <div class="flex-container alignItemsCenter">
1713 <b data-i18n="Character Names Behavior">Character Names Behavior</b>1713 <b data-i18n="Character Names Behavior">Character Names Behavior</b>
1714 <span title="Helps the model to associate messages with characters." data-i18n="[title]Helps the model to associate messages with characters." class="note-link-span fa-solid fa-circle-question"></span>1714 <span title="Helps the model to associate messages with characters." data-i18n="[title]Helps the model to associate messages with characters." class="opacity50p fa-solid fa-circle-info"></span>
1715 <small class="flexBasis100p">(<span id="character_names_display"></span>)</small>1715 <small class="flexBasis100p">(<span id="character_names_display"></span>)</small>
1716 </div>1716 </div>
1717 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>1717 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
@@ -1720,25 +1720,22 @@
1720 <label class="checkbox_label flexWrap alignItemsCenter" for="character_names_none">1720 <label class="checkbox_label flexWrap alignItemsCenter" for="character_names_none">
1721 <input type="radio" id="character_names_none" name="character_names" value="-1">1721 <input type="radio" id="character_names_none" name="character_names" value="-1">
1722 <span data-i18n="None">None</span>1722 <span data-i18n="None">None</span>
1723 <i class="right_menu_button fa-solid fa-circle-exclamation" title="Never add character name prefixes. May behave poorly in groups, choose with caution." data-i18n="[title]character_names_none"></i>1723 <small class="flexBasis100p" data-i18n="character_names_none">
1724 <small class="flexBasis100p" data-i18n="Never add character names.">1724 Never add character name prefixes. May behave poorly in groups, choose with caution.
1725 Never add character names.
1726 </small>1725 </small>
1727 </label>1726 </label>
1728 <label class="checkbox_label flexWrap alignItemsCenter" for="character_names_default">1727 <label class="checkbox_label flexWrap alignItemsCenter" for="character_names_default">
1729 <input type="radio" id="character_names_default" name="character_names" value="0">1728 <input type="radio" id="character_names_default" name="character_names" value="0">
1730 <span data-i18n="Default">Default</span>1729 <span data-i18n="Default">Default</span>
1731 <i class="right_menu_button fa-solid fa-circle-exclamation" title="Add prefixes for groups and past personas. Otherwise, make sure you provide names in the prompt." data-i18n="[title]character_names_default"></i>1730 <small class="flexBasis100p" data-i18n="character_names_default">
1732 <small class="flexBasis100p" data-i18n="Don't add character names unless necessary.">1731 Add prefixes for groups and past personas. Otherwise, make sure you provide names in the prompt.
1733 Don't add character names unless necessary.
1734 </small>1732 </small>
1735 </label>1733 </label>
1736 <label class="checkbox_label flexWrap alignItemsCenter" for="character_names_completion">1734 <label class="checkbox_label flexWrap alignItemsCenter" for="character_names_completion">
1737 <input type="radio" id="character_names_completion" name="character_names" value="1">1735 <input type="radio" id="character_names_completion" name="character_names" value="1">
1738 <span data-i18n="Completion Object">Completion Object</span>1736 <span data-i18n="Completion Object">Completion Object</span>
1739 <i class="right_menu_button fa-solid fa-circle-exclamation" title="Restrictions apply: only Latin alphanumerics and underscores. Doesn't work for all sources, notably: Claude, MistralAI, Google." data-i18n="[title]character_names_completion"></i>1737 <small class="flexBasis100p" data-i18n="character_names_completion">
1740 <small class="flexBasis100p" data-i18n="Add character names to completion objects.">1738 Add character names to completion objects. Restrictions apply: only Latin alphanumerics and underscores.
1741 Add character names to completion objects.
1742 </small>1739 </small>
1743 </label>1740 </label>
1744 <label class="checkbox_label flexWrap alignItemsCenter" for="character_names_content">1741 <label class="checkbox_label flexWrap alignItemsCenter" for="character_names_content">
@@ -1754,9 +1751,9 @@
1754 </div>1751 </div>
1755 <div class="inline-drawer wide100p flexFlowColumn marginBot10">1752 <div class="inline-drawer wide100p flexFlowColumn marginBot10">
1756 <div class="inline-drawer-toggle inline-drawer-header">1753 <div class="inline-drawer-toggle inline-drawer-header">
1757 <div class="flex-container alignItemsCenter flexNoGap">1754 <div class="flex-container alignItemsCenter">
1758 <b data-i18n="Continue Postfix">Continue Postfix</b>1755 <b data-i18n="Continue Postfix">Continue Postfix</b>
1759 <span data-i18n="[title]The next chunk of the continued message will be appended using this as a separator." title="The next chunk of the continued message will be appended using this as a separator." class="note-link-span fa-solid fa-circle-question"></span>1756 <span data-i18n="[title]The next chunk of the continued message will be appended using this as a separator." title="The next chunk of the continued message will be appended using this as a separator." class="opacity50p fa-solid fa-circle-info"></span>
1760 <small class="flexBasis100p">(<span id="continue_postfix_display"></span>)</small>1757 <small class="flexBasis100p">(<span id="continue_postfix_display"></span>)</small>
1761 </div>1758 </div>
1762 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>1759 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
@@ -2891,6 +2888,7 @@
2891 <option value="gemini-1.5-pro-latest">Gemini 1.5 Pro [latest]</option>2888 <option value="gemini-1.5-pro-latest">Gemini 1.5 Pro [latest]</option>
2892 <option value="gemini-1.5-pro-001">Gemini 1.5 Pro [001]</option>2889 <option value="gemini-1.5-pro-001">Gemini 1.5 Pro [001]</option>
2893 <option value="gemini-1.5-pro-002">Gemini 1.5 Pro [002]</option>2890 <option value="gemini-1.5-pro-002">Gemini 1.5 Pro [002]</option>
2891 <option value="gemini-1.5-flash-8b">Gemini 1.5 Flash 8B</option>
2894 <option value="gemini-1.5-flash-exp-0827">Gemini 1.5 Flash Experiment 2024-08-27</option>2892 <option value="gemini-1.5-flash-exp-0827">Gemini 1.5 Flash Experiment 2024-08-27</option>
2895 <option value="gemini-1.5-flash-8b-exp-0827">Gemini 1.5 Flash 8B Experiment 2024-08-27</option>2893 <option value="gemini-1.5-flash-8b-exp-0827">Gemini 1.5 Flash 8B Experiment 2024-08-27</option>
2896 <option value="gemini-1.5-flash-8b-exp-0924">Gemini 1.5 Flash 8B Experiment 2024-09-24</option>2894 <option value="gemini-1.5-flash-8b-exp-0924">Gemini 1.5 Flash 8B Experiment 2024-09-24</option>
@@ -2957,16 +2955,31 @@
2957 </div>2955 </div>
2958 <h4 data-i18n="Groq Model">Groq Model</h4>2956 <h4 data-i18n="Groq Model">Groq Model</h4>
2959 <select id="model_groq_select">2957 <select id="model_groq_select">
2960 <option value="llama-3.1-405b-reasoning">llama-3.1-405b-reasoning</option>2958 <optgroup label="Llama 3.2">
2961 <option value="llama-3.1-70b-versatile">llama-3.1-70b-versatile</option>2959 <option value="llama-3.2-1b-preview">llama-3.2-1b-preview</option>
2960 <option value="llama-3.2-3b-preview">llama-3.2-3b-preview</option>
2961 <option value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>
2962 <option value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>
2963 </optgroup>
2964 <optgroup label="Llama 3.1">
2962 <option value="llama-3.1-8b-instant">llama-3.1-8b-instant</option>2965 <option value="llama-3.1-8b-instant">llama-3.1-8b-instant</option>
2963 <option value="llama3-groq-70b-8192-tool-use-preview">llama3-groq-70b-8192-tool-use-preview</option>2966 <option value="llama-3.1-70b-versatile">llama-3.1-70b-versatile</option>
2967 <option value="llama-3.1-405b-reasoning">llama-3.1-405b-reasoning</option>
2968 </optgroup>
2969 <optgroup label="Llama 3">
2964 <option value="llama3-groq-8b-8192-tool-use-preview">llama3-groq-8b-8192-tool-use-preview</option>2970 <option value="llama3-groq-8b-8192-tool-use-preview">llama3-groq-8b-8192-tool-use-preview</option>
2971 <option value="llama3-groq-70b-8192-tool-use-preview">llama3-groq-70b-8192-tool-use-preview</option>
2965 <option value="llama3-8b-8192">llama3-8b-8192</option>2972 <option value="llama3-8b-8192">llama3-8b-8192</option>
2966 <option value="llama3-70b-8192">llama3-70b-8192</option>2973 <option value="llama3-70b-8192">llama3-70b-8192</option>
2967 <option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>2974 </optgroup>
2975 <optgroup label="Gemma">
2968 <option value="gemma-7b-it">gemma-7b-it</option>2976 <option value="gemma-7b-it">gemma-7b-it</option>
2969 <option value="gemma2-9b-it">gemma2-9b-it</option>2977 <option value="gemma2-9b-it">gemma2-9b-it</option>
2978 </optgroup>
2979 <optgroup label="Other">
2980 <option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
2981 <option value="llava-v1.5-7b-4096-preview">llava-v1.5-7b-4096-preview</option>
2982 </optgroup>
2970 </select>2983 </select>
2971 </div>2984 </div>
2972 <div id="perplexity_form" data-source="perplexity">2985 <div id="perplexity_form" data-source="perplexity">
@@ -4013,6 +4026,10 @@
4013 <input id="compact_input_area" type="checkbox" />4026 <input id="compact_input_area" type="checkbox" />
4014 <small data-i18n="Compact Input Area (Mobile)">Compact Input Area</small><i class="fa-solid fa-mobile-screen-button"></i>4027 <small data-i18n="Compact Input Area (Mobile)">Compact Input Area</small><i class="fa-solid fa-mobile-screen-button"></i>
4015 </label>4028 </label>
4029 <label for="show_swipe_num_all_messages" class="checkbox_label" title="Display swipe numbers for all messages, not just the last." data-i18n="[title]Display swipe numbers for all messages, not just the last.">
4030 <input id="show_swipe_num_all_messages" type="checkbox" />
4031 <small data-i18n="Swipe # for All Messages">Swipe # for All Messages</small><i class="fa-solid fa-mobile-screen-button"></i>
4032 </label>
4016 <label for="hotswapEnabled" class="checkbox_label" title="In the Character Management panel, show quick selection buttons for favorited characters." data-i18n="[title]In the Character Management panel, show quick selection buttons for favorited characters">4033 <label for="hotswapEnabled" class="checkbox_label" title="In the Character Management panel, show quick selection buttons for favorited characters." data-i18n="[title]In the Character Management panel, show quick selection buttons for favorited characters">
4017 <input id="hotswapEnabled" type="checkbox" />4034 <input id="hotswapEnabled" type="checkbox" />
4018 <small data-i18n="Characters Hotswap">Characters Hotswap</small>4035 <small data-i18n="Characters Hotswap">Characters Hotswap</small>
@@ -5941,7 +5958,8 @@
5941 </div>5958 </div>
5942 <div class="mes_bias"></div>5959 <div class="mes_bias"></div>
5943 </div>5960 </div>
5944 <div class="swipe_right fa-solid fa-chevron-right" style="display: none;">5961 <div class="flex-container swipeRightBlock flexFlowColumn flexNoGap">
5962 <div class="swipe_right fa-solid fa-chevron-right" style="display: none;"></div>
5945 <div class="swipes-counter"></div>5963 <div class="swipes-counter"></div>
5946 </div>5964 </div>
5947 </div>5965 </div>
public/lib/dialog-polyfill.css+37 -0
@@ -0,0 +1,37 @@
1.poly_dialog {
2 position: absolute;
3 left: 0; right: 0;
4 width: -moz-fit-content;
5 width: -webkit-fit-content;
6 width: fit-content;
7 height: -moz-fit-content;
8 height: -webkit-fit-content;
9 height: fit-content;
10 margin: auto;
11 border: solid;
12 padding: 1em;
13 background: white;
14 color: black;
15 display: block;
16}
17
18.poly_dialog:not([open]) {
19 display: none;
20}
21
22.poly_dialog + .backdrop {
23 position: fixed;
24 top: 0; right: 0; bottom: 0; left: 0;
25 background: rgba(0,0,0,0.1);
26}
27
28._poly_dialog_overlay {
29 position: fixed;
30 top: 0; right: 0; bottom: 0; left: 0;
31}
32
33.poly_dialog.fixed {
34 position: fixed;
35 top: 50%;
36 transform: translate(0, -50%);
37}
\ No newline at end of file37 \ No newline at end of file
public/lib/dialog-polyfill.esm.js+858 -0
@@ -0,0 +1,858 @@
1// nb. This is for IE10 and lower _only_.
2var supportCustomEvent = window.CustomEvent;
3if (!supportCustomEvent || typeof supportCustomEvent === 'object') {
4 supportCustomEvent = function CustomEvent(event, x) {
5 x = x || {};
6 var ev = document.createEvent('CustomEvent');
7 ev.initCustomEvent(event, !!x.bubbles, !!x.cancelable, x.detail || null);
8 return ev;
9 };
10 supportCustomEvent.prototype = window.Event.prototype;
11}
12
13/**
14 * Dispatches the passed event to both an "on<type>" handler as well as via the
15 * normal dispatch operation. Does not bubble.
16 *
17 * @param {!EventTarget} target
18 * @param {!Event} event
19 * @return {boolean}
20 */
21function safeDispatchEvent(target, event) {
22 var check = 'on' + event.type.toLowerCase();
23 if (typeof target[check] === 'function') {
24 target[check](event);
25 }
26 return target.dispatchEvent(event);
27}
28
29/**
30 * @param {Element} el to check for stacking context
31 * @return {boolean} whether this el or its parents creates a stacking context
32 */
33function createsStackingContext(el) {
34 while (el && el !== document.body) {
35 var s = window.getComputedStyle(el);
36 var invalid = function(k, ok) {
37 return !(s[k] === undefined || s[k] === ok);
38 };
39
40 if (s.opacity < 1 ||
41 invalid('zIndex', 'auto') ||
42 invalid('transform', 'none') ||
43 invalid('mixBlendMode', 'normal') ||
44 invalid('filter', 'none') ||
45 invalid('perspective', 'none') ||
46 s['isolation'] === 'isolate' ||
47 s.position === 'fixed' ||
48 s.webkitOverflowScrolling === 'touch') {
49 return true;
50 }
51 el = el.parentElement;
52 }
53 return false;
54}
55
56/**
57 * Finds the nearest <dialog> from the passed element.
58 *
59 * @param {Element} el to search from
60 * @return {HTMLDialogElement} dialog found
61 */
62function findNearestDialog(el) {
63 while (el) {
64 if (el.localName === 'dialog') {
65 return /** @type {HTMLDialogElement} */ (el);
66 }
67 if (el.parentElement) {
68 el = el.parentElement;
69 } else if (el.parentNode) {
70 el = el.parentNode.host;
71 } else {
72 el = null;
73 }
74 }
75 return null;
76}
77
78/**
79 * Blur the specified element, as long as it's not the HTML body element.
80 * This works around an IE9/10 bug - blurring the body causes Windows to
81 * blur the whole application.
82 *
83 * @param {Element} el to blur
84 */
85function safeBlur(el) {
86 // Find the actual focused element when the active element is inside a shadow root
87 while (el && el.shadowRoot && el.shadowRoot.activeElement) {
88 el = el.shadowRoot.activeElement;
89 }
90
91 if (el && el.blur && el !== document.body) {
92 el.blur();
93 }
94}
95
96/**
97 * @param {!NodeList} nodeList to search
98 * @param {Node} node to find
99 * @return {boolean} whether node is inside nodeList
100 */
101function inNodeList(nodeList, node) {
102 for (var i = 0; i < nodeList.length; ++i) {
103 if (nodeList[i] === node) {
104 return true;
105 }
106 }
107 return false;
108}
109
110/**
111 * @param {HTMLFormElement} el to check
112 * @return {boolean} whether this form has method="dialog"
113 */
114function isFormMethodDialog(el) {
115 if (!el || !el.hasAttribute('method')) {
116 return false;
117 }
118 return el.getAttribute('method').toLowerCase() === 'dialog';
119}
120
121/**
122 * @param {!DocumentFragment|!Element} hostElement
123 * @return {?Element}
124 */
125function findFocusableElementWithin(hostElement) {
126 // Note that this is 'any focusable area'. This list is probably not exhaustive, but the
127 // alternative involves stepping through and trying to focus everything.
128 var opts = ['button', 'input', 'keygen', 'select', 'textarea'];
129 var query = opts.map(function(el) {
130 return el + ':not([disabled])';
131 });
132 // TODO(samthor): tabindex values that are not numeric are not focusable.
133 query.push('[tabindex]:not([disabled]):not([tabindex=""])'); // tabindex != "", not disabled
134 var target = hostElement.querySelector(query.join(', '));
135
136 if (!target && 'attachShadow' in Element.prototype) {
137 // If we haven't found a focusable target, see if the host element contains an element
138 // which has a shadowRoot.
139 // Recursively search for the first focusable item in shadow roots.
140 var elems = hostElement.querySelectorAll('*');
141 for (var i = 0; i < elems.length; i++) {
142 if (elems[i].tagName && elems[i].shadowRoot) {
143 target = findFocusableElementWithin(elems[i].shadowRoot);
144 if (target) {
145 break;
146 }
147 }
148 }
149 }
150 return target;
151}
152
153/**
154 * Determines if an element is attached to the DOM.
155 * @param {Element} element to check
156 * @return {boolean} whether the element is in DOM
157 */
158function isConnected(element) {
159 return element.isConnected || document.body.contains(element);
160}
161
162/**
163 * @param {!Event} event
164 * @return {?Element}
165 */
166function findFormSubmitter(event) {
167 if (event.submitter) {
168 return event.submitter;
169 }
170
171 var form = event.target;
172 if (!(form instanceof HTMLFormElement)) {
173 return null;
174 }
175
176 var submitter = dialogPolyfill.formSubmitter;
177 if (!submitter) {
178 var target = event.target;
179 var root = ('getRootNode' in target && target.getRootNode() || document);
180 submitter = root.activeElement;
181 }
182
183 if (!submitter || submitter.form !== form) {
184 return null;
185 }
186 return submitter;
187}
188
189/**
190 * @param {!Event} event
191 */
192function maybeHandleSubmit(event) {
193 if (event.defaultPrevented) {
194 return;
195 }
196 var form = /** @type {!HTMLFormElement} */ (event.target);
197
198 // We'd have a value if we clicked on an imagemap.
199 var value = dialogPolyfill.imagemapUseValue;
200 var submitter = findFormSubmitter(event);
201 if (value === null && submitter) {
202 value = submitter.value;
203 }
204
205 // There should always be a dialog as this handler is added specifically on them, but check just
206 // in case.
207 var dialog = findNearestDialog(form);
208 if (!dialog) {
209 return;
210 }
211
212 // Prefer formmethod on the button.
213 var formmethod = submitter && submitter.getAttribute('formmethod') || form.getAttribute('method');
214 if (formmethod !== 'dialog') {
215 return;
216 }
217 event.preventDefault();
218
219 if (value != null) {
220 // nb. we explicitly check against null/undefined
221 dialog.close(value);
222 } else {
223 dialog.close();
224 }
225}
226
227/**
228 * @param {!HTMLDialogElement} dialog to upgrade
229 * @constructor
230 */
231function dialogPolyfillInfo(dialog) {
232 this.dialog_ = dialog;
233 this.replacedStyleTop_ = false;
234 this.openAsModal_ = false;
235
236 // Set a11y role. Browsers that support dialog implicitly know this already.
237 if (!dialog.hasAttribute('role')) {
238 dialog.setAttribute('role', 'dialog');
239 }
240
241 dialog.show = this.show.bind(this);
242 dialog.showModal = this.showModal.bind(this);
243 dialog.close = this.close.bind(this);
244
245 dialog.addEventListener('submit', maybeHandleSubmit, false);
246
247 if (!('returnValue' in dialog)) {
248 dialog.returnValue = '';
249 }
250
251 if ('MutationObserver' in window) {
252 var mo = new MutationObserver(this.maybeHideModal.bind(this));
253 mo.observe(dialog, {attributes: true, attributeFilter: ['open']});
254 } else {
255 // IE10 and below support. Note that DOMNodeRemoved etc fire _before_ removal. They also
256 // seem to fire even if the element was removed as part of a parent removal. Use the removed
257 // events to force downgrade (useful if removed/immediately added).
258 var removed = false;
259 var cb = function() {
260 removed ? this.downgradeModal() : this.maybeHideModal();
261 removed = false;
262 }.bind(this);
263 var timeout;
264 var delayModel = function(ev) {
265 if (ev.target !== dialog) { return; } // not for a child element
266 var cand = 'DOMNodeRemoved';
267 removed |= (ev.type.substr(0, cand.length) === cand);
268 window.clearTimeout(timeout);
269 timeout = window.setTimeout(cb, 0);
270 };
271 ['DOMAttrModified', 'DOMNodeRemoved', 'DOMNodeRemovedFromDocument'].forEach(function(name) {
272 dialog.addEventListener(name, delayModel);
273 });
274 }
275 // Note that the DOM is observed inside DialogManager while any dialog
276 // is being displayed as a modal, to catch modal removal from the DOM.
277
278 Object.defineProperty(dialog, 'open', {
279 set: this.setOpen.bind(this),
280 get: dialog.hasAttribute.bind(dialog, 'open')
281 });
282
283 this.backdrop_ = document.createElement('div');
284 this.backdrop_.className = 'backdrop';
285 this.backdrop_.addEventListener('mouseup' , this.backdropMouseEvent_.bind(this));
286 this.backdrop_.addEventListener('mousedown', this.backdropMouseEvent_.bind(this));
287 this.backdrop_.addEventListener('click' , this.backdropMouseEvent_.bind(this));
288}
289
290dialogPolyfillInfo.prototype = /** @type {HTMLDialogElement.prototype} */ ({
291
292 get dialog() {
293 return this.dialog_;
294 },
295
296 /**
297 * Maybe remove this dialog from the modal top layer. This is called when
298 * a modal dialog may no longer be tenable, e.g., when the dialog is no
299 * longer open or is no longer part of the DOM.
300 */
301 maybeHideModal: function() {
302 if (this.dialog_.hasAttribute('open') && isConnected(this.dialog_)) { return; }
303 this.downgradeModal();
304 },
305
306 /**
307 * Remove this dialog from the modal top layer, leaving it as a non-modal.
308 */
309 downgradeModal: function() {
310 if (!this.openAsModal_) { return; }
311 this.openAsModal_ = false;
312 this.dialog_.style.zIndex = '';
313
314 // This won't match the native <dialog> exactly because if the user set top on a centered
315 // polyfill dialog, that top gets thrown away when the dialog is closed. Not sure it's
316 // possible to polyfill this perfectly.
317 if (this.replacedStyleTop_) {
318 this.dialog_.style.top = '';
319 this.replacedStyleTop_ = false;
320 }
321
322 // Clear the backdrop and remove from the manager.
323 this.backdrop_.parentNode && this.backdrop_.parentNode.removeChild(this.backdrop_);
324 dialogPolyfill.dm.removeDialog(this);
325 },
326
327 /**
328 * @param {boolean} value whether to open or close this dialog
329 */
330 setOpen: function(value) {
331 if (value) {
332 this.dialog_.hasAttribute('open') || this.dialog_.setAttribute('open', '');
333 } else {
334 this.dialog_.removeAttribute('open');
335 this.maybeHideModal(); // nb. redundant with MutationObserver
336 }
337 },
338
339 /**
340 * Handles mouse events ('mouseup', 'mousedown', 'click') on the fake .backdrop element, redirecting them as if
341 * they were on the dialog itself.
342 *
343 * @param {!Event} e to redirect
344 */
345 backdropMouseEvent_: function(e) {
346 if (!this.dialog_.hasAttribute('tabindex')) {
347 // Clicking on the backdrop should move the implicit cursor, even if dialog cannot be
348 // focused. Create a fake thing to focus on. If the backdrop was _before_ the dialog, this
349 // would not be needed - clicks would move the implicit cursor there.
350 var fake = document.createElement('div');
351 this.dialog_.insertBefore(fake, this.dialog_.firstChild);
352 fake.tabIndex = -1;
353 fake.focus();
354 this.dialog_.removeChild(fake);
355 } else {
356 this.dialog_.focus();
357 }
358
359 var redirectedEvent = document.createEvent('MouseEvents');
360 redirectedEvent.initMouseEvent(e.type, e.bubbles, e.cancelable, window,
361 e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey,
362 e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget);
363 this.dialog_.dispatchEvent(redirectedEvent);
364 e.stopPropagation();
365 },
366
367 /**
368 * Focuses on the first focusable element within the dialog. This will always blur the current
369 * focus, even if nothing within the dialog is found.
370 */
371 focus_: function() {
372 // Find element with `autofocus` attribute, or fall back to the first form/tabindex control.
373 var target = this.dialog_.querySelector('[autofocus]:not([disabled])');
374 if (!target && this.dialog_.tabIndex >= 0) {
375 target = this.dialog_;
376 }
377 if (!target) {
378 target = findFocusableElementWithin(this.dialog_);
379 }
380 safeBlur(document.activeElement);
381 target && target.focus();
382 },
383
384 /**
385 * Sets the zIndex for the backdrop and dialog.
386 *
387 * @param {number} dialogZ
388 * @param {number} backdropZ
389 */
390 updateZIndex: function(dialogZ, backdropZ) {
391 if (dialogZ < backdropZ) {
392 throw new Error('dialogZ should never be < backdropZ');
393 }
394 this.dialog_.style.zIndex = dialogZ;
395 this.backdrop_.style.zIndex = backdropZ;
396 },
397
398 /**
399 * Shows the dialog. If the dialog is already open, this does nothing.
400 */
401 show: function() {
402 if (!this.dialog_.open) {
403 this.setOpen(true);
404 this.focus_();
405 }
406 },
407
408 /**
409 * Show this dialog modally.
410 */
411 showModal: function() {
412 if (this.dialog_.hasAttribute('open')) {
413 throw new Error('Failed to execute \'showModal\' on dialog: The element is already open, and therefore cannot be opened modally.');
414 }
415 if (!isConnected(this.dialog_)) {
416 throw new Error('Failed to execute \'showModal\' on dialog: The element is not in a Document.');
417 }
418 if (!dialogPolyfill.dm.pushDialog(this)) {
419 throw new Error('Failed to execute \'showModal\' on dialog: There are too many open modal dialogs.');
420 }
421
422 if (createsStackingContext(this.dialog_.parentElement)) {
423 console.warn('A dialog is being shown inside a stacking context. ' +
424 'This may cause it to be unusable. For more information, see this link: ' +
425 'https://github.com/GoogleChrome/dialog-polyfill/#stacking-context');
426 }
427
428 this.setOpen(true);
429 this.openAsModal_ = true;
430
431 // Optionally center vertically, relative to the current viewport.
432 if (dialogPolyfill.needsCentering(this.dialog_)) {
433 dialogPolyfill.reposition(this.dialog_);
434 this.replacedStyleTop_ = true;
435 } else {
436 this.replacedStyleTop_ = false;
437 }
438
439 // Insert backdrop.
440 this.dialog_.parentNode.insertBefore(this.backdrop_, this.dialog_.nextSibling);
441
442 // Focus on whatever inside the dialog.
443 this.focus_();
444 },
445
446 /**
447 * Closes this HTMLDialogElement. This is optional vs clearing the open
448 * attribute, however this fires a 'close' event.
449 *
450 * @param {string=} opt_returnValue to use as the returnValue
451 */
452 close: function(opt_returnValue) {
453 if (!this.dialog_.hasAttribute('open')) {
454 throw new Error('Failed to execute \'close\' on dialog: The element does not have an \'open\' attribute, and therefore cannot be closed.');
455 }
456 this.setOpen(false);
457
458 // Leave returnValue untouched in case it was set directly on the element
459 if (opt_returnValue !== undefined) {
460 this.dialog_.returnValue = opt_returnValue;
461 }
462
463 // Triggering "close" event for any attached listeners on the <dialog>.
464 var closeEvent = new supportCustomEvent('close', {
465 bubbles: false,
466 cancelable: false
467 });
468 safeDispatchEvent(this.dialog_, closeEvent);
469 }
470
471});
472
473var dialogPolyfill = {};
474
475dialogPolyfill.reposition = function(element) {
476 var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
477 var topValue = scrollTop + (window.innerHeight - element.offsetHeight) / 2;
478 element.style.top = Math.max(scrollTop, topValue) + 'px';
479};
480
481dialogPolyfill.isInlinePositionSetByStylesheet = function(element) {
482 for (var i = 0; i < document.styleSheets.length; ++i) {
483 var styleSheet = document.styleSheets[i];
484 var cssRules = null;
485 // Some browsers throw on cssRules.
486 try {
487 cssRules = styleSheet.cssRules;
488 } catch (e) {}
489 if (!cssRules) { continue; }
490 for (var j = 0; j < cssRules.length; ++j) {
491 var rule = cssRules[j];
492 var selectedNodes = null;
493 // Ignore errors on invalid selector texts.
494 try {
495 selectedNodes = document.querySelectorAll(rule.selectorText);
496 } catch(e) {}
497 if (!selectedNodes || !inNodeList(selectedNodes, element)) {
498 continue;
499 }
500 var cssTop = rule.style.getPropertyValue('top');
501 var cssBottom = rule.style.getPropertyValue('bottom');
502 if ((cssTop && cssTop !== 'auto') || (cssBottom && cssBottom !== 'auto')) {
503 return true;
504 }
505 }
506 }
507 return false;
508};
509
510dialogPolyfill.needsCentering = function(dialog) {
511 var computedStyle = window.getComputedStyle(dialog);
512 if (computedStyle.position !== 'absolute') {
513 return false;
514 }
515
516 // We must determine whether the top/bottom specified value is non-auto. In
517 // WebKit/Blink, checking computedStyle.top == 'auto' is sufficient, but
518 // Firefox returns the used value. So we do this crazy thing instead: check
519 // the inline style and then go through CSS rules.
520 if ((dialog.style.top !== 'auto' && dialog.style.top !== '') ||
521 (dialog.style.bottom !== 'auto' && dialog.style.bottom !== '')) {
522 return false;
523 }
524 return !dialogPolyfill.isInlinePositionSetByStylesheet(dialog);
525};
526
527/**
528 * @param {!Element} element to force upgrade
529 */
530dialogPolyfill.forceRegisterDialog = function(element) {
531 if (window.HTMLDialogElement || element.showModal) {
532 console.warn('This browser already supports <dialog>, the polyfill ' +
533 'may not work correctly', element);
534 }
535 if (element.localName !== 'dialog') {
536 throw new Error('Failed to register dialog: The element is not a dialog.');
537 }
538 new dialogPolyfillInfo(/** @type {!HTMLDialogElement} */ (element));
539};
540
541/**
542 * @param {!Element} element to upgrade, if necessary
543 */
544dialogPolyfill.registerDialog = function(element) {
545 if (!element.showModal) {
546 dialogPolyfill.forceRegisterDialog(element);
547 }
548};
549
550/**
551 * @constructor
552 */
553dialogPolyfill.DialogManager = function() {
554 /** @type {!Array<!dialogPolyfillInfo>} */
555 this.pendingDialogStack = [];
556
557 var checkDOM = this.checkDOM_.bind(this);
558
559 // The overlay is used to simulate how a modal dialog blocks the document.
560 // The blocking dialog is positioned on top of the overlay, and the rest of
561 // the dialogs on the pending dialog stack are positioned below it. In the
562 // actual implementation, the modal dialog stacking is controlled by the
563 // top layer, where z-index has no effect.
564 this.overlay = document.createElement('div');
565 this.overlay.className = '_poly_dialog_overlay';
566 this.overlay.addEventListener('click', function(e) {
567 this.forwardTab_ = undefined;
568 e.stopPropagation();
569 checkDOM([]); // sanity-check DOM
570 }.bind(this));
571
572 this.handleKey_ = this.handleKey_.bind(this);
573 this.handleFocus_ = this.handleFocus_.bind(this);
574
575 this.zIndexLow_ = 100000;
576 this.zIndexHigh_ = 100000 + 150;
577
578 this.forwardTab_ = undefined;
579
580 if ('MutationObserver' in window) {
581 this.mo_ = new MutationObserver(function(records) {
582 var removed = [];
583 records.forEach(function(rec) {
584 for (var i = 0, c; c = rec.removedNodes[i]; ++i) {
585 if (!(c instanceof Element)) {
586 continue;
587 } else if (c.localName === 'dialog') {
588 removed.push(c);
589 }
590 removed = removed.concat(c.querySelectorAll('dialog'));
591 }
592 });
593 removed.length && checkDOM(removed);
594 });
595 }
596};
597
598/**
599 * Called on the first modal dialog being shown. Adds the overlay and related
600 * handlers.
601 */
602dialogPolyfill.DialogManager.prototype.blockDocument = function() {
603 document.documentElement.addEventListener('focus', this.handleFocus_, true);
604 document.addEventListener('keydown', this.handleKey_);
605 this.mo_ && this.mo_.observe(document, {childList: true, subtree: true});
606};
607
608/**
609 * Called on the first modal dialog being removed, i.e., when no more modal
610 * dialogs are visible.
611 */
612dialogPolyfill.DialogManager.prototype.unblockDocument = function() {
613 document.documentElement.removeEventListener('focus', this.handleFocus_, true);
614 document.removeEventListener('keydown', this.handleKey_);
615 this.mo_ && this.mo_.disconnect();
616};
617
618/**
619 * Updates the stacking of all known dialogs.
620 */
621dialogPolyfill.DialogManager.prototype.updateStacking = function() {
622 var zIndex = this.zIndexHigh_;
623
624 for (var i = 0, dpi; dpi = this.pendingDialogStack[i]; ++i) {
625 dpi.updateZIndex(--zIndex, --zIndex);
626 if (i === 0) {
627 this.overlay.style.zIndex = --zIndex;
628 }
629 }
630
631 // Make the overlay a sibling of the dialog itself.
632 var last = this.pendingDialogStack[0];
633 if (last) {
634 var p = last.dialog.parentNode || document.body;
635 p.appendChild(this.overlay);
636 } else if (this.overlay.parentNode) {
637 this.overlay.parentNode.removeChild(this.overlay);
638 }
639};
640
641/**
642 * @param {Element} candidate to check if contained or is the top-most modal dialog
643 * @return {boolean} whether candidate is contained in top dialog
644 */
645dialogPolyfill.DialogManager.prototype.containedByTopDialog_ = function(candidate) {
646 while (candidate = findNearestDialog(candidate)) {
647 for (var i = 0, dpi; dpi = this.pendingDialogStack[i]; ++i) {
648 if (dpi.dialog === candidate) {
649 return i === 0; // only valid if top-most
650 }
651 }
652 candidate = candidate.parentElement;
653 }
654 return false;
655};
656
657dialogPolyfill.DialogManager.prototype.handleFocus_ = function(event) {
658 var target = event.composedPath ? event.composedPath()[0] : event.target;
659
660 if (this.containedByTopDialog_(target)) { return; }
661
662 if (document.activeElement === document.documentElement) { return; }
663
664 event.preventDefault();
665 event.stopPropagation();
666 safeBlur(/** @type {Element} */ (target));
667
668 if (this.forwardTab_ === undefined) { return; } // move focus only from a tab key
669
670 var dpi = this.pendingDialogStack[0];
671 var dialog = dpi.dialog;
672 var position = dialog.compareDocumentPosition(target);
673 if (position & Node.DOCUMENT_POSITION_PRECEDING) {
674 if (this.forwardTab_) {
675 // forward
676 dpi.focus_();
677 } else if (target !== document.documentElement) {
678 // backwards if we're not already focused on <html>
679 document.documentElement.focus();
680 }
681 }
682
683 return false;
684};
685
686dialogPolyfill.DialogManager.prototype.handleKey_ = function(event) {
687 this.forwardTab_ = undefined;
688 if (event.keyCode === 27) {
689 event.preventDefault();
690 event.stopPropagation();
691 var cancelEvent = new supportCustomEvent('cancel', {
692 bubbles: false,
693 cancelable: true
694 });
695 var dpi = this.pendingDialogStack[0];
696 if (dpi && safeDispatchEvent(dpi.dialog, cancelEvent)) {
697 dpi.dialog.close();
698 }
699 } else if (event.keyCode === 9) {
700 this.forwardTab_ = !event.shiftKey;
701 }
702};
703
704/**
705 * Finds and downgrades any known modal dialogs that are no longer displayed. Dialogs that are
706 * removed and immediately readded don't stay modal, they become normal.
707 *
708 * @param {!Array<!HTMLDialogElement>} removed that have definitely been removed
709 */
710dialogPolyfill.DialogManager.prototype.checkDOM_ = function(removed) {
711 // This operates on a clone because it may cause it to change. Each change also calls
712 // updateStacking, which only actually needs to happen once. But who removes many modal dialogs
713 // at a time?!
714 var clone = this.pendingDialogStack.slice();
715 clone.forEach(function(dpi) {
716 if (removed.indexOf(dpi.dialog) !== -1) {
717 dpi.downgradeModal();
718 } else {
719 dpi.maybeHideModal();
720 }
721 });
722};
723
724/**
725 * @param {!dialogPolyfillInfo} dpi
726 * @return {boolean} whether the dialog was allowed
727 */
728dialogPolyfill.DialogManager.prototype.pushDialog = function(dpi) {
729 var allowed = (this.zIndexHigh_ - this.zIndexLow_) / 2 - 1;
730 if (this.pendingDialogStack.length >= allowed) {
731 return false;
732 }
733 if (this.pendingDialogStack.unshift(dpi) === 1) {
734 this.blockDocument();
735 }
736 this.updateStacking();
737 return true;
738};
739
740/**
741 * @param {!dialogPolyfillInfo} dpi
742 */
743dialogPolyfill.DialogManager.prototype.removeDialog = function(dpi) {
744 var index = this.pendingDialogStack.indexOf(dpi);
745 if (index === -1) { return; }
746
747 this.pendingDialogStack.splice(index, 1);
748 if (this.pendingDialogStack.length === 0) {
749 this.unblockDocument();
750 }
751 this.updateStacking();
752};
753
754dialogPolyfill.dm = new dialogPolyfill.DialogManager();
755dialogPolyfill.formSubmitter = null;
756dialogPolyfill.imagemapUseValue = null;
757
758/**
759 * Installs global handlers, such as click listers and native method overrides. These are needed
760 * even if a no dialog is registered, as they deal with <form method="dialog">.
761 */
762if (window.HTMLDialogElement === undefined) {
763
764 /**
765 * If HTMLFormElement translates method="DIALOG" into 'get', then replace the descriptor with
766 * one that returns the correct value.
767 */
768 var testForm = document.createElement('form');
769 testForm.setAttribute('method', 'dialog');
770 if (testForm.method !== 'dialog') {
771 var methodDescriptor = Object.getOwnPropertyDescriptor(HTMLFormElement.prototype, 'method');
772 if (methodDescriptor) {
773 // nb. Some older iOS and older PhantomJS fail to return the descriptor. Don't do anything
774 // and don't bother to update the element.
775 var realGet = methodDescriptor.get;
776 methodDescriptor.get = function() {
777 if (isFormMethodDialog(this)) {
778 return 'dialog';
779 }
780 return realGet.call(this);
781 };
782 var realSet = methodDescriptor.set;
783 /** @this {HTMLElement} */
784 methodDescriptor.set = function(v) {
785 if (typeof v === 'string' && v.toLowerCase() === 'dialog') {
786 return this.setAttribute('method', v);
787 }
788 return realSet.call(this, v);
789 };
790 Object.defineProperty(HTMLFormElement.prototype, 'method', methodDescriptor);
791 }
792 }
793
794 /**
795 * Global 'click' handler, to capture the <input type="submit"> or <button> element which has
796 * submitted a <form method="dialog">. Needed as Safari and others don't report this inside
797 * document.activeElement.
798 */
799 document.addEventListener('click', function(ev) {
800 dialogPolyfill.formSubmitter = null;
801 dialogPolyfill.imagemapUseValue = null;
802 if (ev.defaultPrevented) { return; } // e.g. a submit which prevents default submission
803
804 var target = /** @type {Element} */ (ev.target);
805 if ('composedPath' in ev) {
806 var path = ev.composedPath();
807 target = path.shift() || target;
808 }
809 if (!target || !isFormMethodDialog(target.form)) { return; }
810
811 var valid = (target.type === 'submit' && ['button', 'input'].indexOf(target.localName) > -1);
812 if (!valid) {
813 if (!(target.localName === 'input' && target.type === 'image')) { return; }
814 // this is a <input type="image">, which can submit forms
815 dialogPolyfill.imagemapUseValue = ev.offsetX + ',' + ev.offsetY;
816 }
817
818 var dialog = findNearestDialog(target);
819 if (!dialog) { return; }
820
821 dialogPolyfill.formSubmitter = target;
822
823 }, false);
824
825 /**
826 * Global 'submit' handler. This handles submits of `method="dialog"` which are invalid, i.e.,
827 * outside a dialog. They get prevented.
828 */
829 document.addEventListener('submit', function(ev) {
830 var form = ev.target;
831 var dialog = findNearestDialog(form);
832 if (dialog) {
833 return; // ignore, handle there
834 }
835
836 var submitter = findFormSubmitter(ev);
837 var formmethod = submitter && submitter.getAttribute('formmethod') || form.getAttribute('method');
838 if (formmethod === 'dialog') {
839 ev.preventDefault();
840 }
841 });
842
843 /**
844 * Replace the native HTMLFormElement.submit() method, as it won't fire the
845 * submit event and give us a chance to respond.
846 */
847 var nativeFormSubmit = HTMLFormElement.prototype.submit;
848 var replacementFormSubmit = function () {
849 if (!isFormMethodDialog(this)) {
850 return nativeFormSubmit.call(this);
851 }
852 var dialog = findNearestDialog(this);
853 dialog && dialog.close();
854 };
855 HTMLFormElement.prototype.submit = replacementFormSubmit;
856}
857
858export default dialogPolyfill;
public/script.js+54 -24
@@ -83,6 +83,7 @@ import {
83 resetMovableStyles,83 resetMovableStyles,
84 forceCharacterEditorTokenize,84 forceCharacterEditorTokenize,
85 applyPowerUserSettings,85 applyPowerUserSettings,
86 switchSwipeNumAllMessages,
86} from './scripts/power-user.js';87} from './scripts/power-user.js';
8788
88import {89import {
@@ -1884,6 +1885,7 @@ export async function reloadCurrentChat() {
1884export async function sendTextareaMessage() {1885export async function sendTextareaMessage() {
1885 if (is_send_press) return;1886 if (is_send_press) return;
1886 if (isExecutingCommandsFromChatInput) return;1887 if (isExecutingCommandsFromChatInput) return;
1888 if (this_edit_mes_id) return; // don't proceed if editing a message
18871889
1888 let generateType;1890 let generateType;
1889 // "Continue on send" is activated when the user hits "send" (or presses enter) on an empty chat box, and the last1891 // "Continue on send" is activated when the user hits "send" (or presses enter) on an empty chat box, and the last
@@ -2369,6 +2371,13 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
23692371
2370 addCopyToCodeBlocks(newMessage);2372 addCopyToCodeBlocks(newMessage);
23712373
2374 // Set the swipes counter for past messages, only visible if 'Show Swipes on All Message' is enabled
2375 if (!params.isUser && newMessageId !== 0 && newMessageId !== chat.length - 1) {
2376 const swipesNum = chat[newMessageId].swipes?.length;
2377 const swipeId = chat[newMessageId].swipe_id + 1;
2378 newMessage.find('.swipes-counter').text(formatSwipeCounter(swipeId, swipesNum));
2379 }
2380
2372 if (showSwipes) {2381 if (showSwipes) {
2373 $('#chat .mes').last().addClass('last_mes');2382 $('#chat .mes').last().addClass('last_mes');
2374 $('#chat .mes').eq(-2).removeClass('last_mes');2383 $('#chat .mes').eq(-2).removeClass('last_mes');
@@ -4785,7 +4794,7 @@ export function removeMacros(str) {
4785 * @param {boolean} [compact] Send as a compact display message.4794 * @param {boolean} [compact] Send as a compact display message.
4786 * @param {string} [name] Name of the user sending the message. Defaults to name1.4795 * @param {string} [name] Name of the user sending the message. Defaults to name1.
4787 * @param {string} [avatar] Avatar of the user sending the message. Defaults to user_avatar.4796 * @param {string} [avatar] Avatar of the user sending the message. Defaults to user_avatar.
4788 * @returns {Promise<void>} A promise that resolves when the message is inserted.4797 * @returns {Promise<any>} A promise that resolves to the message when it is inserted.
4789 */4798 */
4790export async function sendMessageAsUser(messageText, messageBias, insertAt = null, compact = false, name = name1, avatar = user_avatar) {4799export async function sendMessageAsUser(messageText, messageBias, insertAt = null, compact = false, name = name1, avatar = user_avatar) {
4791 messageText = getRegexedString(messageText, regex_placement.USER_INPUT);4800 messageText = getRegexedString(messageText, regex_placement.USER_INPUT);
@@ -4832,6 +4841,8 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul
4832 await eventSource.emit(event_types.USER_MESSAGE_RENDERED, chat_id);4841 await eventSource.emit(event_types.USER_MESSAGE_RENDERED, chat_id);
4833 await saveChatConditional();4842 await saveChatConditional();
4834 }4843 }
4844
4845 return message;
4835}4846}
48364847
4837/**4848/**
@@ -7469,7 +7480,6 @@ export function showSwipeButtons() {
7469 //had to add this to make the swipe counter work7480 //had to add this to make the swipe counter work
7470 //(copied from the onclick functions for swipe buttons..7481 //(copied from the onclick functions for swipe buttons..
7471 //don't know why the array isn't set for non-swipe messsages in Generate or addOneMessage..)7482 //don't know why the array isn't set for non-swipe messsages in Generate or addOneMessage..)
7472
7473 if (chat[chat.length - 1]['swipe_id'] === undefined) { // if there is no swipe-message in the last spot of the chat array7483 if (chat[chat.length - 1]['swipe_id'] === undefined) { // if there is no swipe-message in the last spot of the chat array
7474 chat[chat.length - 1]['swipe_id'] = 0; // set it to id 07484 chat[chat.length - 1]['swipe_id'] = 0; // set it to id 0
7475 chat[chat.length - 1]['swipes'] = []; // empty the array7485 chat[chat.length - 1]['swipes'] = []; // empty the array
@@ -7478,33 +7488,37 @@ export function showSwipeButtons() {
74787488
7479 const currentMessage = $('#chat').children().filter(`[mesid="${chat.length - 1}"]`);7489 const currentMessage = $('#chat').children().filter(`[mesid="${chat.length - 1}"]`);
7480 const swipeId = chat[chat.length - 1].swipe_id;7490 const swipeId = chat[chat.length - 1].swipe_id;
7481 const swipeCounterText = (`${(swipeId + 1)}\u200B/\u200b${(chat[chat.length - 1].swipes.length)}`);7491 const swipeCounterText = formatSwipeCounter((swipeId + 1), chat[chat.length - 1].swipes.length);
7492 const swipeRight = currentMessage.find('.swipe_right');
7493 const swipeLeft = currentMessage.find('.swipe_left');
7494 const swipeCounter = currentMessage.find('.swipes-counter');
74827495
7483 if (swipeId !== undefined && (chat[chat.length - 1].swipes.length > 1 || swipeId > 0)) {7496 if (swipeId !== undefined && (chat[chat.length - 1].swipes.length > 1 || swipeId > 0)) {
7484 currentMessage.children('.swipe_left').css('display', 'flex');7497 swipeLeft.css('display', 'flex');
7485 }7498 }
7486 //only show right when generate is off, or when next right swipe would not make a generate happen7499 //only show right when generate is off, or when next right swipe would not make a generate happen
7487 if (is_send_press === false || chat[chat.length - 1].swipes.length >= swipeId) {7500 if (is_send_press === false || chat[chat.length - 1].swipes.length >= swipeId) {
7488 currentMessage.children('.swipe_right').css('display', 'flex');7501 swipeRight.css('display', 'flex').css('opacity', '0.3');
7489 currentMessage.children('.swipe_right').css('opacity', '0.3');7502 swipeCounter.css('opacity', '0.3');
7490 }7503 }
7491 //console.log((chat[chat.length - 1]));
7492 if ((chat[chat.length - 1].swipes.length - swipeId) === 1) {7504 if ((chat[chat.length - 1].swipes.length - swipeId) === 1) {
7493 //console.log('highlighting R swipe');7505 //chevron was moved out of hardcode in HTML to class toggle dependent on last_mes or not
7494 currentMessage.children('.swipe_right').css('opacity', '0.7');7506 //necessary for 'swipe_right' div in past messages to have no chevron if 'show swipes for all messages' is turned on
7507 swipeRight.css('opacity', '0.7');
7508 swipeCounter.css('opacity', '0.7');
7495 }7509 }
7496 //console.log(swipesCounterHTML);
74977510
7498 $('.swipes-counter').text(swipeCounterText);7511 //allows for writing individual swipe counters for past messages
7512 const lastSwipeCounter = $('.last_mes .swipes-counter');
7513 lastSwipeCounter.text(swipeCounterText).show();
74997514
7500 //console.log(swipeId);7515 switchSwipeNumAllMessages();
7501 //console.log(chat[chat.length - 1].swipes.length);
7502}7516}
75037517
7504export function hideSwipeButtons() {7518export function hideSwipeButtons() {
7505 //console.log('hideswipebuttons entered');7519 $('#chat').find('.swipe_right').hide();
7506 $('#chat').find('.swipe_right').css('display', 'none');7520 $('#chat').find('.last_mes .swipes-counter').hide();
7507 $('#chat').find('.swipe_left').css('display', 'none');7521 $('#chat').find('.swipe_left').hide();
7508}7522}
75097523
7510/**7524/**
@@ -8152,6 +8166,20 @@ window['SillyTavern'].getContext = function () {
8152 };8166 };
8153};8167};
81548168
8169/**
8170 * Formats a counter for a swipe view.
8171 * @param {number} current The current number of items.
8172 * @param {number} total The total number of items.
8173 * @returns {string} The formatted counter.
8174 */
8175function formatSwipeCounter(current, total) {
8176 if (isNaN(current) || isNaN(total)) {
8177 return '';
8178 }
8179
8180 return `${current}\u200b/\u200b${total}`;
8181}
8182
8155function swipe_left() { // when we swipe left..but no generation.8183function swipe_left() { // when we swipe left..but no generation.
8156 if (chat.length - 1 === Number(this_edit_mes_id)) {8184 if (chat.length - 1 === Number(this_edit_mes_id)) {
8157 closeMessageEditor();8185 closeMessageEditor();
@@ -8339,8 +8367,8 @@ const swipe_right = () => {
8339 }8367 }
83408368
8341 const currentMessage = $('#chat').children().filter(`[mesid="${chat.length - 1}"]`);8369 const currentMessage = $('#chat').children().filter(`[mesid="${chat.length - 1}"]`);
8342 let this_div = currentMessage.children('.swipe_right');8370 let this_div = currentMessage.find('.swipe_right');
8343 let this_mes_div = this_div.parent();8371 let this_mes_div = this_div.parent().parent();
83448372
8345 if (chat[chat.length - 1]['swipe_id'] > chat[chat.length - 1]['swipes'].length) { //if we swipe right while generating (the swipe ID is greater than what we are viewing now)8373 if (chat[chat.length - 1]['swipe_id'] > chat[chat.length - 1]['swipes'].length) { //if we swipe right while generating (the swipe ID is greater than what we are viewing now)
8346 chat[chat.length - 1]['swipe_id'] = chat[chat.length - 1]['swipes'].length; //show that message slot (will be '...' while generating)8374 chat[chat.length - 1]['swipe_id'] = chat[chat.length - 1]['swipes'].length; //show that message slot (will be '...' while generating)
@@ -8350,7 +8378,7 @@ const swipe_right = () => {
8350 }8378 }
8351 // handles animated transitions when swipe right, specifically height transitions between messages8379 // handles animated transitions when swipe right, specifically height transitions between messages
8352 if (run_generate || run_swipe_right) {8380 if (run_generate || run_swipe_right) {
8353 let this_mes_block = this_mes_div.children('.mes_block').children('.mes_text');8381 let this_mes_block = this_mes_div.find('.mes_block .mes_text');
8354 const this_mes_div_height = this_mes_div[0].scrollHeight;8382 const this_mes_div_height = this_mes_div[0].scrollHeight;
8355 const this_mes_block_height = this_mes_block[0].scrollHeight;8383 const this_mes_block_height = this_mes_block[0].scrollHeight;
83568384
@@ -9407,9 +9435,9 @@ jQuery(async function () {
94079435
9408 ///// SWIPE BUTTON CLICKS ///////9436 ///// SWIPE BUTTON CLICKS ///////
94099437
9410 $(document).on('click', '.swipe_right', swipe_right);9438 //limit swiping to only last message clicks
94119439 $(document).on('click', '.last_mes .swipe_right', swipe_right);
9412 $(document).on('click', '.swipe_left', swipe_left);9440 $(document).on('click', '.last_mes .swipe_left', swipe_left);
94139441
9414 const debouncedCharacterSearch = debounce((searchQuery) => {9442 const debouncedCharacterSearch = debounce((searchQuery) => {
9415 entitiesFilter.setFilterData(FILTER_TYPES.SEARCH, searchQuery);9443 entitiesFilter.setFilterData(FILTER_TYPES.SEARCH, searchQuery);
@@ -9970,6 +9998,8 @@ jQuery(async function () {
9970 }9998 }
99719999
9972 else if (id == 'option_continue') {10000 else if (id == 'option_continue') {
10001 if (this_edit_mes_id) return; // don't proceed if editing a message
10002
9973 if (is_send_press == false || fromSlashCommand) {10003 if (is_send_press == false || fromSlashCommand) {
9974 is_send_press = true;10004 is_send_press = true;
9975 Generate('continue', buildOrFillAdditionalArgs());10005 Generate('continue', buildOrFillAdditionalArgs());
@@ -10636,7 +10666,7 @@ jQuery(async function () {
10636 $('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', async function () {10666 $('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', async function () {
10637 await delay(50); $(this).closest('.drawer-content').removeClass('resizing');10667 await delay(50); $(this).closest('.drawer-content').removeClass('resizing');
10638 });10668 });
10639 $('.openIcon').toggleClass('closedIcon openIcon');10669 $('.openIcon').not('.drawerPinnedOpen').toggleClass('closedIcon openIcon');
10640 $('.openDrawer').not('.pinnedOpen').toggleClass('closedDrawer openDrawer');10670 $('.openDrawer').not('.pinnedOpen').toggleClass('closedDrawer openDrawer');
10641 icon.toggleClass('openIcon closedIcon');10671 icon.toggleClass('openIcon closedIcon');
10642 drawer.toggleClass('openDrawer closedDrawer');10672 drawer.toggleClass('openDrawer closedDrawer');
@@ -10722,7 +10752,7 @@ jQuery(async function () {
10722 $('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', function () {10752 $('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', function () {
10723 $(this).closest('.drawer-content').removeClass('resizing');10753 $(this).closest('.drawer-content').removeClass('resizing');
10724 });10754 });
10725 $('.openIcon').toggleClass('closedIcon openIcon');10755 $('.openIcon').not('.drawerPinnedOpen').toggleClass('closedIcon openIcon');
10726 $('.openDrawer').not('.pinnedOpen').toggleClass('closedDrawer openDrawer');10756 $('.openDrawer').not('.pinnedOpen').toggleClass('closedDrawer openDrawer');
1072710757
10728 }10758 }
public/scripts/RossAscends-mods.js+19 -3
@@ -45,8 +45,11 @@ var LPanelPin = document.getElementById('lm_button_panel_pin');
45var WIPanelPin = document.getElementById('WI_panel_pin');45var WIPanelPin = document.getElementById('WI_panel_pin');
4646
47var RightNavPanel = document.getElementById('right-nav-panel');47var RightNavPanel = document.getElementById('right-nav-panel');
48var RightNavDrawerIcon = document.getElementById('rightNavDrawerIcon');
48var LeftNavPanel = document.getElementById('left-nav-panel');49var LeftNavPanel = document.getElementById('left-nav-panel');
50var LeftNavDrawerIcon = document.getElementById('leftNavDrawerIcon');
49var WorldInfo = document.getElementById('WorldInfo');51var WorldInfo = document.getElementById('WorldInfo');
52var WIDrawerIcon = document.getElementById('WIDrawerIcon');
5053
51var SelectedCharacterTab = document.getElementById('rm_button_selected_ch');54var SelectedCharacterTab = document.getElementById('rm_button_selected_ch');
5255
@@ -761,13 +764,15 @@ export function initRossMods() {
761 if ($(RPanelPin).prop('checked') == true) {764 if ($(RPanelPin).prop('checked') == true) {
762 //console.log('adding pin class to right nav');765 //console.log('adding pin class to right nav');
763 $(RightNavPanel).addClass('pinnedOpen');766 $(RightNavPanel).addClass('pinnedOpen');
767 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
764 } else {768 } else {
765 //console.log('removing pin class from right nav');769 //console.log('removing pin class from right nav');
766 $(RightNavPanel).removeClass('pinnedOpen');770 $(RightNavPanel).removeClass('pinnedOpen');
771 $(RightNavDrawerIcon).removeClass('drawerPinnedOpen');
767772
768 if ($(RightNavPanel).hasClass('openDrawer') && $('.openDrawer').length > 1) {773 if ($(RightNavPanel).hasClass('openDrawer') && $('.openDrawer').length > 1) {
769 $(RightNavPanel).slideToggle(200, 'swing');774 $(RightNavPanel).slideToggle(200, 'swing');
770 //$(rightNavDrawerIcon).toggleClass('openIcon closedIcon');775 $(RightNavDrawerIcon).toggleClass('openIcon closedIcon');
771 $(RightNavPanel).toggleClass('openDrawer closedDrawer');776 $(RightNavPanel).toggleClass('openDrawer closedDrawer');
772 }777 }
773 }778 }
@@ -777,13 +782,15 @@ export function initRossMods() {
777 if ($(LPanelPin).prop('checked') == true) {782 if ($(LPanelPin).prop('checked') == true) {
778 //console.log('adding pin class to Left nav');783 //console.log('adding pin class to Left nav');
779 $(LeftNavPanel).addClass('pinnedOpen');784 $(LeftNavPanel).addClass('pinnedOpen');
785 $(LeftNavDrawerIcon).addClass('drawerPinnedOpen');
780 } else {786 } else {
781 //console.log('removing pin class from Left nav');787 //console.log('removing pin class from Left nav');
782 $(LeftNavPanel).removeClass('pinnedOpen');788 $(LeftNavPanel).removeClass('pinnedOpen');
789 $(LeftNavDrawerIcon).removeClass('drawerPinnedOpen');
783790
784 if ($(LeftNavPanel).hasClass('openDrawer') && $('.openDrawer').length > 1) {791 if ($(LeftNavPanel).hasClass('openDrawer') && $('.openDrawer').length > 1) {
785 $(LeftNavPanel).slideToggle(200, 'swing');792 $(LeftNavPanel).slideToggle(200, 'swing');
786 //$(leftNavDrawerIcon).toggleClass('openIcon closedIcon');793 $(LeftNavDrawerIcon).toggleClass('openIcon closedIcon');
787 $(LeftNavPanel).toggleClass('openDrawer closedDrawer');794 $(LeftNavPanel).toggleClass('openDrawer closedDrawer');
788 }795 }
789 }796 }
@@ -794,14 +801,16 @@ export function initRossMods() {
794 if ($(WIPanelPin).prop('checked') == true) {801 if ($(WIPanelPin).prop('checked') == true) {
795 console.debug('adding pin class to WI');802 console.debug('adding pin class to WI');
796 $(WorldInfo).addClass('pinnedOpen');803 $(WorldInfo).addClass('pinnedOpen');
804 $(WIDrawerIcon).addClass('drawerPinnedOpen');
797 } else {805 } else {
798 console.debug('removing pin class from WI');806 console.debug('removing pin class from WI');
799 $(WorldInfo).removeClass('pinnedOpen');807 $(WorldInfo).removeClass('pinnedOpen');
808 $(WIDrawerIcon).removeClass('drawerPinnedOpen');
800809
801 if ($(WorldInfo).hasClass('openDrawer') && $('.openDrawer').length > 1) {810 if ($(WorldInfo).hasClass('openDrawer') && $('.openDrawer').length > 1) {
802 console.debug('closing WI after lock removal');811 console.debug('closing WI after lock removal');
803 $(WorldInfo).slideToggle(200, 'swing');812 $(WorldInfo).slideToggle(200, 'swing');
804 //$(WorldInfoDrawerIcon).toggleClass('openIcon closedIcon');813 $(WIDrawerIcon).toggleClass('openIcon closedIcon');
805 $(WorldInfo).toggleClass('openDrawer closedDrawer');814 $(WorldInfo).toggleClass('openDrawer closedDrawer');
806 }815 }
807 }816 }
@@ -812,20 +821,24 @@ export function initRossMods() {
812 if (LoadLocalBool('NavLockOn') == true) {821 if (LoadLocalBool('NavLockOn') == true) {
813 //console.log('setting pin class via local var');822 //console.log('setting pin class via local var');
814 $(RightNavPanel).addClass('pinnedOpen');823 $(RightNavPanel).addClass('pinnedOpen');
824 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
815 }825 }
816 if ($(RPanelPin).prop('checked')) {826 if ($(RPanelPin).prop('checked')) {
817 console.debug('setting pin class via checkbox state');827 console.debug('setting pin class via checkbox state');
818 $(RightNavPanel).addClass('pinnedOpen');828 $(RightNavPanel).addClass('pinnedOpen');
829 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
819 }830 }
820 // read the state of left Nav Lock and apply to leftnav classlist831 // read the state of left Nav Lock and apply to leftnav classlist
821 $(LPanelPin).prop('checked', LoadLocalBool('LNavLockOn'));832 $(LPanelPin).prop('checked', LoadLocalBool('LNavLockOn'));
822 if (LoadLocalBool('LNavLockOn') == true) {833 if (LoadLocalBool('LNavLockOn') == true) {
823 //console.log('setting pin class via local var');834 //console.log('setting pin class via local var');
824 $(LeftNavPanel).addClass('pinnedOpen');835 $(LeftNavPanel).addClass('pinnedOpen');
836 $(LeftNavDrawerIcon).addClass('drawerPinnedOpen');
825 }837 }
826 if ($(LPanelPin).prop('checked')) {838 if ($(LPanelPin).prop('checked')) {
827 console.debug('setting pin class via checkbox state');839 console.debug('setting pin class via checkbox state');
828 $(LeftNavPanel).addClass('pinnedOpen');840 $(LeftNavPanel).addClass('pinnedOpen');
841 $(LeftNavDrawerIcon).addClass('drawerPinnedOpen');
829 }842 }
830843
831 // read the state of left Nav Lock and apply to leftnav classlist844 // read the state of left Nav Lock and apply to leftnav classlist
@@ -833,11 +846,13 @@ export function initRossMods() {
833 if (LoadLocalBool('WINavLockOn') == true) {846 if (LoadLocalBool('WINavLockOn') == true) {
834 //console.log('setting pin class via local var');847 //console.log('setting pin class via local var');
835 $(WorldInfo).addClass('pinnedOpen');848 $(WorldInfo).addClass('pinnedOpen');
849 $(WIDrawerIcon).addClass('drawerPinnedOpen');
836 }850 }
837851
838 if ($(WIPanelPin).prop('checked')) {852 if ($(WIPanelPin).prop('checked')) {
839 console.debug('setting pin class via checkbox state');853 console.debug('setting pin class via checkbox state');
840 $(WorldInfo).addClass('pinnedOpen');854 $(WorldInfo).addClass('pinnedOpen');
855 $(WIDrawerIcon).addClass('drawerPinnedOpen');
841 }856 }
842857
843 //save state of Right nav being open or closed858 //save state of Right nav being open or closed
@@ -1027,6 +1042,7 @@ export function initRossMods() {
1027 const editMesDone = $('.mes_edit_done:visible');1042 const editMesDone = $('.mes_edit_done:visible');
1028 if (editMesDone.length > 0) {1043 if (editMesDone.length > 0) {
1029 console.debug('Accepting edits with Ctrl+Enter');1044 console.debug('Accepting edits with Ctrl+Enter');
1045 $('#send_textarea').focus();
1030 editMesDone.trigger('click');1046 editMesDone.trigger('click');
1031 return;1047 return;
1032 } else if (is_send_press == false) {1048 } else if (is_send_press == false) {
public/scripts/extensions/attachments/index.js+21 -0
@@ -1,3 +1,4 @@
1import { event_types, eventSource, saveSettingsDebounced } from '../../../script.js';
1import { deleteAttachment, getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment, uploadFileAttachmentToServer } from '../../chats.js';2import { deleteAttachment, getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment, uploadFileAttachmentToServer } from '../../chats.js';
2import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';3import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
3import { SlashCommand } from '../../slash-commands/SlashCommand.js';4import { SlashCommand } from '../../slash-commands/SlashCommand.js';
@@ -196,7 +197,27 @@ async function enableDataBankAttachment(args, value) {
196 return '';197 return '';
197}198}
198199
200function cleanUpAttachments() {
201 let shouldSaveSettings = false;
202 if (extension_settings.character_attachments) {
203 Object.values(extension_settings.character_attachments).flat().filter(a => a.text).forEach(a => {
204 shouldSaveSettings = true;
205 delete a.text;
206 });
207 }
208 if (Array.isArray(extension_settings.attachments)) {
209 extension_settings.attachments.filter(a => a.text).forEach(a => {
210 shouldSaveSettings = true;
211 delete a.text;
212 });
213 }
214 if (shouldSaveSettings) {
215 saveSettingsDebounced();
216 }
217}
218
199jQuery(async () => {219jQuery(async () => {
220 eventSource.on(event_types.APP_READY, cleanUpAttachments);
200 const manageButton = await renderExtensionTemplateAsync('attachments', 'manage-button', {});221 const manageButton = await renderExtensionTemplateAsync('attachments', 'manage-button', {});
201 const attachButton = await renderExtensionTemplateAsync('attachments', 'attach-button', {});222 const attachButton = await renderExtensionTemplateAsync('attachments', 'attach-button', {});
202 $('#data_bank_wand_container').append(manageButton);223 $('#data_bank_wand_container').append(manageButton);
public/scripts/extensions/caption/index.js+1 -0
@@ -403,6 +403,7 @@ jQuery(async function () {
403 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && (secret_state[SECRET_KEYS.OPENAI] || extension_settings.caption.allow_reverse_proxy)) ||403 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && (secret_state[SECRET_KEYS.OPENAI] || extension_settings.caption.allow_reverse_proxy)) ||
404 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openrouter' && secret_state[SECRET_KEYS.OPENROUTER]) ||404 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openrouter' && secret_state[SECRET_KEYS.OPENROUTER]) ||
405 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'zerooneai' && secret_state[SECRET_KEYS.ZEROONEAI]) ||405 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'zerooneai' && secret_state[SECRET_KEYS.ZEROONEAI]) ||
406 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'groq' && secret_state[SECRET_KEYS.GROQ]) ||
406 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'mistral' && (secret_state[SECRET_KEYS.MISTRALAI] || extension_settings.caption.allow_reverse_proxy)) ||407 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'mistral' && (secret_state[SECRET_KEYS.MISTRALAI] || extension_settings.caption.allow_reverse_proxy)) ||
407 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && (secret_state[SECRET_KEYS.MAKERSUITE] || extension_settings.caption.allow_reverse_proxy)) ||408 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && (secret_state[SECRET_KEYS.MAKERSUITE] || extension_settings.caption.allow_reverse_proxy)) ||
408 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'anthropic' && (secret_state[SECRET_KEYS.CLAUDE] || extension_settings.caption.allow_reverse_proxy)) ||409 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'anthropic' && (secret_state[SECRET_KEYS.CLAUDE] || extension_settings.caption.allow_reverse_proxy)) ||
public/scripts/extensions/caption/settings.html+4 -0
@@ -21,6 +21,7 @@
21 <option value="anthropic">Anthropic</option>21 <option value="anthropic">Anthropic</option>
22 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>22 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
23 <option value="google">Google AI Studio</option>23 <option value="google">Google AI Studio</option>
24 <option value="groq">Groq</option>
24 <option value="koboldcpp">KoboldCpp</option>25 <option value="koboldcpp">KoboldCpp</option>
25 <option value="llamacpp">llama.cpp</option>26 <option value="llamacpp">llama.cpp</option>
26 <option value="mistral">MistralAI</option>27 <option value="mistral">MistralAI</option>
@@ -60,6 +61,9 @@
60 <option data-type="google" value="gemini-1.5-pro-exp-0801">gemini-1.5-pro-exp-0801</option>61 <option data-type="google" value="gemini-1.5-pro-exp-0801">gemini-1.5-pro-exp-0801</option>
61 <option data-type="google" value="gemini-1.5-pro-exp-0827">gemini-1.5-pro-exp-0827</option>62 <option data-type="google" value="gemini-1.5-pro-exp-0827">gemini-1.5-pro-exp-0827</option>
62 <option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>63 <option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>
64 <option data-type="groq" value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>
65 <option data-type="groq" value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>
66 <option data-type="groq" value="llava-v1.5-7b-4096-preview">llava-v1.5-7b-4096-preview</option>
63 <option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>67 <option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
64 <option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>68 <option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>
65 <option data-type="openrouter" value="openai/gpt-4o-2024-05-13">openai/gpt-4o-2024-05-13</option>69 <option data-type="openrouter" value="openai/gpt-4o-2024-05-13">openai/gpt-4o-2024-05-13</option>
public/scripts/extensions/expressions/index.js+31 -5
@@ -12,6 +12,8 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
12import { isFunctionCallingSupported } from '../../openai.js';12import { isFunctionCallingSupported } from '../../openai.js';
13import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';13import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
14import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';14import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
15import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
16import { SlashCommandClosure } from '../../slash-commands/SlashCommandClosure.js';
15export { MODULE_NAME };17export { MODULE_NAME };
1618
17const MODULE_NAME = 'expressions';19const MODULE_NAME = 'expressions';
@@ -2134,18 +2136,42 @@ function migrateSettings() {
2134 name: 'classify-expressions',2136 name: 'classify-expressions',
2135 aliases: ['expressions'],2137 aliases: ['expressions'],
2136 callback: async (args) => {2138 callback: async (args) => {
2137 const list = await getExpressionsList();2139 /** @type {import('../../slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
2138 switch (String(args.format).toLowerCase()) {2140 // @ts-ignore
2141 let returnType = args.return;
2142
2143 // Old legacy return type handling
2144 if (args.format) {
2145 toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning');
2146 const type = String(args?.format).toLowerCase().trim();
2147 switch (type) {
2139 case 'json':2148 case 'json':
2140 return JSON.stringify(list);2149 returnType = 'object';
2150 break;
2141 default:2151 default:
2142 return list.join(', ');2152 returnType = 'pipe';
2153 break;
2154 }
2143 }2155 }
2156
2157 // Now the actual new return type handling
2158 const list = await getExpressionsList();
2159
2160 return await slashCommandReturnHelper.doReturn(returnType ?? 'pipe', list, { objectToStringFunc: list => list.join(', ') });
2144 },2161 },
2145 namedArgumentList: [2162 namedArgumentList: [
2146 SlashCommandNamedArgument.fromProps({2163 SlashCommandNamedArgument.fromProps({
2164 name: 'return',
2165 description: 'The way how you want the return value to be provided',
2166 typeList: [ARGUMENT_TYPE.STRING],
2167 defaultValue: 'pipe',
2168 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
2169 forceEnum: true,
2170 }),
2171 // TODO remove some day
2172 SlashCommandNamedArgument.fromProps({
2147 name: 'format',2173 name: 'format',
2148 description: 'The format to return the list in: comma-separated plain text or JSON array. Default is plain text.',2174 description: '!!! DEPRECATED - use "return" instead !!! The format to return the list in: comma-separated plain text or JSON array. Default is plain text.',
2149 typeList: [ARGUMENT_TYPE.STRING],2175 typeList: [ARGUMENT_TYPE.STRING],
2150 enumList: [2176 enumList: [
2151 new SlashCommandEnumValue('plain', null, enumTypes.enum, ', '),2177 new SlashCommandEnumValue('plain', null, enumTypes.enum, ', '),
public/scripts/extensions/shared.js+5 -1
@@ -36,7 +36,7 @@ export async function getMultimodalCaption(base64Img, prompt) {
36 const isVllm = extension_settings.caption.multimodal_api === 'vllm';36 const isVllm = extension_settings.caption.multimodal_api === 'vllm';
37 const base64Bytes = base64Img.length * 0.75;37 const base64Bytes = base64Img.length * 0.75;
38 const compressionLimit = 2 * 1024 * 1024;38 const compressionLimit = 2 * 1024 * 1024;
39 if ((['google', 'openrouter', 'mistral'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) {39 if ((['google', 'openrouter', 'mistral', 'groq'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) {
40 const maxSide = 1024;40 const maxSide = 1024;
41 base64Img = await createThumbnail(base64Img, maxSide, maxSide, 'image/jpeg');41 base64Img = await createThumbnail(base64Img, maxSide, maxSide, 'image/jpeg');
42 }42 }
@@ -135,6 +135,10 @@ function throwIfInvalidModel(useReverseProxy) {
135 throw new Error('01.AI API key is not set.');135 throw new Error('01.AI API key is not set.');
136 }136 }
137137
138 if (extension_settings.caption.multimodal_api === 'groq' && !secret_state[SECRET_KEYS.GROQ]) {
139 throw new Error('Groq API key is not set.');
140 }
141
138 if (extension_settings.caption.multimodal_api === 'google' && !secret_state[SECRET_KEYS.MAKERSUITE] && !useReverseProxy) {142 if (extension_settings.caption.multimodal_api === 'google' && !secret_state[SECRET_KEYS.MAKERSUITE] && !useReverseProxy) {
139 throw new Error('Google AI Studio API key is not set.');143 throw new Error('Google AI Studio API key is not set.');
140 }144 }
public/scripts/extensions/vectors/index.js+2 -2
@@ -466,13 +466,13 @@ async function ingestDataBankAttachments(source) {
466 }466 }
467467
468 // Download and process the file468 // Download and process the file
469 file.text = await getFileAttachment(file.url);469 const fileText = await getFileAttachment(file.url);
470 console.log(`Vectors: Retrieved file ${file.name} from Data Bank`);470 console.log(`Vectors: Retrieved file ${file.name} from Data Bank`);
471 // Convert kilobytes to string length471 // Convert kilobytes to string length
472 const thresholdLength = settings.size_threshold_db * 1024;472 const thresholdLength = settings.size_threshold_db * 1024;
473 // Use chunk size from settings if file is larger than threshold473 // Use chunk size from settings if file is larger than threshold
474 const chunkSize = file.size > thresholdLength ? settings.chunk_size_db : -1;474 const chunkSize = file.size > thresholdLength ? settings.chunk_size_db : -1;
475 await vectorizeFile(file.text, file.name, collectionId, chunkSize, settings.overlap_percent_db);475 await vectorizeFile(fileText, file.name, collectionId, chunkSize, settings.overlap_percent_db);
476 }476 }
477477
478 return dataBankCollectionIds;478 return dataBankCollectionIds;
public/scripts/openai.js+13 -5
@@ -724,6 +724,12 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
724724
725 if (chatCompletion.canAfford(chatMessage)) {725 if (chatCompletion.canAfford(chatMessage)) {
726 if (type === 'continue' && oai_settings.continue_prefill && chatPrompt === firstNonInjected) {726 if (type === 'continue' && oai_settings.continue_prefill && chatPrompt === firstNonInjected) {
727 // in case we are using continue_prefill and the latest message is an assistant message, we want to prepend the users assistant prefill on the message
728 if (chatPrompt.role === 'assistant') {
729 const collection = new MessageCollection('continuePrefill', new Message(chatMessage.role, substituteParams(oai_settings.assistant_prefill + '\n\n') + chatMessage.content, chatMessage.identifier));
730 chatCompletion.add(collection, -1);
731 continue;
732 }
727 const collection = new MessageCollection('continuePrefill', chatMessage);733 const collection = new MessageCollection('continuePrefill', chatMessage);
728 chatCompletion.add(collection, -1);734 chatCompletion.add(collection, -1);
729 continue;735 continue;
@@ -1767,8 +1773,8 @@ async function sendOpenAIRequest(type, messages, signal) {
1767 generate_data['claude_use_sysprompt'] = oai_settings.claude_use_sysprompt;1773 generate_data['claude_use_sysprompt'] = oai_settings.claude_use_sysprompt;
1768 generate_data['stop'] = getCustomStoppingStrings(); // Claude shouldn't have limits on stop strings.1774 generate_data['stop'] = getCustomStoppingStrings(); // Claude shouldn't have limits on stop strings.
1769 generate_data['human_sysprompt_message'] = substituteParams(oai_settings.human_sysprompt_message);1775 generate_data['human_sysprompt_message'] = substituteParams(oai_settings.human_sysprompt_message);
1770 // Don't add a prefill on quiet gens (summarization)1776 // Don't add a prefill on quiet gens (summarization) and when using continue prefill.
1771 if (!isQuiet) {1777 if (!isQuiet && !(isContinue && oai_settings.continue_prefill)) {
1772 generate_data['assistant_prefill'] = isImpersonate ? substituteParams(oai_settings.assistant_impersonation) : substituteParams(oai_settings.assistant_prefill);1778 generate_data['assistant_prefill'] = isImpersonate ? substituteParams(oai_settings.assistant_impersonation) : substituteParams(oai_settings.assistant_prefill);
1773 }1779 }
1774 }1780 }
@@ -4000,8 +4006,6 @@ async function onModelChange() {
4000 $('#openai_max_context').attr('max', max_2mil);4006 $('#openai_max_context').attr('max', max_2mil);
4001 } else if (value.includes('gemini-1.5-pro')) {4007 } else if (value.includes('gemini-1.5-pro')) {
4002 $('#openai_max_context').attr('max', max_2mil);4008 $('#openai_max_context').attr('max', max_2mil);
4003 } else if (value.match('gemini-1.5-flash-002')) {
4004 $('#openai_max_context').attr('max', max_2mil);
4005 } else if (value.includes('gemini-1.5-flash')) {4009 } else if (value.includes('gemini-1.5-flash')) {
4006 $('#openai_max_context').attr('max', max_1mil);4010 $('#openai_max_context').attr('max', max_1mil);
4007 } else if (value.includes('gemini-1.0-pro-vision') || value === 'gemini-pro-vision') {4011 } else if (value.includes('gemini-1.0-pro-vision') || value === 'gemini-pro-vision') {
@@ -4187,7 +4191,10 @@ async function onModelChange() {
4187 if (oai_settings.max_context_unlocked) {4191 if (oai_settings.max_context_unlocked) {
4188 $('#openai_max_context').attr('max', unlocked_max);4192 $('#openai_max_context').attr('max', unlocked_max);
4189 }4193 }
4190 else if (oai_settings.groq_model.includes('llama-3.1')) {4194 else if (oai_settings.groq_model.includes('llama-3.2') && oai_settings.groq_model.includes('-preview')) {
4195 $('#openai_max_context').attr('max', max_8k);
4196 }
4197 else if (oai_settings.groq_model.includes('llama-3.2') || oai_settings.groq_model.includes('llama-3.1')) {
4191 $('#openai_max_context').attr('max', max_128k);4198 $('#openai_max_context').attr('max', max_128k);
4192 }4199 }
4193 else if (oai_settings.groq_model.includes('llama3-groq')) {4200 else if (oai_settings.groq_model.includes('llama3-groq')) {
@@ -4650,6 +4657,7 @@ export function isImageInliningSupported() {
4650 'gemini-1.5-flash-001',4657 'gemini-1.5-flash-001',
4651 'gemini-1.5-flash-002',4658 'gemini-1.5-flash-002',
4652 'gemini-1.5-flash-exp-0827',4659 'gemini-1.5-flash-exp-0827',
4660 'gemini-1.5-flash-8b',
4653 'gemini-1.5-flash-8b-exp-0827',4661 'gemini-1.5-flash-8b-exp-0827',
4654 'gemini-1.5-flash-8b-exp-0924',4662 'gemini-1.5-flash-8b-exp-0924',
4655 'gemini-1.0-pro-vision-latest',4663 'gemini-1.0-pro-vision-latest',
public/scripts/popup.js+5 -0
@@ -1,3 +1,4 @@
1import dialogPolyfill from '../lib/dialog-polyfill.esm.js';
1import { shouldSendOnEnter } from './RossAscends-mods.js';2import { shouldSendOnEnter } from './RossAscends-mods.js';
2import { power_user } from './power-user.js';3import { power_user } from './power-user.js';
3import { removeFromArray, runAfterAnimation, uuidv4 } from './utils.js';4import { removeFromArray, runAfterAnimation, uuidv4 } from './utils.js';
@@ -178,6 +179,10 @@ export class Popup {
178 const template = document.querySelector('#popup_template');179 const template = document.querySelector('#popup_template');
179 // @ts-ignore180 // @ts-ignore
180 this.dlg = template.content.cloneNode(true).querySelector('.popup');181 this.dlg = template.content.cloneNode(true).querySelector('.popup');
182 if (!this.dlg.showModal) {
183 this.dlg.classList.add('poly_dialog');
184 dialogPolyfill.registerDialog(this.dlg);
185 }
181 this.body = this.dlg.querySelector('.popup-body');186 this.body = this.dlg.querySelector('.popup-body');
182 this.content = this.dlg.querySelector('.popup-content');187 this.content = this.dlg.querySelector('.popup-content');
183 this.mainInput = this.dlg.querySelector('.popup-input');188 this.mainInput = this.dlg.querySelector('.popup-input');
public/scripts/power-user.js+21 -0
@@ -290,6 +290,7 @@ let power_user = {
290 restore_user_input: true,290 restore_user_input: true,
291 reduced_motion: false,291 reduced_motion: false,
292 compact_input_area: true,292 compact_input_area: true,
293 show_swipe_num_all_messages: false,
293 auto_connect: false,294 auto_connect: false,
294 auto_load_chat: false,295 auto_load_chat: false,
295 forbid_external_media: true,296 forbid_external_media: true,
@@ -469,6 +470,11 @@ function switchCompactInputArea() {
469 $('#compact_input_area').prop('checked', power_user.compact_input_area);470 $('#compact_input_area').prop('checked', power_user.compact_input_area);
470}471}
471472
473export function switchSwipeNumAllMessages() {
474 $('#show_swipe_num_all_messages').prop('checked', power_user.show_swipe_num_all_messages);
475 $('.mes:not(.last_mes) .swipes-counter').css('opacity', '').toggle(power_user.show_swipe_num_all_messages);
476}
477
472var originalSliderValues = [];478var originalSliderValues = [];
473479
474async function switchLabMode() {480async function switchLabMode() {
@@ -1283,6 +1289,13 @@ function applyTheme(name) {
1283 switchCompactInputArea();1289 switchCompactInputArea();
1284 },1290 },
1285 },1291 },
1292 {
1293 key: 'show_swipe_num_all_messages',
1294 action: () => {
1295 $('#show_swipe_num_all_messages').prop('checked', power_user.show_swipe_num_all_messages);
1296 switchSwipeNumAllMessages();
1297 },
1298 },
1286 ];1299 ];
12871300
1288 for (const { key, selector, type, action } of themeProperties) {1301 for (const { key, selector, type, action } of themeProperties) {
@@ -1352,6 +1365,7 @@ function applyPowerUserSettings() {
1352 switchHideChatAvatars();1365 switchHideChatAvatars();
1353 switchTokenCount();1366 switchTokenCount();
1354 switchMessageActions();1367 switchMessageActions();
1368 switchSwipeNumAllMessages();
1355}1369}
13561370
1357function getExampleMessagesBehavior() {1371function getExampleMessagesBehavior() {
@@ -2296,6 +2310,7 @@ function getThemeObject(name) {
2296 zoomed_avatar_magnification: power_user.zoomed_avatar_magnification,2310 zoomed_avatar_magnification: power_user.zoomed_avatar_magnification,
2297 reduced_motion: power_user.reduced_motion,2311 reduced_motion: power_user.reduced_motion,
2298 compact_input_area: power_user.compact_input_area,2312 compact_input_area: power_user.compact_input_area,
2313 show_swipe_num_all_messages: power_user.show_swipe_num_all_messages,
2299 };2314 };
2300}2315}
23012316
@@ -3755,6 +3770,12 @@ $(document).ready(() => {
3755 saveSettingsDebounced();3770 saveSettingsDebounced();
3756 });3771 });
37573772
3773 $('#show_swipe_num_all_messages').on('input', function () {
3774 power_user.show_swipe_num_all_messages = !!$(this).prop('checked');
3775 switchSwipeNumAllMessages();
3776 saveSettingsDebounced();
3777 });
3778
3758 $('#auto-connect-checkbox').on('input', function () {3779 $('#auto-connect-checkbox').on('input', function () {
3759 power_user.auto_connect = !!$(this).prop('checked');3780 power_user.auto_connect = !!$(this).prop('checked');
3760 saveSettingsDebounced();3781 saveSettingsDebounced();
public/scripts/slash-commands.js+136 -53
@@ -55,7 +55,7 @@ import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockStat
55import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';55import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
56import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';56import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
57import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';57import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
58import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';58import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
59import { registerVariableCommands, resolveVariable } from './variables.js';59import { registerVariableCommands, resolveVariable } from './variables.js';
60import { background_settings } from './backgrounds.js';60import { background_settings } from './backgrounds.js';
61import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';61import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -70,6 +70,7 @@ import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
70import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';70import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
71import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';71import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
72import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';72import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
73import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
73export {74export {
74 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,75 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
75};76};
@@ -242,6 +243,7 @@ export function initDefaultSlashCommands() {
242 SlashCommandParser.addCommandObject(SlashCommand.fromProps({243 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
243 name: 'sendas',244 name: 'sendas',
244 callback: sendMessageAs,245 callback: sendMessageAs,
246 returns: 'Optionally the text of the sent message, if specified in the "return" argument',
245 namedArgumentList: [247 namedArgumentList: [
246 SlashCommandNamedArgument.fromProps({248 SlashCommandNamedArgument.fromProps({
247 name: 'name',249 name: 'name',
@@ -269,6 +271,14 @@ export function initDefaultSlashCommands() {
269 typeList: [ARGUMENT_TYPE.NUMBER],271 typeList: [ARGUMENT_TYPE.NUMBER],
270 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),272 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
271 }),273 }),
274 SlashCommandNamedArgument.fromProps({
275 name: 'return',
276 description: 'The way how you want the return value to be provided',
277 typeList: [ARGUMENT_TYPE.STRING],
278 defaultValue: 'none',
279 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
280 forceEnum: true,
281 }),
272 ],282 ],
273 unnamedArgumentList: [283 unnamedArgumentList: [
274 new SlashCommandArgument(284 new SlashCommandArgument(
@@ -301,6 +311,7 @@ export function initDefaultSlashCommands() {
301 name: 'sys',311 name: 'sys',
302 callback: sendNarratorMessage,312 callback: sendNarratorMessage,
303 aliases: ['nar'],313 aliases: ['nar'],
314 returns: 'Optionally the text of the sent message, if specified in the "return" argument',
304 namedArgumentList: [315 namedArgumentList: [
305 new SlashCommandNamedArgument(316 new SlashCommandNamedArgument(
306 'compact',317 'compact',
@@ -316,6 +327,14 @@ export function initDefaultSlashCommands() {
316 typeList: [ARGUMENT_TYPE.NUMBER],327 typeList: [ARGUMENT_TYPE.NUMBER],
317 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),328 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
318 }),329 }),
330 SlashCommandNamedArgument.fromProps({
331 name: 'return',
332 description: 'The way how you want the return value to be provided',
333 typeList: [ARGUMENT_TYPE.STRING],
334 defaultValue: 'none',
335 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
336 forceEnum: true,
337 }),
319 ],338 ],
320 unnamedArgumentList: [339 unnamedArgumentList: [
321 new SlashCommandArgument(340 new SlashCommandArgument(
@@ -355,6 +374,7 @@ export function initDefaultSlashCommands() {
355 SlashCommandParser.addCommandObject(SlashCommand.fromProps({374 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
356 name: 'comment',375 name: 'comment',
357 callback: sendCommentMessage,376 callback: sendCommentMessage,
377 returns: 'Optionally the text of the sent message, if specified in the "return" argument',
358 namedArgumentList: [378 namedArgumentList: [
359 new SlashCommandNamedArgument(379 new SlashCommandNamedArgument(
360 'compact',380 'compact',
@@ -370,6 +390,14 @@ export function initDefaultSlashCommands() {
370 typeList: [ARGUMENT_TYPE.NUMBER],390 typeList: [ARGUMENT_TYPE.NUMBER],
371 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),391 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
372 }),392 }),
393 SlashCommandNamedArgument.fromProps({
394 name: 'return',
395 description: 'The way how you want the return value to be provided',
396 typeList: [ARGUMENT_TYPE.STRING],
397 defaultValue: 'none',
398 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
399 forceEnum: true,
400 }),
373 ],401 ],
374 unnamedArgumentList: [402 unnamedArgumentList: [
375 new SlashCommandArgument(403 new SlashCommandArgument(
@@ -509,7 +537,7 @@ export function initDefaultSlashCommands() {
509 SlashCommandParser.addCommandObject(SlashCommand.fromProps({537 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
510 name: 'ask',538 name: 'ask',
511 callback: askCharacter,539 callback: askCharacter,
512 returns: 'the generated text',540 returns: 'Optionally the text of the sent message, if specified in the "return" argument',
513 namedArgumentList: [541 namedArgumentList: [
514 SlashCommandNamedArgument.fromProps({542 SlashCommandNamedArgument.fromProps({
515 name: 'name',543 name: 'name',
@@ -518,6 +546,14 @@ export function initDefaultSlashCommands() {
518 isRequired: true,546 isRequired: true,
519 enumProvider: commonEnumProviders.characters('character'),547 enumProvider: commonEnumProviders.characters('character'),
520 }),548 }),
549 SlashCommandNamedArgument.fromProps({
550 name: 'return',
551 description: 'The way how you want the return value to be provided',
552 typeList: [ARGUMENT_TYPE.STRING],
553 defaultValue: 'pipe',
554 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
555 forceEnum: true,
556 }),
521 ],557 ],
522 unnamedArgumentList: [558 unnamedArgumentList: [
523 new SlashCommandArgument(559 new SlashCommandArgument(
@@ -556,6 +592,7 @@ export function initDefaultSlashCommands() {
556 SlashCommandParser.addCommandObject(SlashCommand.fromProps({592 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
557 name: 'send',593 name: 'send',
558 callback: sendUserMessageCallback,594 callback: sendUserMessageCallback,
595 returns: 'Optionally the text of the sent message, if specified in the "return" argument',
559 namedArgumentList: [596 namedArgumentList: [
560 new SlashCommandNamedArgument(597 new SlashCommandNamedArgument(
561 'compact',598 'compact',
@@ -578,6 +615,14 @@ export function initDefaultSlashCommands() {
578 defaultValue: '{{user}}',615 defaultValue: '{{user}}',
579 enumProvider: commonEnumProviders.personas,616 enumProvider: commonEnumProviders.personas,
580 }),617 }),
618 SlashCommandNamedArgument.fromProps({
619 name: 'return',
620 description: 'The way how you want the return value to be provided',
621 typeList: [ARGUMENT_TYPE.STRING],
622 defaultValue: 'none',
623 enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
624 forceEnum: true,
625 }),
581 ],626 ],
582 unnamedArgumentList: [627 unnamedArgumentList: [
583 new SlashCommandArgument(628 new SlashCommandArgument(
@@ -1568,12 +1613,21 @@ export function initDefaultSlashCommands() {
1568 SlashCommandParser.addCommandObject(SlashCommand.fromProps({1613 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1569 name: 'listinjects',1614 name: 'listinjects',
1570 callback: listInjectsCallback,1615 callback: listInjectsCallback,
1571 helpString: 'Lists all script injections for the current chat. Displays injects in a popup by default. Use the <code>format</code> argument to change the output format.',1616 helpString: 'Lists all script injections for the current chat. Displays injects in a popup by default. Use the <code>return</code> argument to change the return type.',
1572 returns: 'JSON object of script injections',1617 returns: 'Optionalls the JSON object of script injections',
1573 namedArgumentList: [1618 namedArgumentList: [
1574 SlashCommandNamedArgument.fromProps({1619 SlashCommandNamedArgument.fromProps({
1620 name: 'return',
1621 description: 'The way how you want the return value to be provided',
1622 typeList: [ARGUMENT_TYPE.STRING],
1623 defaultValue: 'popup-html',
1624 enumList: slashCommandReturnHelper.enumList({ allowPipe: false, allowObject: true, allowChat: true, allowPopup: true, allowTextVersion: false }),
1625 forceEnum: true,
1626 }),
1627 // TODO remove some day
1628 SlashCommandNamedArgument.fromProps({
1575 name: 'format',1629 name: 'format',
1576 description: 'output format',1630 description: '!!! DEPRECATED - use "return" instead !!! output format',
1577 typeList: [ARGUMENT_TYPE.STRING],1631 typeList: [ARGUMENT_TYPE.STRING],
1578 isRequired: true,1632 isRequired: true,
1579 forceEnum: true,1633 forceEnum: true,
@@ -1842,37 +1896,43 @@ function injectCallback(args, value) {
1842}1896}
18431897
1844async function listInjectsCallback(args) {1898async function listInjectsCallback(args) {
1899 /** @type {import('./slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
1900 let returnType = args.return;
1901
1902 // Old legacy return type handling
1903 if (args.format) {
1904 toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning');
1845 const type = String(args?.format).toLowerCase().trim();1905 const type = String(args?.format).toLowerCase().trim();
1846 if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) {1906 if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) {
1847 type !== 'none' && toastr.info('No script injections for the current chat');1907 type !== 'none' && toastr.info('No script injections for the current chat');
1848 return JSON.stringify({});
1849 }1908 }
1850
1851 const injects = Object.entries(chat_metadata.script_injects)
1852 .map(([id, inject]) => {
1853 const position = Object.entries(extension_prompt_types);
1854 const positionName = position.find(([_, value]) => value === inject.position)?.[0] ?? 'unknown';
1855 return `* **${id}**: <code>${inject.value}</code> (${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}, role: ${inject.role ?? extension_prompt_roles.SYSTEM})`;
1856 })
1857 .join('\n');
1858
1859 const converter = new showdown.Converter();
1860 const messageText = `### Script injections:\n${injects}`;
1861 const htmlMessage = DOMPurify.sanitize(converter.makeHtml(messageText));
1862
1863 switch (type) {1909 switch (type) {
1864 case 'none':1910 case 'none':
1911 returnType = 'none';
1865 break;1912 break;
1866 case 'chat':1913 case 'chat':
1867 sendSystemMessage(system_message_types.GENERIC, htmlMessage);1914 returnType = 'chat-html';
1868 break;1915 break;
1869 case 'popup':1916 case 'popup':
1870 default:1917 default:
1871 await callGenericPopup(htmlMessage, POPUP_TYPE.TEXT);1918 returnType = 'popup-html';
1872 break;1919 break;
1873 }1920 }
1921 }
18741922
1875 return JSON.stringify(chat_metadata.script_injects);1923 // Now the actual new return type handling
1924 const buildTextValue = (injects) => {
1925 const injectsStr = Object.entries(injects)
1926 .map(([id, inject]) => {
1927 const position = Object.entries(extension_prompt_types);
1928 const positionName = position.find(([_, value]) => value === inject.position)?.[0] ?? 'unknown';
1929 return `* **${id}**: <code>${inject.value}</code> (${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}, role: ${inject.role ?? extension_prompt_roles.SYSTEM})`;
1930 })
1931 .join('\n');
1932 return `### Script injections:\n${injectsStr || 'No script injections for the current chat'}`;
1933 };
1934
1935 return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', chat_metadata.script_injects ?? {}, { objectToStringFunc: buildTextValue });
1876}1936}
18771937
1878/**1938/**
@@ -2559,7 +2619,7 @@ async function askCharacter(args, text) {
2559 // Not supported in group chats2619 // Not supported in group chats
2560 // TODO: Maybe support group chats?2620 // TODO: Maybe support group chats?
2561 if (selected_group) {2621 if (selected_group) {
2562 toastr.error('Cannot run /ask command in a group chat!');2622 toastr.warning('Cannot run /ask command in a group chat!');
2563 return '';2623 return '';
2564 }2624 }
25652625
@@ -2633,7 +2693,9 @@ async function askCharacter(args, text) {
2633 }2693 }
2634 }2694 }
26352695
2636 return askResult;2696 const message = askResult ? chat[chat.length - 1] : null;
2697
2698 return await slashCommandReturnHelper.doReturn(args.return ?? 'pipe', message, { objectToStringFunc: x => x.mes });
2637}2699}
26382700
2639async function hideMessageCallback(_, arg) {2701async function hideMessageCallback(_, arg) {
@@ -2908,7 +2970,7 @@ function findPersonaByName(name) {
29082970
2909async function sendUserMessageCallback(args, text) {2971async function sendUserMessageCallback(args, text) {
2910 if (!text) {2972 if (!text) {
2911 console.warn('WARN: No text provided for /send command');2973 toastr.warning('You must specify text to send');
2912 return;2974 return;
2913 }2975 }
29142976
@@ -2924,16 +2986,17 @@ async function sendUserMessageCallback(args, text) {
2924 insertAt = chat.length + insertAt;2986 insertAt = chat.length + insertAt;
2925 }2987 }
29262988
2989 let message;
2927 if ('name' in args) {2990 if ('name' in args) {
2928 const name = args.name || '';2991 const name = args.name || '';
2929 const avatar = findPersonaByName(name) || user_avatar;2992 const avatar = findPersonaByName(name) || user_avatar;
2930 await sendMessageAsUser(text, bias, insertAt, compact, name, avatar);2993 message = await sendMessageAsUser(text, bias, insertAt, compact, name, avatar);
2931 }2994 }
2932 else {2995 else {
2933 await sendMessageAsUser(text, bias, insertAt, compact);2996 message = await sendMessageAsUser(text, bias, insertAt, compact);
2934 }2997 }
29352998
2936 return '';2999 return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
2937}3000}
29383001
2939async function deleteMessagesByNameCallback(_, name) {3002async function deleteMessagesByNameCallback(_, name) {
@@ -3221,30 +3284,20 @@ export function getNameAndAvatarForMessage(character, name = null) {
32213284
3222export async function sendMessageAs(args, text) {3285export async function sendMessageAs(args, text) {
3223 if (!text) {3286 if (!text) {
3287 toastr.warning('You must specify text to send as');
3224 return '';3288 return '';
3225 }3289 }
32263290
3227 let name;3291 let name = args.name?.trim();
3228 let mesText;3292 let mesText;
32293293
3230 if (args.name) {3294 if (!name) {
3231 name = args.name.trim();
3232
3233 if (!name && !text) {
3234 toastr.warning('You must specify a name and text to send as');
3235 return '';
3236 }
3237 } else {
3238 const namelessWarningKey = 'sendAsNamelessWarningShown';3295 const namelessWarningKey = 'sendAsNamelessWarningShown';
3239 if (localStorage.getItem(namelessWarningKey) !== 'true') {3296 if (localStorage.getItem(namelessWarningKey) !== 'true') {
3240 toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 });3297 toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 });
3241 localStorage.setItem(namelessWarningKey, 'true');3298 localStorage.setItem(namelessWarningKey, 'true');
3242 }3299 }
3243 name = name2;3300 name = name2;
3244 if (!text) {
3245 toastr.warning('You must specify text to send as');
3246 return '';
3247 }
3248 }3301 }
32493302
3250 mesText = text.trim();3303 mesText = text.trim();
@@ -3321,11 +3374,12 @@ export async function sendMessageAs(args, text) {
3321 await saveChatConditional();3374 await saveChatConditional();
3322 }3375 }
33233376
3324 return '';3377 return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
3325}3378}
33263379
3327export async function sendNarratorMessage(args, text) {3380export async function sendNarratorMessage(args, text) {
3328 if (!text) {3381 if (!text) {
3382 toastr.warning('You must specify text to send');
3329 return '';3383 return '';
3330 }3384 }
33313385
@@ -3374,7 +3428,7 @@ export async function sendNarratorMessage(args, text) {
3374 await saveChatConditional();3428 await saveChatConditional();
3375 }3429 }
33763430
3377 return '';3431 return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
3378}3432}
33793433
3380export async function promptQuietForLoudResponse(who, text) {3434export async function promptQuietForLoudResponse(who, text) {
@@ -3420,6 +3474,7 @@ export async function promptQuietForLoudResponse(who, text) {
34203474
3421async function sendCommentMessage(args, text) {3475async function sendCommentMessage(args, text) {
3422 if (!text) {3476 if (!text) {
3477 toastr.warning('You must specify text to send');
3423 return '';3478 return '';
3424 }3479 }
34253480
@@ -3462,7 +3517,7 @@ async function sendCommentMessage(args, text) {
3462 await saveChatConditional();3517 await saveChatConditional();
3463 }3518 }
34643519
3465 return '';3520 return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
3466}3521}
34673522
3468/**3523/**
@@ -3542,11 +3597,12 @@ function setBackgroundCallback(_, bg) {
3542 * Retrieves the available model options based on the currently selected main API and its subtype3597 * Retrieves the available model options based on the currently selected main API and its subtype
3543 * @param {boolean} quiet - Whether to suppress toasts3598 * @param {boolean} quiet - Whether to suppress toasts
3544 *3599 *
3545 * @returns {{control: HTMLSelectElement, options: HTMLOptionElement[]}?} An array of objects representing the available model options, or null if not supported3600 * @returns {{control: HTMLSelectElement|HTMLInputElement, options: HTMLOptionElement[]}?} An array of objects representing the available model options, or null if not supported
3546 */3601 */
3547function getModelOptions(quiet) {3602function getModelOptions(quiet) {
3548 const nullResult = { control: null, options: null };3603 const nullResult = { control: null, options: null };
3549 const modelSelectMap = [3604 const modelSelectMap = [
3605 { id: 'custom_model_textgenerationwebui', api: 'textgenerationwebui', type: textgen_types.OOBA },
3550 { id: 'model_togetherai_select', api: 'textgenerationwebui', type: textgen_types.TOGETHERAI },3606 { id: 'model_togetherai_select', api: 'textgenerationwebui', type: textgen_types.TOGETHERAI },
3551 { id: 'openrouter_model', api: 'textgenerationwebui', type: textgen_types.OPENROUTER },3607 { id: 'openrouter_model', api: 'textgenerationwebui', type: textgen_types.OPENROUTER },
3552 { id: 'model_infermaticai_select', api: 'textgenerationwebui', type: textgen_types.INFERMATICAI },3608 { id: 'model_infermaticai_select', api: 'textgenerationwebui', type: textgen_types.INFERMATICAI },
@@ -3556,6 +3612,7 @@ function getModelOptions(quiet) {
3556 { id: 'aphrodite_model', api: 'textgenerationwebui', type: textgen_types.APHRODITE },3612 { id: 'aphrodite_model', api: 'textgenerationwebui', type: textgen_types.APHRODITE },
3557 { id: 'ollama_model', api: 'textgenerationwebui', type: textgen_types.OLLAMA },3613 { id: 'ollama_model', api: 'textgenerationwebui', type: textgen_types.OLLAMA },
3558 { id: 'tabby_model', api: 'textgenerationwebui', type: textgen_types.TABBY },3614 { id: 'tabby_model', api: 'textgenerationwebui', type: textgen_types.TABBY },
3615 { id: 'featherless_model', api: 'textgenerationwebui', type: textgen_types.FEATHERLESS },
3559 { id: 'model_openai_select', api: 'openai', type: chat_completion_sources.OPENAI },3616 { id: 'model_openai_select', api: 'openai', type: chat_completion_sources.OPENAI },
3560 { id: 'model_claude_select', api: 'openai', type: chat_completion_sources.CLAUDE },3617 { id: 'model_claude_select', api: 'openai', type: chat_completion_sources.CLAUDE },
3561 { id: 'model_windowai_select', api: 'openai', type: chat_completion_sources.WINDOWAI },3618 { id: 'model_windowai_select', api: 'openai', type: chat_completion_sources.WINDOWAI },
@@ -3563,7 +3620,7 @@ function getModelOptions(quiet) {
3563 { id: 'model_ai21_select', api: 'openai', type: chat_completion_sources.AI21 },3620 { id: 'model_ai21_select', api: 'openai', type: chat_completion_sources.AI21 },
3564 { id: 'model_google_select', api: 'openai', type: chat_completion_sources.MAKERSUITE },3621 { id: 'model_google_select', api: 'openai', type: chat_completion_sources.MAKERSUITE },
3565 { id: 'model_mistralai_select', api: 'openai', type: chat_completion_sources.MISTRALAI },3622 { id: 'model_mistralai_select', api: 'openai', type: chat_completion_sources.MISTRALAI },
3566 { id: 'model_custom_select', api: 'openai', type: chat_completion_sources.CUSTOM },3623 { id: 'custom_model_id', api: 'openai', type: chat_completion_sources.CUSTOM },
3567 { id: 'model_cohere_select', api: 'openai', type: chat_completion_sources.COHERE },3624 { id: 'model_cohere_select', api: 'openai', type: chat_completion_sources.COHERE },
3568 { id: 'model_perplexity_select', api: 'openai', type: chat_completion_sources.PERPLEXITY },3625 { id: 'model_perplexity_select', api: 'openai', type: chat_completion_sources.PERPLEXITY },
3569 { id: 'model_groq_select', api: 'openai', type: chat_completion_sources.GROQ },3626 { id: 'model_groq_select', api: 'openai', type: chat_completion_sources.GROQ },
@@ -3594,12 +3651,31 @@ function getModelOptions(quiet) {
35943651
3595 const modelSelectControl = document.getElementById(modelSelectItem);3652 const modelSelectControl = document.getElementById(modelSelectItem);
35963653
3597 if (!(modelSelectControl instanceof HTMLSelectElement)) {3654 if (!(modelSelectControl instanceof HTMLSelectElement) && !(modelSelectControl instanceof HTMLInputElement)) {
3598 !quiet && toastr.error(`Model select control not found: ${main_api}[${apiSubType}]`);3655 !quiet && toastr.error(`Model select control not found: ${main_api}[${apiSubType}]`);
3599 return nullResult;3656 return nullResult;
3600 }3657 }
36013658
3602 const options = Array.from(modelSelectControl.options).filter(x => x.value);3659 /**
3660 * Get options from a HTMLSelectElement or HTMLInputElement with a list.
3661 * @param {HTMLSelectElement | HTMLInputElement} control Control containing the options
3662 * @returns {HTMLOptionElement[]} Array of options
3663 */
3664 const getOptions = (control) => {
3665 if (control instanceof HTMLSelectElement) {
3666 return Array.from(control.options);
3667 }
3668
3669 const valueOption = new Option(control.value, control.value);
3670
3671 if (control instanceof HTMLInputElement && control.list instanceof HTMLDataListElement) {
3672 return [valueOption, ...Array.from(control.list.options)];
3673 }
3674
3675 return [valueOption];
3676 };
3677
3678 const options = getOptions(modelSelectControl).filter(x => x.value).filter(onlyUnique);
3603 return { control: modelSelectControl, options };3679 return { control: modelSelectControl, options };
3604}3680}
36053681
@@ -3618,11 +3694,6 @@ function modelCallback(args, model) {
3618 return '';3694 return '';
3619 }3695 }
36203696
3621 if (!options.length) {
3622 !quiet && toastr.warning('No model options found. Check your API settings.');
3623 return '';
3624 }
3625
3626 model = String(model || '').trim();3697 model = String(model || '').trim();
36273698
3628 if (!model) {3699 if (!model) {
@@ -3631,6 +3702,18 @@ function modelCallback(args, model) {
36313702
3632 console.log('Set model to ' + model);3703 console.log('Set model to ' + model);
36333704
3705 if (modelSelectControl instanceof HTMLInputElement) {
3706 modelSelectControl.value = model;
3707 $(modelSelectControl).trigger('input');
3708 !quiet && toastr.success(`Model set to "${model}"`);
3709 return model;
3710 }
3711
3712 if (!options.length) {
3713 !quiet && toastr.warning('No model options found. Check your API settings.');
3714 return '';
3715 }
3716
3634 let newSelectedOption = null;3717 let newSelectedOption = null;
36353718
3636 const fuse = new Fuse(options, { keys: ['text', 'value'] });3719 const fuse = new Fuse(options, { keys: ['text', 'value'] });
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+1 -0
@@ -36,6 +36,7 @@ export const enumIcons = {
36 message: '💬',36 message: '💬',
37 voice: '🎤',37 voice: '🎤',
38 server: '🖥️',38 server: '🖥️',
39 popup: '🗔',
3940
40 true: '✔️',41 true: '✔️',
41 false: '❌',42 false: '❌',
public/scripts/slash-commands/SlashCommandReturnHelper.js+80 -0
@@ -0,0 +1,80 @@
1import { sendSystemMessage, system_message_types } from '../../script.js';
2import { callGenericPopup, POPUP_TYPE } from '../popup.js';
3import { escapeHtml } from '../utils.js';
4import { enumIcons } from './SlashCommandCommonEnumsProvider.js';
5import { enumTypes, SlashCommandEnumValue } from './SlashCommandEnumValue.js';
6
7/** @typedef {'pipe'|'object'|'chat-html'|'chat-text'|'popup-html'|'popup-text'|'toast-html'|'toast-text'|'console'|'none'} SlashCommandReturnType */
8
9export const slashCommandReturnHelper = {
10 // Without this, VSCode formatter fucks up JS docs. Don't ask me why.
11 _: false,
12
13 /**
14 * Gets/creates the enum list of types of return relevant for a slash command
15 *
16 * @param {object} [options={}] Options
17 * @param {boolean} [options.allowPipe=true] Allow option to pipe the return value
18 * @param {boolean} [options.allowObject=false] Allow option to return the value as an object
19 * @param {boolean} [options.allowChat=false] Allow option to return the value as a chat message
20 * @param {boolean} [options.allowPopup=false] Allow option to return the value as a popup
21 * @param {boolean}[options.allowTextVersion=true] Used in combination with chat/popup/toast, some of them do not make sense for text versions, e.g.if you are building a HTML string anyway
22 * @returns {SlashCommandEnumValue[]} The enum list
23 */
24 enumList: ({ allowPipe = true, allowObject = false, allowChat = false, allowPopup = false, allowTextVersion = true } = {}) => [
25 allowPipe && new SlashCommandEnumValue('pipe', 'Return to the pipe for the next command', enumTypes.name, '|'),
26 allowObject && new SlashCommandEnumValue('object', 'Return as an object (or array) to the pipe for the next command', enumTypes.variable, enumIcons.dictionary),
27 allowChat && new SlashCommandEnumValue('chat-html', 'Sending a chat message with the return value - Can display HTML', enumTypes.command, enumIcons.message),
28 allowChat && allowTextVersion && new SlashCommandEnumValue('chat-text', 'Sending a chat message with the return value - Will only display as text', enumTypes.qr, enumIcons.message),
29 allowPopup && new SlashCommandEnumValue('popup-html', 'Showing as a popup with the return value - Can display HTML', enumTypes.command, enumIcons.popup),
30 allowPopup && allowTextVersion && new SlashCommandEnumValue('popup-text', 'Showing as a popup with the return value - Will only display as text', enumTypes.qr, enumIcons.popup),
31 new SlashCommandEnumValue('toast-html', 'Show the return value as a toast notification - Can display HTML', enumTypes.command, 'ℹ️'),
32 allowTextVersion && new SlashCommandEnumValue('toast-text', 'Show the return value as a toast notification - Will only display as text', enumTypes.qr, 'ℹ️'),
33 new SlashCommandEnumValue('console', 'Log the return value (object, if it can be one) to the console', enumTypes.enum, '>'),
34 new SlashCommandEnumValue('none', 'No return value'),
35 ].filter(x => !!x),
36
37 /**
38 * Handles the return value based on the specified type
39 *
40 * @param {SlashCommandReturnType} type The type of return
41 * @param {object|number|string} value The value to return
42 * @param {object} [options={}] Options
43 * @param {(o: object) => string} [options.objectToStringFunc=null] Function to convert the object to a string, if object was provided and 'object' was not the chosen return type
44 * @param {(o: object) => string} [options.objectToHtmlFunc=null] Analog to 'objectToStringFunc', which will be used here if not provided - but can do a different string layout if HTML is requested
45 * @returns {Promise<*>} The processed return value
46 */
47 async doReturn(type, value, { objectToStringFunc = o => o?.toString(), objectToHtmlFunc = null } = {}) {
48 const shouldHtml = type.endsWith('html');
49 const actualConverterFunc = shouldHtml && objectToHtmlFunc ? objectToHtmlFunc : objectToStringFunc;
50 const stringValue = typeof value !== 'string' ? actualConverterFunc(value) : value;
51
52 switch (type) {
53 case 'popup-html':
54 case 'popup-text':
55 case 'chat-text':
56 case 'chat-html':
57 case 'toast-text':
58 case 'toast-html': {
59 const htmlOrNotHtml = shouldHtml ? DOMPurify.sanitize((new showdown.Converter()).makeHtml(stringValue)) : escapeHtml(stringValue);
60
61 if (type.startsWith('popup')) await callGenericPopup(htmlOrNotHtml, POPUP_TYPE.TEXT);
62 if (type.startsWith('chat')) sendSystemMessage(system_message_types.GENERIC, htmlOrNotHtml);
63 if (type.startsWith('toast')) toastr.info(htmlOrNotHtml, null, { escapeHtml: !shouldHtml });
64
65 return '';
66 }
67 case 'pipe':
68 return stringValue ?? '';
69 case 'object':
70 return JSON.stringify(value);
71 case 'console':
72 console.info(value);
73 return '';
74 case 'none':
75 return '';
76 default:
77 throw new Error(`Unknown return type: ${type}`);
78 }
79 },
80};
public/scripts/variables.js+42 -22
@@ -11,6 +11,7 @@ import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureR
11import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';11import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
12import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';12import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
13import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';13import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
14import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
14import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';15import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
15import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';16import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';
1617
@@ -305,7 +306,28 @@ export function replaceVariableMacros(input) {
305}306}
306307
307async function listVariablesCallback(args) {308async function listVariablesCallback(args) {
308 const type = String(args?.format || '').toLowerCase().trim() || 'popup';309 /** @type {import('./slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
310 let returnType = args.return;
311
312 // Old legacy return type handling
313 if (args.format) {
314 toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning');
315 const type = String(args?.format).toLowerCase().trim();
316 switch (type) {
317 case 'none':
318 returnType = 'none';
319 break;
320 case 'chat':
321 returnType = 'chat-html';
322 break;
323 case 'popup':
324 default:
325 returnType = 'popup-html';
326 break;
327 }
328 }
329
330 // Now the actual new return type handling
309 const scope = String(args?.scope || '').toLowerCase().trim() || 'all';331 const scope = String(args?.scope || '').toLowerCase().trim() || 'all';
310 if (!chat_metadata.variables) {332 if (!chat_metadata.variables) {
311 chat_metadata.variables = {};333 chat_metadata.variables = {};
@@ -317,35 +339,24 @@ async function listVariablesCallback(args) {
317 const localVariables = includeLocalVariables ? Object.entries(chat_metadata.variables).map(([name, value]) => `${name}: ${value}`) : [];339 const localVariables = includeLocalVariables ? Object.entries(chat_metadata.variables).map(([name, value]) => `${name}: ${value}`) : [];
318 const globalVariables = includeGlobalVariables ? Object.entries(extension_settings.variables.global).map(([name, value]) => `${name}: ${value}`) : [];340 const globalVariables = includeGlobalVariables ? Object.entries(extension_settings.variables.global).map(([name, value]) => `${name}: ${value}`) : [];
319341
320 const jsonVariables = [342 const buildTextValue = (_) => {
321 ...Object.entries(chat_metadata.variables).map(x => ({ key: x[0], value: x[1], scope: 'local' })),
322 ...Object.entries(extension_settings.variables.global).map(x => ({ key: x[0], value: x[1], scope: 'global' })),
323 ];
324
325 const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables';343 const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables';
326 const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables';344 const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables';
327 const chatName = getCurrentChatId();345 const chatName = getCurrentChatId();
328346
329 const converter = new showdown.Converter();
330 const message = [347 const message = [
331 includeLocalVariables ? `### Local variables (${chatName}):\n${localVariablesString}` : '',348 includeLocalVariables ? `### Local variables (${chatName}):\n${localVariablesString}` : '',
332 includeGlobalVariables ? `### Global variables:\n${globalVariablesString}` : '',349 includeGlobalVariables ? `### Global variables:\n${globalVariablesString}` : '',
333 ].filter(x => x).join('\n\n');350 ].filter(x => x).join('\n\n');
334 const htmlMessage = DOMPurify.sanitize(converter.makeHtml(message));351 return message;
352 };
335353
336 switch (type) {354 const jsonVariables = [
337 case 'none':355 ...Object.entries(chat_metadata.variables).map(x => ({ key: x[0], value: x[1], scope: 'local' })),
338 break;356 ...Object.entries(extension_settings.variables.global).map(x => ({ key: x[0], value: x[1], scope: 'global' })),
339 case 'chat':357 ];
340 sendSystemMessage(system_message_types.GENERIC, htmlMessage);
341 break;
342 case 'popup':
343 default:
344 await callGenericPopup(htmlMessage, POPUP_TYPE.TEXT);
345 break;
346 }
347358
348 return JSON.stringify(jsonVariables);359 return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', jsonVariables, { objectToStringFunc: buildTextValue });
349}360}
350361
351/**362/**
@@ -916,7 +927,7 @@ export function registerVariableCommands() {
916 name: 'listvar',927 name: 'listvar',
917 callback: listVariablesCallback,928 callback: listVariablesCallback,
918 aliases: ['listchatvar'],929 aliases: ['listchatvar'],
919 helpString: 'List registered chat variables. Displays variables in a popup by default. Use the <code>format</code> argument to change the output format.',930 helpString: 'List registered chat variables. Displays variables in a popup by default. Use the <code>return</code> argument to change the return type.',
920 returns: 'JSON list of local variables',931 returns: 'JSON list of local variables',
921 namedArgumentList: [932 namedArgumentList: [
922 SlashCommandNamedArgument.fromProps({933 SlashCommandNamedArgument.fromProps({
@@ -933,8 +944,17 @@ export function registerVariableCommands() {
933 ],944 ],
934 }),945 }),
935 SlashCommandNamedArgument.fromProps({946 SlashCommandNamedArgument.fromProps({
947 name: 'return',
948 description: 'The way how you want the return value to be provided',
949 typeList: [ARGUMENT_TYPE.STRING],
950 defaultValue: 'popup-html',
951 enumList: slashCommandReturnHelper.enumList({ allowPipe: false, allowObject: true, allowChat: true, allowPopup: true, allowTextVersion: false }),
952 forceEnum: true,
953 }),
954 // TODO remove some day
955 SlashCommandNamedArgument.fromProps({
936 name: 'format',956 name: 'format',
937 description: 'output format',957 description: '!!! DEPRECATED - use "return" instead !!! output format',
938 typeList: [ARGUMENT_TYPE.STRING],958 typeList: [ARGUMENT_TYPE.STRING],
939 isRequired: true,959 isRequired: true,
940 forceEnum: true,960 forceEnum: true,
public/style.css+25 -6
@@ -969,34 +969,53 @@ body .panelControlBar {
969969
970/* SWIPE RELATED STYLES*/970/* SWIPE RELATED STYLES*/
971971
972.mes {
973 --swipeCounterHeight: 15px;
974 --swipeCounterMargin: 5px;
975}
976
972.swipe_right,977.swipe_right,
973.swipe_left {978.swipe_left {
974 height: 40px;979 width: 25px;
975 width: 40px;980 height: 25px;
976 opacity: 0.3;981 opacity: 0.3;
977 align-items: center;982 align-items: center;
978 justify-content: center;983 justify-content: center;
979 z-index: 9999;984 z-index: 9999;
980 grid-row-start: 2;985 grid-row-start: 2;
981 font-size: 30px;986 font-size: 20px;
982 cursor: pointer;987 cursor: pointer;
983 align-self: center;988 align-self: center;
989}
990
991.swipe_left {
984 position: absolute;992 position: absolute;
985bottom: 15px;993 bottom: calc(var(--swipeCounterHeight) + var(--swipeCounterMargin));
986 flex-flow: column;994 flex-flow: column;
987}995}
988996
997.swipeRightBlock {
998 position: absolute;
999 right: 0;
1000 bottom: 0;
1001}
9891002
990.swipes-counter {1003.swipes-counter {
991 color: var(--SmartThemeBodyColor);1004 color: var(--SmartThemeBodyColor);
992 font-size: 12px;1005 font-size: 12px;
993 padding: 0;1006 padding: 0 5px;
994 font-family: var(--mainFontFamily);1007 font-family: var(--mainFontFamily);
995 font-weight: 400;1008 font-weight: 400;
996 align-self: center;1009 align-self: center;
997 min-width: 40px;1010 min-width: 40px;
998 display: flex;1011 display: flex;
999 justify-content: center;1012 justify-content: center;
1013 margin-bottom: var(--swipeCounterMargin);
1014 height: var(--swipeCounterHeight);
1015}
1016
1017.mes:not(.last_mes) .swipes-counter {
1018 opacity: 0.3;
1000}1019}
10011020
1002.swipe_left {1021.swipe_left {
@@ -1006,7 +1025,7 @@ flex-flow: column;
10061025
1007.swipe_right {1026.swipe_right {
1008 right: 5px;1027 right: 5px;
1009 align-self:end;1028 align-self: center;
1010}1029}
10111030
1012.ui-settings {1031.ui-settings {
src/endpoints/backends/chat-completions.js+6 -2
@@ -107,6 +107,7 @@ async function sendClaudeRequest(request, response) {
107 }107 }
108108
109 const requestBody = {109 const requestBody = {
110 /** @type {any} */ system: '',
110 messages: convertedPrompt.messages,111 messages: convertedPrompt.messages,
111 model: request.body.model,112 model: request.body.model,
112 max_tokens: request.body.max_tokens,113 max_tokens: request.body.max_tokens,
@@ -120,6 +121,8 @@ async function sendClaudeRequest(request, response) {
120 requestBody.system = enableSystemPromptCache121 requestBody.system = enableSystemPromptCache
121 ? [{ type: 'text', text: convertedPrompt.systemPrompt, cache_control: { type: 'ephemeral' } }]122 ? [{ type: 'text', text: convertedPrompt.systemPrompt, cache_control: { type: 'ephemeral' } }]
122 : convertedPrompt.systemPrompt;123 : convertedPrompt.systemPrompt;
124 } else {
125 delete requestBody.system;
123 }126 }
124 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {127 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
125 // Claude doesn't do prefills on function calls, and doesn't allow empty messages128 // Claude doesn't do prefills on function calls, and doesn't allow empty messages
@@ -156,12 +159,13 @@ async function sendClaudeRequest(request, response) {
156 forwardFetchResponse(generateResponse, response);159 forwardFetchResponse(generateResponse, response);
157 } else {160 } else {
158 if (!generateResponse.ok) {161 if (!generateResponse.ok) {
159 console.log(color.red(`Claude API returned error: ${generateResponse.status} ${generateResponse.statusText}\n${await generateResponse.text()}\n${divider}`));162 const generateResponseText = await generateResponse.text();
163 console.log(color.red(`Claude API returned error: ${generateResponse.status} ${generateResponse.statusText}\n${generateResponseText}\n${divider}`));
160 return response.status(generateResponse.status).send({ error: true });164 return response.status(generateResponse.status).send({ error: true });
161 }165 }
162166
163 const generateResponseJson = await generateResponse.json();167 const generateResponseJson = await generateResponse.json();
164 const responseText = generateResponseJson.content[0].text;168 const responseText = generateResponseJson?.content?.[0]?.text || '';
165 console.log('Claude response:', generateResponseJson);169 console.log('Claude response:', generateResponseJson);
166170
167 // Wrap it back to OAI format + save the original content171 // Wrap it back to OAI format + save the original content
src/endpoints/openai.js+8 -0
@@ -55,6 +55,10 @@ router.post('/caption-image', jsonParser, async (request, response) => {
55 key = readSecret(request.user.directories, SECRET_KEYS.MISTRALAI);55 key = readSecret(request.user.directories, SECRET_KEYS.MISTRALAI);
56 }56 }
5757
58 if (request.body.api === 'groq') {
59 key = readSecret(request.user.directories, SECRET_KEYS.GROQ);
60 }
61
58 if (!key && !request.body.reverse_proxy && ['custom', 'ooba', 'koboldcpp', 'vllm'].includes(request.body.api) === false) {62 if (!key && !request.body.reverse_proxy && ['custom', 'ooba', 'koboldcpp', 'vllm'].includes(request.body.api) === false) {
59 console.log('No key found for API', request.body.api);63 console.log('No key found for API', request.body.api);
60 return response.sendStatus(400);64 return response.sendStatus(400);
@@ -111,6 +115,10 @@ router.post('/caption-image', jsonParser, async (request, response) => {
111 apiUrl = 'https://api.01.ai/v1/chat/completions';115 apiUrl = 'https://api.01.ai/v1/chat/completions';
112 }116 }
113117
118 if (request.body.api === 'groq') {
119 apiUrl = 'https://api.groq.com/openai/v1/chat/completions';
120 }
121
114 if (request.body.api === 'mistral') {122 if (request.body.api === 'mistral') {
115 apiUrl = 'https://api.mistral.ai/v1/chat/completions';123 apiUrl = 'https://api.mistral.ai/v1/chat/completions';
116 }124 }
src/prompt-converters.js+1 -0
@@ -269,6 +269,7 @@ function convertGooglePrompt(messages, model, useSysPrompt = false, charName = '
269 'gemini-1.5-flash-001',269 'gemini-1.5-flash-001',
270 'gemini-1.5-flash-002',270 'gemini-1.5-flash-002',
271 'gemini-1.5-flash-exp-0827',271 'gemini-1.5-flash-exp-0827',
272 'gemini-1.5-flash-8b',
272 'gemini-1.5-flash-8b-exp-0827',273 'gemini-1.5-flash-8b-exp-0827',
273 'gemini-1.5-flash-8b-exp-0924',274 'gemini-1.5-flash-8b-exp-0924',
274 'gemini-1.5-pro',275 'gemini-1.5-pro',