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+
22+SillyTavern 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+
24+Wir 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+
28+SillyTavern (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+
30+Angefangen 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+
34+1. 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!
35+2. Wir bieten weder Online- oder gehosteten Dienste an, noch verfolgen wir programmgesteuert Benutzerdaten.
36+3. 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+
40+SillyTavern 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+
45+Wenn du nicht vertraut mit der Verwendung der git CLI bist oder nicht verstehst, was ein Branch ist, mach dir keine Sorgen! Der Release-Branch ist immer die bevorzugte Option für dich.
46+
47+## Was brauche ich zusätzlich zu SillyTavern?
48+
49+Da SillyTavern nur eine Benutzeroberfläche ist, benötigst du Zugriff auf ein LLM-Backend, um Inferenz bereitzustellen. Du kannst AI Horde für sofortiges Chatten ohne weitere Einrichtung verwenden. Darüber hinaus unterstützen wir viele andere lokale und cloudbasierte LLM-Backends: OpenAI-kompatible API, KoboldAI, Tabby und viele mehr. Du kannst mehr über unsere unterstützten APIs in [der FAQ](https://docs.sillytavern.app/usage/api-connections/) lesen.
50+
51+### Brauche ich einen leistungsstarken PC, um SillyTavern auszuführen?
52+
53+Die Hardwareanforderungen sind minimal: Es läuft auf allem, was NodeJS 18 oder höher ausführen kann. Wenn du LLM-Inferenz auf deinem lokalen Rechner durchführen möchtest, empfehlen wir eine NVIDIA-Grafikkarte der 3000er-Serie mit mindestens 6 GB VRAM. Überprüfe die Dokumentation deines Backends für weitere Einzelheiten.
54+
55+### Vorgeschlagene Backends (keine Partnerschaft oder Werbebeziehung)
56+
57+* [AI Horde](https://aihorde.net/) - verwende Modelle, die von Freiwilligen gehostet werden. Erfordert keine weitere Einrichtung
58+* [KoboldCpp](https://github.com/LostRuins/koboldcpp) - ein Favorit der Community, um GGUF-Modelle lokal auszuführen
59+* [tabbyAPI](https://github.com/theroyallab/tabbyAPI) - eine beliebte, portable, speicherplatzoptimierte und lokal gehostete exl2 Inferenz-API
60+* [OpenRouter](https://openrouter.ai) - eine einzige API für viele Cloud-Anbieter (OpenAI, Claude, Meta Llama usw.) sowie beliebte Community-Modelle.
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+
69+Oder 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+
77+1. Sende Pull-Requests. Lerne, wie du beitragen kannst: [CONTRIBUTING.md](../CONTRIBUTING.md)
78+2. Sende Feature Requests und Issues unter Verwendung der bereitgestellten Vorlagen.
79+3. 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+
88+SillyTavern basiert auf dem Konzept der "Charakterkarten". Eine Charakterkarte ist eine Sammlung von Prompts, die das Verhalten des LLM festlegen und erforderlich sind, um persistente Gespräche in SillyTavern zu führen. Sie funktionieren ähnlich wie ChatGPT's GPTs oder Poe's Bots. Der Inhalt einer Charakterkarte kann alles sein: ein abstraktes Szenario, ein Assistent, der für eine bestimmte Aufgabe maßgeschneidert ist, eine berühmte Persönlichkeit oder ein fiktiver Charakter.
89+
90+Das Namensfeld ist der einzige erforderliche Eingabewert für die Charakterkarte. Um ein neutrales Gespräch mit dem LLM zu beginnen, erstelle eine neue Karte, die einfach "Assistent" genannt wird, und lasse die restlichen Felder leer. Für einen thematischeren Chat kannst du dem LLM verschiedene Hintergrundinformationen, Verhaltensweisen und Schreibmuster sowie ein Szenario geben, um das Gespräch zu beginnen.
91+
92+Um ein schnelles Gespräch zu führen, ohne eine Charakterkarte auszuwählen, oder um einfach die LLM-Verbindung zu testen, gib einfach dein Prompt in die Eingabezeile auf dem Willkommensbildschirm ein, nachdem du SillyTavern geöffnet hast. Bitte beachte, dass solche Chats vorübergehend sind und nicht gespeichert werden.
93+
94+Um eine allgemeine Vorstellung davon zu bekommen, wie man Charakterkarten definiert, sieh dir die mitgelieferte Charakterkarte (Seraphina) an oder lade ausgewählte von der Community erstellte Karten im Menü "Erweiterungen & Assets herunterladen" herunter.
95+
96+## Wichtigste Features
97+
98+* Erweiterte Text-Generierungs-Einstellungen mit vielen von der Community erstellten mitgelieferten Einstellungen (Presets)
99+* Unterstützung für World Info: Erstelle reichhaltige Lore oder reduziere die Tokens in deiner Charakterkarte
100+* Gruppenchats: Multi-Bot-Räume für Charaktere, die mit dir und/oder untereinander sprechen
101+* Reichhaltige UI-Anpassungsoptionen: Themes zur Farbenwahl, Hintergrundbilder, benutzerdefiniertes CSS und mehr
102+* Benutzer-Personas: Lass die KI ein wenig über dich wissen, um die Immersion zu erhöhen
103+* Eingebaute RAG-Unterstützung: Füge Dokumente zu deinen Chats hinzu, auf die die KI verweisen kann
104+* Umfangreiches "Chat-Befehle"-System und eigene [Scripting-Engine](https://docs.sillytavern.app/usage/st-script/)
105+
106+## Erweiterungen
107+
108+SillyTavern unterstützt Erweiterungen.
109+
110+* Emotionale Ausdrucksformen von Charakteren (Sprites)
111+* Automatische Zusammenfassung des Chatverlaufs
112+* Automatische UI- und Chat-Übersetzung
113+* Bildgenerierung mit Stable Diffusion/FLUX/DALL-E
114+* Text-to-Speech für KI-Antwortnachrichten (über ElevenLabs, Silero oder die TTS-Funktion des Betriebssystems)
115+* Websuchfunktionen zum Hinzufügen zusätzlicher realer Kontexte zu deinen Eingabeaufforderungen
116+* Viele weitere sind im Menü "Erweiterungen & Assets herunterladen" verfügbar.
117+
118+Tutorials zur Nutzung findest du in der [Dokumentation](https://docs.sillytavern.app/).
119+
120+# ⌛ Installation
121+
122+> \[!WARNING]
123+>
124+> * INSTALLIERE NICHT IN EINEM VON WINDOWS KONTROLLIERTEN ORDNER (Programme, System32 usw.).
125+> * FÜHRE START.BAT NICHT MIT ADMIN-BERECHTIGUNGEN AUS.
126+> * DIE INSTALLATION AUF WINDOWS 7 IST UNMÖGLICH, DA ES NODEJS 18.16 NICHT AUSFÜHREN KANN.
127+
128+## 🪟 Windows
129+
130+### Installation über Git
131+
132+1. Installiere [NodeJS](https://nodejs.org/en) (die neueste LTS-Version wird empfohlen).
133+2. Installiere [Git für Windows](https://gitforwindows.org/).
134+3. Öffne den Windows-Explorer (`Win+E`).
135+4. Gehe zu oder erstelle einen Ordner, der nicht von Windows kontrolliert oder überwacht wird. (z.B.: C:\MySpecialFolder\)
136+5. Öffne ein Eingabeaufforderungsfenster in diesem Ordner, indem du in die 'Adressleiste' oben klickst, `cmd` eingibst und Enter drückst.
137+6. Sobald das schwarze Fenster (Eingabeaufforderung) erscheint, gib EINE der folgenden Optionen ein und drücke Enter:
138+
139+* für den Release-Branch: `git clone https://github.com/SillyTavern/SillyTavern -b release`
140+* für den Staging-Branch: `git clone https://github.com/SillyTavern/SillyTavern -b staging`
141+
142+7. Sobald alles geklont ist, doppelklicke auf `Start.bat`, damit NodeJS seine Anforderungen installiert.
143+8. Der Server wird dann gestartet, und SillyTavern wird in deinem Browser geöffnet.
144+
145+### Installation über GitHub Desktop
146+
147+(Dies ermöglicht die Nutzung von git **nur** in GitHub Desktop. Wenn du `git` auch in der Eingabeaufforderung verwenden möchtest, musst du auch [Git für Windows](https://gitforwindows.org/) installieren.)
148+
149+1. Installiere [NodeJS](https://nodejs.org/en) (die neueste LTS-Version wird empfohlen).
150+2. Installiere [GitHub Desktop](https://central.github.com/deployments/desktop/desktop/latest/win32).
151+3. Klicke nach der Installation von GitHub Desktop auf `Ein Repository aus dem Internet klonen....` (Hinweis: Du **musst kein** GitHub-Konto für diesen Schritt erstellen).
152+4. Klicke im Menü auf den Tab URL, gib diese URL ein `https://github.com/SillyTavern/SillyTavern` und klicke auf Klonen. Du kannst den lokalen Pfad ändern, um festzulegen, wohin SillyTavern heruntergeladen werden soll.
153+5. Um SillyTavern zu öffnen, durchsuche mit dem Windows-Explorer den Ordner, in den du das Repository geklont hast. Standardmäßig wird das Repository hier hin geklont: `C:\Users\[Dein Windows-Benutzername]\Documents\GitHub\SillyTavern`.
154+6. Doppelklicke auf die Datei `start.bat`. (Hinweis: Der Teil `.bat` des Dateinamens könnte von deinem Betriebssystem verborgen sein. In diesem Fall sieht es aus wie eine Datei namens "`Start`". Dies ist die Datei, auf die du doppelklickst, um SillyTavern auszuführen.)
155+7. Nach dem Doppelklicken sollte ein großes schwarzes Konsolenfenster erscheinen, und SillyTavern beginnt, das zu installieren, was es zum Betrieb benötigt.
156+8. Nach dem Installationsprozess sollte das Konsolenfenster so aussehen, und ein SillyTavern-Tab sollte in deinem Browser geöffnet sein.
157+9. Verbinde dich mit einer der [unterstützten APIs](https://docs.sillytavern.app/usage/api-connections/) und beginne zu chatten!
158+
159+## 🐧 Linux & 🍎 MacOS
160+
161+Für MacOS/Linux werden all diese Schritte in einem Terminal durchgeführt.
162+
163+1. Installiere git und nodeJS (die Methode zur Durchführung hängt von deinem Betriebssystem ab).
164+2. Klone das Repository.
165+
166+* für den Release-Branch: `git clone https://github.com/SillyTavern/SillyTavern -b release`
167+* für den Staging-Branch: `git clone https://github.com/SillyTavern/SillyTavern -b staging`
168+
169+3. `cd SillyTavern`, um in den Installationsordner zu navigieren.
170+4. Führe das Skript `start.sh` mit einem dieser Befehle aus:
171+
172+* `./start.sh`
173+* `bash start.sh`
174+
175+## ⚡ Installation über SillyTavern Launcher
176+
177+Der SillyTavern Launcher ist ein Installationsassistent, der dir bei der Einrichtung mit vielen Optionen helfen wird, einschließlich der Installation eines Backends für lokale Inferenz.
178+
179+### Für Windows-Nutzer
180+
181+1. Drücke auf deiner Tastatur **`WINDOWS + R`**, um das Ausführen-Dialogfeld zu öffnen. Führe dann den folgenden Befehl aus, um git zu installieren:
182+
183+```shell
184+cmd /c winget install -e --id Git.Git
185+```
186+
187+2. Drücke auf deiner Tastatur **`WINDOWS + E`**, um den Datei-Explorer zu öffnen, und navigiere dann zu dem Ordner, in dem du den Launcher installieren möchtest. Gib im gewünschten Ordner in die Adressleiste `cmd` ein und drücke Enter. Führe dann den folgenden Befehl aus:
188+
189+```shell
190+git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher && start installer.bat
191+```
192+
193+### Für Linux-Nutzer
194+
195+1. Öffne dein bevorzugtes Terminal und installiere git.
196+2. Klone den SillyTavern-Launcher mit:
197+
198+```shell
199+git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
200+```
201+
202+3. Starte die installer.sh mit:
203+
204+```shell
205+chmod +x install.sh && ./install.sh
206+```
207+
208+4. Nach der Installation starte die launcher.sh mit:
209+
210+```shell
211+chmod +x launcher.sh && ./launcher.sh
212+```
213+
214+### Für Mac-Nutzer
215+
216+1. Öffne ein Terminal und installiere brew mit:
217+
218+```shell
219+/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
220+```
221+
222+2. Installiere git mit:
223+
224+```shell
225+brew install git
226+```
227+
228+3. Klone den SillyTavern-Launcher mit:
229+
230+```shell
231+git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
232+```
233+
234+4. Starte die installer.sh mit:
235+
236+```shell
237+chmod +x install.sh && ./install.sh
238+```
239+
240+5. Nach der Installation starte die launcher.sh mit:
241+
242+```shell
243+chmod +x launcher.sh && ./launcher.sh
244+```
245+
246+## 🐋 Installation über Docker
247+
248+Diese Anweisungen setzen voraus, dass du Docker installiert hast, auf deine Befehlszeile für die Installation von Containern zugreifen kannst und mit deren allgemeiner Funktionsweise vertraut bist.
249+
250+### Image selbst bauen
251+
252+Wir haben einen umfassenden Leitfaden zur Nutzung von SillyTavern in Docker [hier](http://docs.sillytavern.app/installation/docker/) der die Installationen auf Windows, macOS und Linux abdeckt! Lies ihn, wenn du das Image selbst bauen möchtest.
253+
254+### Verwendung der GitHub Container Registry (am einfachsten)
255+
256+Du benötigst zwei zwingende Verzeichniszuordnungen und eine Portzuordnung, um SillyTavern funktionsfähig zu machen. Ersetze in dem Befehl deine Auswahl an den folgenden Stellen:
257+
258+#### Container-Variablen
259+
260+##### Volumen-Zuordnungen
261+
262+* [config] - Das Verzeichnis, in dem die SillyTavern-Konfigurationsdateien auf deinem Host-Computer gespeichert werden
263+* [data] - Das Verzeichnis, in dem die Benutzerdaten von SillyTavern (einschließlich Charaktere) auf deinem Host-Computer gespeichert werden
264+* [plugins] - (optional) Das Verzeichnis, in dem die SillyTavern-Server-Plugins auf deinem Host-Computer gespeichert werden
265+
266+##### Port-Zuordnungen
267+
268+* [PublicPort] - Der Port, über den der Datenverkehr ausgegeben werden soll. Dies ist zwingend erforderlich, da du auf die Instanz von außerhalb des virtuellen Maschinencontainers zugreifst. EXPOSIERE DIES NICHT IM INTERNET, OHNE EINEN GETRENNTEN SERVICE FÜR DIE SICHERHEIT ZU IMPLEMENTIEREN.
269+
270+##### Zusätzliche Einstellungen
271+
272+* [DockerNet] - Das Docker-Netzwerk, mit dem der Container erstellt werden soll. Wenn du nicht weißt, was das ist, sieh dir die [offizielle Docker-Dokumentation](https://docs.docker.com/reference/cli/docker/network/) an.
273+* [version] - Auf der rechten Seite dieser GitHub-Seite siehst du "Packages". Wähle das Paket "sillytavern" und du siehst die Imageversionen. Das Image-Tag "latest" hält dich auf dem Laufenden mit dem aktuellen Release. Du kannst auch "staging" und "release" Tags nutzen, die auf die nightly images der jeweiligen Branche verweisen, aber das könnte unangemessen sein, wenn du Erweiterungen verwendest, die möglicherweise kaputt sind und Zeit benötigen, um aktualisiert zu werden.
274+
275+#### Installationsbefehl
276+
277+1. Öffne deine Befehlszeile.
278+2. Führe den folgenden Befehl aus:
279+
280+`docker create --name='sillytavern' --net='[DockerNet]' -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'`
281+
282+> Beachte, dass 8000 ein Standard-Listening-Port ist. Vergiss nicht, einen geeigneten Port zu verwenden, wenn du ihn in der Konfiguration änderst.
283+
284+## 📱 Installation über Termux auf Android OS
285+
286+> \[!HINWEIS]
287+> **SillyTavern kann nativ auf Android-Geräten über Termux ausgeführt werden, aber wir bieten keine offizielle Unterstützung für diesen Anwendungsfall.**
288+>
289+> **Bitte beziehe dich auf diesen Leitfaden von ArroganceComplex#2659:**
290+>
291+> * <https://rentry.org/STAI-Termux>
292+
293+**Nicht unterstützte Plattform: android arm LEtime-web.** 32-Bit-Android benötigt eine externe Abhängigkeit, die nicht mit npm installiert werden kann. Verwende den folgenden Befehl, um sie zu installieren: `pkg install esbuild`. Führe dann die üblichen Installationsschritte aus.
294+
295+## API-Schlüsselverwaltung
296+
297+SillyTavern speichert deine API-Schlüssel in einer Datei `secrets.json` im Benutzerdatenverzeichnis (`/data/default-user/secrets.json` ist der Standardpfad).
298+
299+Standardmäßig sind API-Schlüssel nach dem Speichern und Aktualisieren der Seite nicht mehr über die Benutzeroberfläche sichtbar.
300+
301+Um die Ansicht deiner Schlüssel zu aktivieren:
302+
303+1. Setze den Wert von `allowKeysExposure` auf `true` in der Datei `config.yaml`.
304+2. Starte den SillyTavern-Server neu.
305+3. Klicke auf den Link 'Verborgene API-Schlüssel anzeigen' in der unteren rechten Ecke des API-Verbindungsfeldes.
306+
307+## Befehlszeilenargumente
308+
309+Sie können Befehlszeilenargumente an den Start des SillyTavern-Servers übergeben, um einige Einstellungen in `config.yaml` zu überschreiben.
310+
311+### Beispiele
312+
313+```shell
314+node server.js --port 8000 --listen false
315+# oder
316+npm run start -- --port 8000 --listen false
317+# oder (nur Windows)
318+Start.bat --port 8000 --listen false
319+```
320+
321+### Unterstützte Argumente
322+
323+| Option | Beschreibung | Typ |
324+|-------------------------|----------------------------------------------------------------------------------------------------|----------|
325+| `--version` | Versionsnummer anzeigen | boolean |
326+| `--enableIPv6` | Aktiviert IPv6. | boolean |
327+| `--enableIPv4` | Aktiviert IPv4. | boolean |
328+| `--port` | Legt den Port fest, unter dem SillyTavern ausgeführt wird. 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+
349+Dies ist in den meisten Fällen für Personen gedacht, die SillyTavern auf ihren Mobiltelefonen verwenden möchten, während ihr PC den ST-Server im selben WLAN-Netzwerk betreibt. Es kann jedoch auch verwendet werden, um Remoteverbindungen von überall her zu ermöglichen.
350+
351+Lies die ausführliche Anleitung zum Einrichten von Remoteverbindungen in den [Docs](https://docs.sillytavern.app/usage/remoteconnections/).
352+
353+Möglicherweise möchtest du SillyTavern-Benutzerprofile auch mit (optionalem) Kennwortschutz konfigurieren: [Benutzer](https://docs.sillytavern.app/installation/st-1.12.0-migration-guide/#users).
354+
355+## Leistungsprobleme?
356+
357+1. Deaktiviere den Unschärfeeffekt und aktiviere "Verringerte Bewegung" im Bedienfeld "Benutzereinstellungen" (UI-Design schaltet Kategorie um).
358+2. Wenn du Response Streaming verwendest, stelle die Streaming-FPS auf einen niedrigeren Wert ein (10-15 FPS werden empfohlen).
359+3. 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,
364+aber OHNE JEGLICHE GARANTIE; nicht einmal die stillschweigende Garantie der
365+MARKTFÄHIGKEIT oder EIGNUNG FÜR EINEN BESTIMMTEN ZWECK. Siehe die
366+GNU Affero General Public License für weitere Details.**
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
66<div align="center">
77
88[English](readme.md) | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | 日本語 | [Русский](readme-ru_ru.md)
99
1010[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
1111[![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
88<div align="center">
99
1010[English](readme.md) | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | [日本語](readme-ja_jp.md) | Русский
1111
1212[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
1313[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)
.github/readme-zh_cn.md+1 -1
@@ -5,7 +5,7 @@
55
66<div align="center">
77
88[English](readme.md) | [German](readme-de_de.md) | 中文 | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)
99
1010[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
1111[![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
55<div align="center">
66
77English | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)
88
99[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
1010[![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');
12@import url('./popup-safari-fix.css');
23
34dialog {
@@ -42,7 +43,7 @@ dialog {
4243 display: flex;
4344 flex-direction: column;
4445 overflow: hidden;
4546 width: min(100%, 100vw);
4647 height: 100%;
4748 padding: 1px;
4849}
public/index.html+36 -18
@@ -1709,9 +1709,9 @@
17091709 <div class="">
17101710 <div class="inline-drawer wide100p flexFlowColumn">
17111711 <div class="inline-drawer-toggle inline-drawer-header">
17121712 <div class="flex-container alignItemsCenter flexNoGap">
17131713 <b data-i18n="Character Names Behavior">Character Names Behavior</b>
17141714 <span title="Helps the model to associate messages with characters." data-i18n="[title]Helps the model to associate messages with characters." class="note-link-spanopacity50p fa-solid fa-circle-questioninfo"></span>
17151715 <small class="flexBasis100p">(<span id="character_names_display"></span>)</small>
17161716 </div>
17171717 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
@@ -1720,25 +1720,22 @@
17201720 <label class="checkbox_label flexWrap alignItemsCenter" for="character_names_none">
17211721 <input type="radio" id="character_names_none" name="character_names" value="-1">
17221722 <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.
17261725 </small>
17271726 </label>
17281727 <label class="checkbox_label flexWrap alignItemsCenter" for="character_names_default">
17291728 <input type="radio" id="character_names_default" name="character_names" value="0">
17301729 <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.
17341732 </small>
17351733 </label>
17361734 <label class="checkbox_label flexWrap alignItemsCenter" for="character_names_completion">
17371735 <input type="radio" id="character_names_completion" name="character_names" value="1">
17381736 <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.
17421739 </small>
17431740 </label>
17441741 <label class="checkbox_label flexWrap alignItemsCenter" for="character_names_content">
@@ -1754,9 +1751,9 @@
17541751 </div>
17551752 <div class="inline-drawer wide100p flexFlowColumn marginBot10">
17561753 <div class="inline-drawer-toggle inline-drawer-header">
17571754 <div class="flex-container alignItemsCenter flexNoGap">
17581755 <b data-i18n="Continue Postfix">Continue Postfix</b>
17591756 <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-spanopacity50p fa-solid fa-circle-questioninfo"></span>
17601757 <small class="flexBasis100p">(<span id="continue_postfix_display"></span>)</small>
17611758 </div>
17621759 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
@@ -2891,6 +2888,7 @@
28912888 <option value="gemini-1.5-pro-latest">Gemini 1.5 Pro [latest]</option>
28922889 <option value="gemini-1.5-pro-001">Gemini 1.5 Pro [001]</option>
28932890 <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>
28942892 <option value="gemini-1.5-flash-exp-0827">Gemini 1.5 Flash Experiment 2024-08-27</option>
28952893 <option value="gemini-1.5-flash-8b-exp-0827">Gemini 1.5 Flash 8B Experiment 2024-08-27</option>
28962894 <option value="gemini-1.5-flash-8b-exp-0924">Gemini 1.5 Flash 8B Experiment 2024-09-24</option>
@@ -2957,16 +2955,31 @@
29572955 </div>
29582956 <h4 data-i18n="Groq Model">Groq Model</h4>
29592957 <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">
29612959 <option value="llama-3.12-70b1b-versatilepreview">llama-3.12-70b1b-versatilepreview</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">
29622965 <option value="llama-3.1-8b-instant">llama-3.1-8b-instant</option>
29632966 <option value="llama3llama-groq3.1-70b-8192-tool-use-previewversatile">llama3llama-groq3.1-70b-8192-tool-use-previewversatile</option>
2967+ <option value="llama-3.1-405b-reasoning">llama-3.1-405b-reasoning</option>
2968+ </optgroup>
2969+ <optgroup label="Llama 3">
29642970 <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>
29652972 <option value="llama3-8b-8192">llama3-8b-8192</option>
29662973 <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">
29682976 <option value="gemma-7b-it">gemma-7b-it</option>
29692977 <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>
29702983 </select>
29712984 </div>
29722985 <div id="perplexity_form" data-source="perplexity">
@@ -4013,6 +4026,10 @@
40134026 <input id="compact_input_area" type="checkbox" />
40144027 <small data-i18n="Compact Input Area (Mobile)">Compact Input Area</small><i class="fa-solid fa-mobile-screen-button"></i>
40154028 </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>
40164033 <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">
40174034 <input id="hotswapEnabled" type="checkbox" />
40184035 <small data-i18n="Characters Hotswap">Characters Hotswap</small>
@@ -5941,7 +5958,8 @@
59415958 </div>
59425959 <div class="mes_bias"></div>
59435960 </div>
59445961 <div class="swipe_right faflex-solidcontainer fa-chevron-right"swipeRightBlock style="display:flexFlowColumn none;flexNoGap">
5962+ <div class="swipe_right fa-solid fa-chevron-right" style="display: none;"></div>
59455963 <div class="swipes-counter"></div>
59465964 </div>
59475965 </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+}
37 \ 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_.
2+var supportCustomEvent = window.CustomEvent;
3+if (!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+ */
21+function 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+ */
33+function 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+ */
62+function 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+ */
85+function 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+ */
101+function 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+ */
114+function 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+ */
125+function 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+ */
158+function isConnected(element) {
159+ return element.isConnected || document.body.contains(element);
160+}
161+
162+/**
163+ * @param {!Event} event
164+ * @return {?Element}
165+ */
166+function 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+ */
192+function 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+ */
231+function 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+
290+dialogPolyfillInfo.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+
473+var dialogPolyfill = {};
474+
475+dialogPolyfill.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+
481+dialogPolyfill.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+
510+dialogPolyfill.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+ */
530+dialogPolyfill.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+ */
544+dialogPolyfill.registerDialog = function(element) {
545+ if (!element.showModal) {
546+ dialogPolyfill.forceRegisterDialog(element);
547+ }
548+};
549+
550+/**
551+ * @constructor
552+ */
553+dialogPolyfill.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+ */
602+dialogPolyfill.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+ */
612+dialogPolyfill.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+ */
621+dialogPolyfill.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+ */
645+dialogPolyfill.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+
657+dialogPolyfill.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+
686+dialogPolyfill.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+ */
710+dialogPolyfill.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+ */
728+dialogPolyfill.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+ */
743+dialogPolyfill.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+
754+dialogPolyfill.dm = new dialogPolyfill.DialogManager();
755+dialogPolyfill.formSubmitter = null;
756+dialogPolyfill.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+ */
762+if (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+
858+export default dialogPolyfill;
public/script.js+54 -24
@@ -83,6 +83,7 @@ import {
8383 resetMovableStyles,
8484 forceCharacterEditorTokenize,
8585 applyPowerUserSettings,
86+ switchSwipeNumAllMessages,
8687} from './scripts/power-user.js';
8788
8889import {
@@ -1884,6 +1885,7 @@ export async function reloadCurrentChat() {
18841885export async function sendTextareaMessage() {
18851886 if (is_send_press) return;
18861887 if (isExecutingCommandsFromChatInput) return;
1888+ if (this_edit_mes_id) return; // don't proceed if editing a message
18871889
18881890 let generateType;
18891891 // "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
23702372 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+
23722381 if (showSwipes) {
23732382 $('#chat .mes').last().addClass('last_mes');
23742383 $('#chat .mes').eq(-2).removeClass('last_mes');
@@ -4785,7 +4794,7 @@ export function removeMacros(str) {
47854794 * @param {boolean} [compact] Send as a compact display message.
47864795 * @param {string} [name] Name of the user sending the message. Defaults to name1.
47874796 * @param {string} [avatar] Avatar of the user sending the message. Defaults to user_avatar.
47884797 * @returns {Promise<voidany>} A promise that resolves whento the message when it is inserted.
47894798 */
47904799export async function sendMessageAsUser(messageText, messageBias, insertAt = null, compact = false, name = name1, avatar = user_avatar) {
47914800 messageText = getRegexedString(messageText, regex_placement.USER_INPUT);
@@ -4832,6 +4841,8 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul
48324841 await eventSource.emit(event_types.USER_MESSAGE_RENDERED, chat_id);
48334842 await saveChatConditional();
48344843 }
4844+
4845+ return message;
48354846}
48364847
48374848/**
@@ -7469,7 +7480,6 @@ export function showSwipeButtons() {
74697480 //had to add this to make the swipe counter work
74707481 //(copied from the onclick functions for swipe buttons..
74717482 //don't know why the array isn't set for non-swipe messsages in Generate or addOneMessage..)
7472-
74737483 if (chat[chat.length - 1]['swipe_id'] === undefined) { // if there is no swipe-message in the last spot of the chat array
74747484 chat[chat.length - 1]['swipe_id'] = 0; // set it to id 0
74757485 chat[chat.length - 1]['swipes'] = []; // empty the array
@@ -7478,33 +7488,37 @@ export function showSwipeButtons() {
74787488
74797489 const currentMessage = $('#chat').children().filter(`[mesid="${chat.length - 1}"]`);
74807490 const swipeId = chat[chat.length - 1].swipe_id;
74817491 const swipeCounterText = formatSwipeCounter(`${(swipeId + 1)}\u200B/\u200b${(, 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
74837496 if (swipeId !== undefined && (chat[chat.length - 1].swipes.length > 1 || swipeId > 0)) {
74847497 currentMessage.children('.swipe_left')swipeLeft.css('display', 'flex');
74857498 }
74867499 //only show right when generate is off, or when next right swipe would not make a generate happen
74877500 if (is_send_press === false || chat[chat.length - 1].swipes.length >= swipeId) {
74887501 currentMessageswipeRight.childrencss('.swipe_rightdisplay', 'flex').css('displayopacity', 'flex0.3');
74897502 currentMessage.children('.swipe_right')swipeCounter.css('opacity', '0.3');
74907503 }
7491- //console.log((chat[chat.length - 1]));
74927504 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');
74957509 }
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
75007515 //console.logswitchSwipeNumAllMessages(swipeId);
7501- //console.log(chat[chat.length - 1].swipes.length);
75027516}
75037517
75047518export function hideSwipeButtons() {
7505- //console.log('hideswipebuttons entered');
7519+ $('#chat').find('.swipe_right').hide();
75067520 $('#chat').find('.swipe_rightlast_mes .swipes-counter').csshide('display', 'none');
75077521 $('#chat').find('.swipe_left').csshide('display', 'none');
75087522}
75097523
75107524/**
@@ -8152,6 +8166,20 @@ window['SillyTavern'].getContext = function () {
81528166 };
81538167};
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+ */
8175+function formatSwipeCounter(current, total) {
8176+ if (isNaN(current) || isNaN(total)) {
8177+ return '';
8178+ }
8179+
8180+ return `${current}\u200b/\u200b${total}`;
8181+}
8182+
81558183function swipe_left() { // when we swipe left..but no generation.
81568184 if (chat.length - 1 === Number(this_edit_mes_id)) {
81578185 closeMessageEditor();
@@ -8339,8 +8367,8 @@ const swipe_right = () => {
83398367 }
83408368
83418369 const currentMessage = $('#chat').children().filter(`[mesid="${chat.length - 1}"]`);
83428370 let this_div = currentMessage.childrenfind('.swipe_right');
83438371 let this_mes_div = this_div.parent().parent();
83448372
83458373 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)
83468374 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 = () => {
83508378 }
83518379 // handles animated transitions when swipe right, specifically height transitions between messages
83528380 if (run_generate || run_swipe_right) {
83538381 let this_mes_block = this_mes_div.childrenfind('.mes_block').children(' .mes_text');
83548382 const this_mes_div_height = this_mes_div[0].scrollHeight;
83558383 const this_mes_block_height = this_mes_block[0].scrollHeight;
83568384
@@ -9407,9 +9435,9 @@ jQuery(async function () {
94079435
94089436 ///// SWIPE BUTTON CLICKS ///////
94099437
9410- $(document).on('click', '.swipe_right', swipe_right);
9438+ //limit swiping to only last message clicks
9411-
9439+ $(document).on('click', '.last_mes .swipe_right', swipe_right);
94129440 $(document).on('click', '.last_mes .swipe_left', swipe_left);
94139441
94149442 const debouncedCharacterSearch = debounce((searchQuery) => {
94159443 entitiesFilter.setFilterData(FILTER_TYPES.SEARCH, searchQuery);
@@ -9970,6 +9998,8 @@ jQuery(async function () {
99709998 }
99719999
997210000 else if (id == 'option_continue') {
10001+ if (this_edit_mes_id) return; // don't proceed if editing a message
10002+
997310003 if (is_send_press == false || fromSlashCommand) {
997410004 is_send_press = true;
997510005 Generate('continue', buildOrFillAdditionalArgs());
@@ -10636,7 +10666,7 @@ jQuery(async function () {
1063610666 $('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', async function () {
1063710667 await delay(50); $(this).closest('.drawer-content').removeClass('resizing');
1063810668 });
1063910669 $('.openIcon').not('.drawerPinnedOpen').toggleClass('closedIcon openIcon');
1064010670 $('.openDrawer').not('.pinnedOpen').toggleClass('closedDrawer openDrawer');
1064110671 icon.toggleClass('openIcon closedIcon');
1064210672 drawer.toggleClass('openDrawer closedDrawer');
@@ -10722,7 +10752,7 @@ jQuery(async function () {
1072210752 $('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', function () {
1072310753 $(this).closest('.drawer-content').removeClass('resizing');
1072410754 });
1072510755 $('.openIcon').not('.drawerPinnedOpen').toggleClass('closedIcon openIcon');
1072610756 $('.openDrawer').not('.pinnedOpen').toggleClass('closedDrawer openDrawer');
1072710757
1072810758 }
public/scripts/RossAscends-mods.js+19 -3
@@ -45,8 +45,11 @@ var LPanelPin = document.getElementById('lm_button_panel_pin');
4545var WIPanelPin = document.getElementById('WI_panel_pin');
4646
4747var RightNavPanel = document.getElementById('right-nav-panel');
48+var RightNavDrawerIcon = document.getElementById('rightNavDrawerIcon');
4849var LeftNavPanel = document.getElementById('left-nav-panel');
50+var LeftNavDrawerIcon = document.getElementById('leftNavDrawerIcon');
4951var WorldInfo = document.getElementById('WorldInfo');
52+var WIDrawerIcon = document.getElementById('WIDrawerIcon');
5053
5154var SelectedCharacterTab = document.getElementById('rm_button_selected_ch');
5255
@@ -761,13 +764,15 @@ export function initRossMods() {
761764 if ($(RPanelPin).prop('checked') == true) {
762765 //console.log('adding pin class to right nav');
763766 $(RightNavPanel).addClass('pinnedOpen');
767+ $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
764768 } else {
765769 //console.log('removing pin class from right nav');
766770 $(RightNavPanel).removeClass('pinnedOpen');
771+ $(RightNavDrawerIcon).removeClass('drawerPinnedOpen');
767772
768773 if ($(RightNavPanel).hasClass('openDrawer') && $('.openDrawer').length > 1) {
769774 $(RightNavPanel).slideToggle(200, 'swing');
770775 //$(rightNavDrawerIconRightNavDrawerIcon).toggleClass('openIcon closedIcon');
771776 $(RightNavPanel).toggleClass('openDrawer closedDrawer');
772777 }
773778 }
@@ -777,13 +782,15 @@ export function initRossMods() {
777782 if ($(LPanelPin).prop('checked') == true) {
778783 //console.log('adding pin class to Left nav');
779784 $(LeftNavPanel).addClass('pinnedOpen');
785+ $(LeftNavDrawerIcon).addClass('drawerPinnedOpen');
780786 } else {
781787 //console.log('removing pin class from Left nav');
782788 $(LeftNavPanel).removeClass('pinnedOpen');
789+ $(LeftNavDrawerIcon).removeClass('drawerPinnedOpen');
783790
784791 if ($(LeftNavPanel).hasClass('openDrawer') && $('.openDrawer').length > 1) {
785792 $(LeftNavPanel).slideToggle(200, 'swing');
786793 //$(leftNavDrawerIconLeftNavDrawerIcon).toggleClass('openIcon closedIcon');
787794 $(LeftNavPanel).toggleClass('openDrawer closedDrawer');
788795 }
789796 }
@@ -794,14 +801,16 @@ export function initRossMods() {
794801 if ($(WIPanelPin).prop('checked') == true) {
795802 console.debug('adding pin class to WI');
796803 $(WorldInfo).addClass('pinnedOpen');
804+ $(WIDrawerIcon).addClass('drawerPinnedOpen');
797805 } else {
798806 console.debug('removing pin class from WI');
799807 $(WorldInfo).removeClass('pinnedOpen');
808+ $(WIDrawerIcon).removeClass('drawerPinnedOpen');
800809
801810 if ($(WorldInfo).hasClass('openDrawer') && $('.openDrawer').length > 1) {
802811 console.debug('closing WI after lock removal');
803812 $(WorldInfo).slideToggle(200, 'swing');
804813 //$(WorldInfoDrawerIconWIDrawerIcon).toggleClass('openIcon closedIcon');
805814 $(WorldInfo).toggleClass('openDrawer closedDrawer');
806815 }
807816 }
@@ -812,20 +821,24 @@ export function initRossMods() {
812821 if (LoadLocalBool('NavLockOn') == true) {
813822 //console.log('setting pin class via local var');
814823 $(RightNavPanel).addClass('pinnedOpen');
824+ $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
815825 }
816826 if ($(RPanelPin).prop('checked')) {
817827 console.debug('setting pin class via checkbox state');
818828 $(RightNavPanel).addClass('pinnedOpen');
829+ $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
819830 }
820831 // read the state of left Nav Lock and apply to leftnav classlist
821832 $(LPanelPin).prop('checked', LoadLocalBool('LNavLockOn'));
822833 if (LoadLocalBool('LNavLockOn') == true) {
823834 //console.log('setting pin class via local var');
824835 $(LeftNavPanel).addClass('pinnedOpen');
836+ $(LeftNavDrawerIcon).addClass('drawerPinnedOpen');
825837 }
826838 if ($(LPanelPin).prop('checked')) {
827839 console.debug('setting pin class via checkbox state');
828840 $(LeftNavPanel).addClass('pinnedOpen');
841+ $(LeftNavDrawerIcon).addClass('drawerPinnedOpen');
829842 }
830843
831844 // read the state of left Nav Lock and apply to leftnav classlist
@@ -833,11 +846,13 @@ export function initRossMods() {
833846 if (LoadLocalBool('WINavLockOn') == true) {
834847 //console.log('setting pin class via local var');
835848 $(WorldInfo).addClass('pinnedOpen');
849+ $(WIDrawerIcon).addClass('drawerPinnedOpen');
836850 }
837851
838852 if ($(WIPanelPin).prop('checked')) {
839853 console.debug('setting pin class via checkbox state');
840854 $(WorldInfo).addClass('pinnedOpen');
855+ $(WIDrawerIcon).addClass('drawerPinnedOpen');
841856 }
842857
843858 //save state of Right nav being open or closed
@@ -1027,6 +1042,7 @@ export function initRossMods() {
10271042 const editMesDone = $('.mes_edit_done:visible');
10281043 if (editMesDone.length > 0) {
10291044 console.debug('Accepting edits with Ctrl+Enter');
1045+ $('#send_textarea').focus();
10301046 editMesDone.trigger('click');
10311047 return;
10321048 } else if (is_send_press == false) {
public/scripts/extensions/attachments/index.js+21 -0
@@ -1,3 +1,4 @@
1+import { event_types, eventSource, saveSettingsDebounced } from '../../../script.js';
12import { deleteAttachment, getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment, uploadFileAttachmentToServer } from '../../chats.js';
23import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
34import { SlashCommand } from '../../slash-commands/SlashCommand.js';
@@ -196,7 +197,27 @@ async function enableDataBankAttachment(args, value) {
196197 return '';
197198}
198199
200+function 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+
199219jQuery(async () => {
220+ eventSource.on(event_types.APP_READY, cleanUpAttachments);
200221 const manageButton = await renderExtensionTemplateAsync('attachments', 'manage-button', {});
201222 const attachButton = await renderExtensionTemplateAsync('attachments', 'attach-button', {});
202223 $('#data_bank_wand_container').append(manageButton);
public/scripts/extensions/caption/index.js+1 -0
@@ -403,6 +403,7 @@ jQuery(async function () {
403403 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && (secret_state[SECRET_KEYS.OPENAI] || extension_settings.caption.allow_reverse_proxy)) ||
404404 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openrouter' && secret_state[SECRET_KEYS.OPENROUTER]) ||
405405 (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]) ||
406407 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'mistral' && (secret_state[SECRET_KEYS.MISTRALAI] || extension_settings.caption.allow_reverse_proxy)) ||
407408 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && (secret_state[SECRET_KEYS.MAKERSUITE] || extension_settings.caption.allow_reverse_proxy)) ||
408409 (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 @@
2121 <option value="anthropic">Anthropic</option>
2222 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
2323 <option value="google">Google AI Studio</option>
24+ <option value="groq">Groq</option>
2425 <option value="koboldcpp">KoboldCpp</option>
2526 <option value="llamacpp">llama.cpp</option>
2627 <option value="mistral">MistralAI</option>
@@ -60,6 +61,9 @@
6061 <option data-type="google" value="gemini-1.5-pro-exp-0801">gemini-1.5-pro-exp-0801</option>
6162 <option data-type="google" value="gemini-1.5-pro-exp-0827">gemini-1.5-pro-exp-0827</option>
6263 <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>
6367 <option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
6468 <option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>
6569 <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 '
1212import { isFunctionCallingSupported } from '../../openai.js';
1313import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
1414import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
15+import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
16+import { SlashCommandClosure } from '../../slash-commands/SlashCommandClosure.js';
1517export { MODULE_NAME };
1618
1719const MODULE_NAME = 'expressions';
@@ -2134,18 +2136,42 @@ function migrateSettings() {
21342136 name: 'classify-expressions',
21352137 aliases: ['expressions'],
21362138 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) {
21392148 case 'json':
2140- return JSON.stringify(list);
2149+ returnType = 'object';
2150+ break;
21412151 default:
2142- return list.join(', ');
2152+ returnType = 'pipe';
2153+ break;
2154+ }
21432155 }
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(', ') });
21442161 },
21452162 namedArgumentList: [
21462163 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({
21472173 name: 'format',
21482174 description: '!!! DEPRECATED - use "return" instead !!! The format to return the list in: comma-separated plain text or JSON array. Default is plain text.',
21492175 typeList: [ARGUMENT_TYPE.STRING],
21502176 enumList: [
21512177 new SlashCommandEnumValue('plain', null, enumTypes.enum, ', '),
public/scripts/extensions/shared.js+5 -1
@@ -36,7 +36,7 @@ export async function getMultimodalCaption(base64Img, prompt) {
3636 const isVllm = extension_settings.caption.multimodal_api === 'vllm';
3737 const base64Bytes = base64Img.length * 0.75;
3838 const compressionLimit = 2 * 1024 * 1024;
3939 if ((['google', 'openrouter', 'mistral', 'groq'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) {
4040 const maxSide = 1024;
4141 base64Img = await createThumbnail(base64Img, maxSide, maxSide, 'image/jpeg');
4242 }
@@ -135,6 +135,10 @@ function throwIfInvalidModel(useReverseProxy) {
135135 throw new Error('01.AI API key is not set.');
136136 }
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+
138142 if (extension_settings.caption.multimodal_api === 'google' && !secret_state[SECRET_KEYS.MAKERSUITE] && !useReverseProxy) {
139143 throw new Error('Google AI Studio API key is not set.');
140144 }
public/scripts/extensions/vectors/index.js+2 -2
@@ -466,13 +466,13 @@ async function ingestDataBankAttachments(source) {
466466 }
467467
468468 // Download and process the file
469469 file.textconst fileText = await getFileAttachment(file.url);
470470 console.log(`Vectors: Retrieved file ${file.name} from Data Bank`);
471471 // Convert kilobytes to string length
472472 const thresholdLength = settings.size_threshold_db * 1024;
473473 // Use chunk size from settings if file is larger than threshold
474474 const chunkSize = file.size > thresholdLength ? settings.chunk_size_db : -1;
475475 await vectorizeFile(file.textfileText, file.name, collectionId, chunkSize, settings.overlap_percent_db);
476476 }
477477
478478 return dataBankCollectionIds;
public/scripts/openai.js+13 -5
@@ -724,6 +724,12 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
724724
725725 if (chatCompletion.canAfford(chatMessage)) {
726726 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+ }
727733 const collection = new MessageCollection('continuePrefill', chatMessage);
728734 chatCompletion.add(collection, -1);
729735 continue;
@@ -1767,8 +1773,8 @@ async function sendOpenAIRequest(type, messages, signal) {
17671773 generate_data['claude_use_sysprompt'] = oai_settings.claude_use_sysprompt;
17681774 generate_data['stop'] = getCustomStoppingStrings(); // Claude shouldn't have limits on stop strings.
17691775 generate_data['human_sysprompt_message'] = substituteParams(oai_settings.human_sysprompt_message);
17701776 // Don't add a prefill on quiet gens (summarization) and when using continue prefill.
17711777 if (!isQuiet && !(isContinue && oai_settings.continue_prefill)) {
17721778 generate_data['assistant_prefill'] = isImpersonate ? substituteParams(oai_settings.assistant_impersonation) : substituteParams(oai_settings.assistant_prefill);
17731779 }
17741780 }
@@ -4000,8 +4006,6 @@ async function onModelChange() {
40004006 $('#openai_max_context').attr('max', max_2mil);
40014007 } else if (value.includes('gemini-1.5-pro')) {
40024008 $('#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);
40054009 } else if (value.includes('gemini-1.5-flash')) {
40064010 $('#openai_max_context').attr('max', max_1mil);
40074011 } else if (value.includes('gemini-1.0-pro-vision') || value === 'gemini-pro-vision') {
@@ -4187,7 +4191,10 @@ async function onModelChange() {
41874191 if (oai_settings.max_context_unlocked) {
41884192 $('#openai_max_context').attr('max', unlocked_max);
41894193 }
41904194 else if (oai_settings.groq_model.includes('llama-3.12') && 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')) {
41914198 $('#openai_max_context').attr('max', max_128k);
41924199 }
41934200 else if (oai_settings.groq_model.includes('llama3-groq')) {
@@ -4650,6 +4657,7 @@ export function isImageInliningSupported() {
46504657 'gemini-1.5-flash-001',
46514658 'gemini-1.5-flash-002',
46524659 'gemini-1.5-flash-exp-0827',
4660+ 'gemini-1.5-flash-8b',
46534661 'gemini-1.5-flash-8b-exp-0827',
46544662 'gemini-1.5-flash-8b-exp-0924',
46554663 'gemini-1.0-pro-vision-latest',
public/scripts/popup.js+5 -0
@@ -1,3 +1,4 @@
1+import dialogPolyfill from '../lib/dialog-polyfill.esm.js';
12import { shouldSendOnEnter } from './RossAscends-mods.js';
23import { power_user } from './power-user.js';
34import { removeFromArray, runAfterAnimation, uuidv4 } from './utils.js';
@@ -178,6 +179,10 @@ export class Popup {
178179 const template = document.querySelector('#popup_template');
179180 // @ts-ignore
180181 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+ }
181186 this.body = this.dlg.querySelector('.popup-body');
182187 this.content = this.dlg.querySelector('.popup-content');
183188 this.mainInput = this.dlg.querySelector('.popup-input');
public/scripts/power-user.js+21 -0
@@ -290,6 +290,7 @@ let power_user = {
290290 restore_user_input: true,
291291 reduced_motion: false,
292292 compact_input_area: true,
293+ show_swipe_num_all_messages: false,
293294 auto_connect: false,
294295 auto_load_chat: false,
295296 forbid_external_media: true,
@@ -469,6 +470,11 @@ function switchCompactInputArea() {
469470 $('#compact_input_area').prop('checked', power_user.compact_input_area);
470471}
471472
473+export 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+
472478var originalSliderValues = [];
473479
474480async function switchLabMode() {
@@ -1283,6 +1289,13 @@ function applyTheme(name) {
12831289 switchCompactInputArea();
12841290 },
12851291 },
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+ },
12861299 ];
12871300
12881301 for (const { key, selector, type, action } of themeProperties) {
@@ -1352,6 +1365,7 @@ function applyPowerUserSettings() {
13521365 switchHideChatAvatars();
13531366 switchTokenCount();
13541367 switchMessageActions();
1368+ switchSwipeNumAllMessages();
13551369}
13561370
13571371function getExampleMessagesBehavior() {
@@ -2296,6 +2310,7 @@ function getThemeObject(name) {
22962310 zoomed_avatar_magnification: power_user.zoomed_avatar_magnification,
22972311 reduced_motion: power_user.reduced_motion,
22982312 compact_input_area: power_user.compact_input_area,
2313+ show_swipe_num_all_messages: power_user.show_swipe_num_all_messages,
22992314 };
23002315}
23012316
@@ -3755,6 +3770,12 @@ $(document).ready(() => {
37553770 saveSettingsDebounced();
37563771 });
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+
37583779 $('#auto-connect-checkbox').on('input', function () {
37593780 power_user.auto_connect = !!$(this).prop('checked');
37603781 saveSettingsDebounced();
public/scripts/slash-commands.js+136 -53
@@ -55,7 +55,7 @@ import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockStat
5555import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
5656import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
5757import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
5858import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
5959import { registerVariableCommands, resolveVariable } from './variables.js';
6060import { background_settings } from './backgrounds.js';
6161import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -70,6 +70,7 @@ import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
7070import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
7171import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
7272import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
73+import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
7374export {
7475 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
7576};
@@ -242,6 +243,7 @@ export function initDefaultSlashCommands() {
242243 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
243244 name: 'sendas',
244245 callback: sendMessageAs,
246+ returns: 'Optionally the text of the sent message, if specified in the "return" argument',
245247 namedArgumentList: [
246248 SlashCommandNamedArgument.fromProps({
247249 name: 'name',
@@ -269,6 +271,14 @@ export function initDefaultSlashCommands() {
269271 typeList: [ARGUMENT_TYPE.NUMBER],
270272 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
271273 }),
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+ }),
272282 ],
273283 unnamedArgumentList: [
274284 new SlashCommandArgument(
@@ -301,6 +311,7 @@ export function initDefaultSlashCommands() {
301311 name: 'sys',
302312 callback: sendNarratorMessage,
303313 aliases: ['nar'],
314+ returns: 'Optionally the text of the sent message, if specified in the "return" argument',
304315 namedArgumentList: [
305316 new SlashCommandNamedArgument(
306317 'compact',
@@ -316,6 +327,14 @@ export function initDefaultSlashCommands() {
316327 typeList: [ARGUMENT_TYPE.NUMBER],
317328 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
318329 }),
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+ }),
319338 ],
320339 unnamedArgumentList: [
321340 new SlashCommandArgument(
@@ -355,6 +374,7 @@ export function initDefaultSlashCommands() {
355374 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
356375 name: 'comment',
357376 callback: sendCommentMessage,
377+ returns: 'Optionally the text of the sent message, if specified in the "return" argument',
358378 namedArgumentList: [
359379 new SlashCommandNamedArgument(
360380 'compact',
@@ -370,6 +390,14 @@ export function initDefaultSlashCommands() {
370390 typeList: [ARGUMENT_TYPE.NUMBER],
371391 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
372392 }),
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+ }),
373401 ],
374402 unnamedArgumentList: [
375403 new SlashCommandArgument(
@@ -509,7 +537,7 @@ export function initDefaultSlashCommands() {
509537 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
510538 name: 'ask',
511539 callback: askCharacter,
512- returns: 'the generated text',
540+ returns: 'Optionally the text of the sent message, if specified in the "return" argument',
513541 namedArgumentList: [
514542 SlashCommandNamedArgument.fromProps({
515543 name: 'name',
@@ -518,6 +546,14 @@ export function initDefaultSlashCommands() {
518546 isRequired: true,
519547 enumProvider: commonEnumProviders.characters('character'),
520548 }),
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+ }),
521557 ],
522558 unnamedArgumentList: [
523559 new SlashCommandArgument(
@@ -556,6 +592,7 @@ export function initDefaultSlashCommands() {
556592 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
557593 name: 'send',
558594 callback: sendUserMessageCallback,
595+ returns: 'Optionally the text of the sent message, if specified in the "return" argument',
559596 namedArgumentList: [
560597 new SlashCommandNamedArgument(
561598 'compact',
@@ -578,6 +615,14 @@ export function initDefaultSlashCommands() {
578615 defaultValue: '{{user}}',
579616 enumProvider: commonEnumProviders.personas,
580617 }),
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+ }),
581626 ],
582627 unnamedArgumentList: [
583628 new SlashCommandArgument(
@@ -1568,12 +1613,21 @@ export function initDefaultSlashCommands() {
15681613 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
15691614 name: 'listinjects',
15701615 callback: listInjectsCallback,
15711616 helpString: 'Lists all script injections for the current chat. Displays injects in a popup by default. Use the <code>formatreturn</code> argument to change the outputreturn formattype.',
15721617 returns: 'Optionalls the JSON object of script injections',
15731618 namedArgumentList: [
15741619 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({
15751629 name: 'format',
15761630 description: '!!! DEPRECATED - use "return" instead !!! output format',
15771631 typeList: [ARGUMENT_TYPE.STRING],
15781632 isRequired: true,
15791633 forceEnum: true,
@@ -1842,37 +1896,43 @@ function injectCallback(args, value) {
18421896}
18431897
18441898async 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');
18451905 const type = String(args?.format).toLowerCase().trim();
18461906 if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) {
18471907 type !== 'none' && toastr.info('No script injections for the current chat');
1848- return JSON.stringify({});
18491908 }
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-
18631909 switch (type) {
18641910 case 'none':
1911+ returnType = 'none';
18651912 break;
18661913 case 'chat':
1867- sendSystemMessage(system_message_types.GENERIC, htmlMessage);
1914+ returnType = 'chat-html';
18681915 break;
18691916 case 'popup':
18701917 default:
1871- await callGenericPopup(htmlMessage, POPUP_TYPE.TEXT);
1918+ returnType = 'popup-html';
18721919 break;
18731920 }
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 });
18761936}
18771937
18781938/**
@@ -2559,7 +2619,7 @@ async function askCharacter(args, text) {
25592619 // Not supported in group chats
25602620 // TODO: Maybe support group chats?
25612621 if (selected_group) {
25622622 toastr.errorwarning('Cannot run /ask command in a group chat!');
25632623 return '';
25642624 }
25652625
@@ -2633,7 +2693,9 @@ async function askCharacter(args, text) {
26332693 }
26342694 }
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 });
26372699}
26382700
26392701async function hideMessageCallback(_, arg) {
@@ -2908,7 +2970,7 @@ function findPersonaByName(name) {
29082970
29092971async function sendUserMessageCallback(args, text) {
29102972 if (!text) {
29112973 consoletoastr.warnwarning('WARN:You Nomust textspecify providedtext forto /send command');
29122974 return;
29132975 }
29142976
@@ -2924,16 +2986,17 @@ async function sendUserMessageCallback(args, text) {
29242986 insertAt = chat.length + insertAt;
29252987 }
29262988
2989+ let message;
29272990 if ('name' in args) {
29282991 const name = args.name || '';
29292992 const avatar = findPersonaByName(name) || user_avatar;
29302993 message = await sendMessageAsUser(text, bias, insertAt, compact, name, avatar);
29312994 }
29322995 else {
29332996 message = await sendMessageAsUser(text, bias, insertAt, compact);
29342997 }
29352998
2936- return '';
2999+ return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
29373000}
29383001
29393002async function deleteMessagesByNameCallback(_, name) {
@@ -3221,30 +3284,20 @@ export function getNameAndAvatarForMessage(character, name = null) {
32213284
32223285export async function sendMessageAs(args, text) {
32233286 if (!text) {
3287+ toastr.warning('You must specify text to send as');
32243288 return '';
32253289 }
32263290
3227- let name;
3291+ let name = args.name?.trim();
32283292 let mesText;
32293293
32303294 if (args.!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 {
32383295 const namelessWarningKey = 'sendAsNamelessWarningShown';
32393296 if (localStorage.getItem(namelessWarningKey) !== 'true') {
32403297 toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 });
32413298 localStorage.setItem(namelessWarningKey, 'true');
32423299 }
32433300 name = name2;
3244- if (!text) {
3245- toastr.warning('You must specify text to send as');
3246- return '';
3247- }
32483301 }
32493302
32503303 mesText = text.trim();
@@ -3321,11 +3374,12 @@ export async function sendMessageAs(args, text) {
33213374 await saveChatConditional();
33223375 }
33233376
3324- return '';
3377+ return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
33253378}
33263379
33273380export async function sendNarratorMessage(args, text) {
33283381 if (!text) {
3382+ toastr.warning('You must specify text to send');
33293383 return '';
33303384 }
33313385
@@ -3374,7 +3428,7 @@ export async function sendNarratorMessage(args, text) {
33743428 await saveChatConditional();
33753429 }
33763430
3377- return '';
3431+ return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
33783432}
33793433
33803434export async function promptQuietForLoudResponse(who, text) {
@@ -3420,6 +3474,7 @@ export async function promptQuietForLoudResponse(who, text) {
34203474
34213475async function sendCommentMessage(args, text) {
34223476 if (!text) {
3477+ toastr.warning('You must specify text to send');
34233478 return '';
34243479 }
34253480
@@ -3462,7 +3517,7 @@ async function sendCommentMessage(args, text) {
34623517 await saveChatConditional();
34633518 }
34643519
3465- return '';
3520+ return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
34663521}
34673522
34683523/**
@@ -3542,11 +3597,12 @@ function setBackgroundCallback(_, bg) {
35423597 * Retrieves the available model options based on the currently selected main API and its subtype
35433598 * @param {boolean} quiet - Whether to suppress toasts
35443599 *
35453600 * @returns {{control: HTMLSelectElement|HTMLInputElement, options: HTMLOptionElement[]}?} An array of objects representing the available model options, or null if not supported
35463601 */
35473602function getModelOptions(quiet) {
35483603 const nullResult = { control: null, options: null };
35493604 const modelSelectMap = [
3605+ { id: 'custom_model_textgenerationwebui', api: 'textgenerationwebui', type: textgen_types.OOBA },
35503606 { id: 'model_togetherai_select', api: 'textgenerationwebui', type: textgen_types.TOGETHERAI },
35513607 { id: 'openrouter_model', api: 'textgenerationwebui', type: textgen_types.OPENROUTER },
35523608 { id: 'model_infermaticai_select', api: 'textgenerationwebui', type: textgen_types.INFERMATICAI },
@@ -3556,6 +3612,7 @@ function getModelOptions(quiet) {
35563612 { id: 'aphrodite_model', api: 'textgenerationwebui', type: textgen_types.APHRODITE },
35573613 { id: 'ollama_model', api: 'textgenerationwebui', type: textgen_types.OLLAMA },
35583614 { id: 'tabby_model', api: 'textgenerationwebui', type: textgen_types.TABBY },
3615+ { id: 'featherless_model', api: 'textgenerationwebui', type: textgen_types.FEATHERLESS },
35593616 { id: 'model_openai_select', api: 'openai', type: chat_completion_sources.OPENAI },
35603617 { id: 'model_claude_select', api: 'openai', type: chat_completion_sources.CLAUDE },
35613618 { id: 'model_windowai_select', api: 'openai', type: chat_completion_sources.WINDOWAI },
@@ -3563,7 +3620,7 @@ function getModelOptions(quiet) {
35633620 { id: 'model_ai21_select', api: 'openai', type: chat_completion_sources.AI21 },
35643621 { id: 'model_google_select', api: 'openai', type: chat_completion_sources.MAKERSUITE },
35653622 { id: 'model_mistralai_select', api: 'openai', type: chat_completion_sources.MISTRALAI },
35663623 { id: 'model_custom_selectcustom_model_id', api: 'openai', type: chat_completion_sources.CUSTOM },
35673624 { id: 'model_cohere_select', api: 'openai', type: chat_completion_sources.COHERE },
35683625 { id: 'model_perplexity_select', api: 'openai', type: chat_completion_sources.PERPLEXITY },
35693626 { id: 'model_groq_select', api: 'openai', type: chat_completion_sources.GROQ },
@@ -3594,12 +3651,31 @@ function getModelOptions(quiet) {
35943651
35953652 const modelSelectControl = document.getElementById(modelSelectItem);
35963653
35973654 if (!(modelSelectControl instanceof HTMLSelectElement) && !(modelSelectControl instanceof HTMLInputElement)) {
35983655 !quiet && toastr.error(`Model select control not found: ${main_api}[${apiSubType}]`);
35993656 return nullResult;
36003657 }
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);
36033679 return { control: modelSelectControl, options };
36043680}
36053681
@@ -3618,11 +3694,6 @@ function modelCallback(args, model) {
36183694 return '';
36193695 }
36203696
3621- if (!options.length) {
3622- !quiet && toastr.warning('No model options found. Check your API settings.');
3623- return '';
3624- }
3625-
36263697 model = String(model || '').trim();
36273698
36283699 if (!model) {
@@ -3631,6 +3702,18 @@ function modelCallback(args, model) {
36313702
36323703 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+
36343717 let newSelectedOption = null;
36353718
36363719 const fuse = new Fuse(options, { keys: ['text', 'value'] });
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+1 -0
@@ -36,6 +36,7 @@ export const enumIcons = {
3636 message: '💬',
3737 voice: '🎤',
3838 server: '🖥️',
39+ popup: '🗔',
3940
4041 true: '✔️',
4142 false: '❌',
public/scripts/slash-commands/SlashCommandReturnHelper.js+80 -0
@@ -0,0 +1,80 @@
1+import { sendSystemMessage, system_message_types } from '../../script.js';
2+import { callGenericPopup, POPUP_TYPE } from '../popup.js';
3+import { escapeHtml } from '../utils.js';
4+import { enumIcons } from './SlashCommandCommonEnumsProvider.js';
5+import { 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+
9+export 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
1111import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
1212import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
1313import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
14+import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
1415import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
1516import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';
1617
@@ -305,7 +306,28 @@ export function replaceVariableMacros(input) {
305306}
306307
307308async 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
309331 const scope = String(args?.scope || '').toLowerCase().trim() || 'all';
310332 if (!chat_metadata.variables) {
311333 chat_metadata.variables = {};
@@ -317,35 +339,24 @@ async function listVariablesCallback(args) {
317339 const localVariables = includeLocalVariables ? Object.entries(chat_metadata.variables).map(([name, value]) => `${name}: ${value}`) : [];
318340 const globalVariables = includeGlobalVariables ? Object.entries(extension_settings.variables.global).map(([name, value]) => `${name}: ${value}`) : [];
319341
320342 const jsonVariablesbuildTextValue = [(_) => {
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-
325343 const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables';
326344 const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables';
327345 const chatName = getCurrentChatId();
328346
329- const converter = new showdown.Converter();
330347 const message = [
331348 includeLocalVariables ? `### Local variables (${chatName}):\n${localVariablesString}` : '',
332349 includeGlobalVariables ? `### Global variables:\n${globalVariablesString}` : '',
333350 ].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 });
349360}
350361
351362/**
@@ -916,7 +927,7 @@ export function registerVariableCommands() {
916927 name: 'listvar',
917928 callback: listVariablesCallback,
918929 aliases: ['listchatvar'],
919930 helpString: 'List registered chat variables. Displays variables in a popup by default. Use the <code>formatreturn</code> argument to change the outputreturn formattype.',
920931 returns: 'JSON list of local variables',
921932 namedArgumentList: [
922933 SlashCommandNamedArgument.fromProps({
@@ -933,8 +944,17 @@ export function registerVariableCommands() {
933944 ],
934945 }),
935946 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({
936956 name: 'format',
937957 description: '!!! DEPRECATED - use "return" instead !!! output format',
938958 typeList: [ARGUMENT_TYPE.STRING],
939959 isRequired: true,
940960 forceEnum: true,
public/style.css+25 -6
@@ -969,34 +969,53 @@ body .panelControlBar {
969969
970970/* SWIPE RELATED STYLES*/
971971
972+.mes {
973+ --swipeCounterHeight: 15px;
974+ --swipeCounterMargin: 5px;
975+}
976+
972977.swipe_right,
973978.swipe_left {
974979 heightwidth: 40px25px;
975980 widthheight: 40px25px;
976981 opacity: 0.3;
977982 align-items: center;
978983 justify-content: center;
979984 z-index: 9999;
980985 grid-row-start: 2;
981986 font-size: 30px20px;
982987 cursor: pointer;
983988 align-self: center;
989+}
990+
991+.swipe_left {
984992 position: absolute;
985-bottom: 15px;
993+ bottom: calc(var(--swipeCounterHeight) + var(--swipeCounterMargin));
986994 flex-flow: column;
987995}
988996
997+.swipeRightBlock {
998+ position: absolute;
999+ right: 0;
1000+ bottom: 0;
1001+}
9891002
9901003.swipes-counter {
9911004 color: var(--SmartThemeBodyColor);
9921005 font-size: 12px;
9931006 padding: 0 5px;
9941007 font-family: var(--mainFontFamily);
9951008 font-weight: 400;
9961009 align-self: center;
9971010 min-width: 40px;
9981011 display: flex;
9991012 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;
10001019}
10011020
10021021.swipe_left {
@@ -1006,7 +1025,7 @@ flex-flow: column;
10061025
10071026.swipe_right {
10081027 right: 5px;
10091028 align-self:end center;
10101029}
10111030
10121031.ui-settings {
src/endpoints/backends/chat-completions.js+6 -2
@@ -107,6 +107,7 @@ async function sendClaudeRequest(request, response) {
107107 }
108108
109109 const requestBody = {
110+ /** @type {any} */ system: '',
110111 messages: convertedPrompt.messages,
111112 model: request.body.model,
112113 max_tokens: request.body.max_tokens,
@@ -120,6 +121,8 @@ async function sendClaudeRequest(request, response) {
120121 requestBody.system = enableSystemPromptCache
121122 ? [{ type: 'text', text: convertedPrompt.systemPrompt, cache_control: { type: 'ephemeral' } }]
122123 : convertedPrompt.systemPrompt;
124+ } else {
125+ delete requestBody.system;
123126 }
124127 if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
125128 // Claude doesn't do prefills on function calls, and doesn't allow empty messages
@@ -156,12 +159,13 @@ async function sendClaudeRequest(request, response) {
156159 forwardFetchResponse(generateResponse, response);
157160 } else {
158161 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}`));
160164 return response.status(generateResponse.status).send({ error: true });
161165 }
162166
163167 const generateResponseJson = await generateResponse.json();
164168 const responseText = generateResponseJson?.content?.[0]?.text || '';
165169 console.log('Claude response:', generateResponseJson);
166170
167171 // 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) => {
5555 key = readSecret(request.user.directories, SECRET_KEYS.MISTRALAI);
5656 }
5757
58+ if (request.body.api === 'groq') {
59+ key = readSecret(request.user.directories, SECRET_KEYS.GROQ);
60+ }
61+
5862 if (!key && !request.body.reverse_proxy && ['custom', 'ooba', 'koboldcpp', 'vllm'].includes(request.body.api) === false) {
5963 console.log('No key found for API', request.body.api);
6064 return response.sendStatus(400);
@@ -111,6 +115,10 @@ router.post('/caption-image', jsonParser, async (request, response) => {
111115 apiUrl = 'https://api.01.ai/v1/chat/completions';
112116 }
113117
118+ if (request.body.api === 'groq') {
119+ apiUrl = 'https://api.groq.com/openai/v1/chat/completions';
120+ }
121+
114122 if (request.body.api === 'mistral') {
115123 apiUrl = 'https://api.mistral.ai/v1/chat/completions';
116124 }
src/prompt-converters.js+1 -0
@@ -269,6 +269,7 @@ function convertGooglePrompt(messages, model, useSysPrompt = false, charName = '
269269 'gemini-1.5-flash-001',
270270 'gemini-1.5-flash-002',
271271 'gemini-1.5-flash-exp-0827',
272+ 'gemini-1.5-flash-8b',
272273 'gemini-1.5-flash-8b-exp-0827',
273274 'gemini-1.5-flash-8b-exp-0924',
274275 'gemini-1.5-pro',