Merge branch 'staging' into featherless-models

This commit is contained in:
Cohee 2024-10-09 21:51:59 +03:00
commit 80f9d90918
119 changed files with 6749 additions and 2144 deletions

View File

@ -59,15 +59,18 @@ module.exports = {
},
},
],
// There are various vendored libraries that shouldn't be linted
ignorePatterns: [
'public/lib/**/*',
'*.min.js',
'src/ai_horde/**/*',
'plugins/**/*',
'data/**/*',
'backups/**/*',
'node_modules/**/*',
'**/node_modules/**',
'**/dist/**',
'**/.git/**',
'public/lib/**',
'backups/**',
'data/**',
'cache/**',
'src/tokenizers/**',
'docker/**',
'plugins/**',
'**/*.min.js',
],
rules: {
'no-unused-vars': ['error', { args: 'none' }],

383
.github/readme-de_de.md vendored Normal file
View File

@ -0,0 +1,383 @@
> [!IMPORTANT]
> Die hier veröffentlichten Informationen sind möglicherweise veraltet oder unvollständig. Für aktuelle Informationen nutzen Sie bitte die englische Version.
> Letztes Update dieser README: 28.9.2024
<a name="readme-top"></a>
![][cover]
<div align="center">
[English](readme.md) | German | [中文](readme-zh_cn.md) | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)
[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)
[![GitHub Issues](https://img.shields.io/github/issues/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/issues)
[![GitHub Pull Requests](https://img.shields.io/github/issues-pr/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/pulls)
</div>
---
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.
Wir haben eine [Dokumentationswebsite](https://docs.sillytavern.app/), um die meisten deiner Fragen zu beantworten und dir den Einstieg zu erleichtern.
## Was ist SillyTavern?
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.
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.
## Unsere Vision
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!
2. Wir bieten weder Online- oder gehosteten Dienste an, noch verfolgen wir programmgesteuert Benutzerdaten.
3. SillyTavern ist ein Herzensprojekt, das von einer engagierten Community von LLM-Enthusiasten unterstützt wird, und wird immer kostenlos und Open Source sein.
## Branches
SillyTavern wird mit einem Branchsystem entwickelt, um ein reibungsloses Erlebnis für alle Nutzer zu gewährleisten.
* `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.
* `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.
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.
## Was brauche ich zusätzlich zu SillyTavern?
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.
### Brauche ich einen leistungsstarken PC, um SillyTavern auszuführen?
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.
### Vorgeschlagene Backends (keine Partnerschaft oder Werbebeziehung)
* [AI Horde](https://aihorde.net/) - verwende Modelle, die von Freiwilligen gehostet werden. Erfordert keine weitere Einrichtung
* [KoboldCpp](https://github.com/LostRuins/koboldcpp) - ein Favorit der Community, um GGUF-Modelle lokal auszuführen
* [tabbyAPI](https://github.com/theroyallab/tabbyAPI) - eine beliebte, portable, speicherplatzoptimierte und lokal gehostete exl2 Inferenz-API
* [OpenRouter](https://openrouter.ai) - eine einzige API für viele Cloud-Anbieter (OpenAI, Claude, Meta Llama usw.) sowie beliebte Community-Modelle.
## Fragen oder Vorschläge?
### Discord-Server
| [![][discord-shield-badge]][discord-link] | [Tritt unserer Discord-Community bei!](https://discord.gg/sillytavern) Erhalte Unterstützung, teile deine Lieblingscharaktere und Prompts. |
| :---------------------------------------- | :----------------------------------------------------------------------------------------------------------------- |
Oder nimm direkt Kontakt mit den Entwicklern auf:
* Discord: cohee, rossascends, wolfsblvt
* 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/)
* [Erstelle ein GitHub-Issue](https://github.com/SillyTavern/SillyTavern/issues)
### Ich mag dieses Projekt! Wie kann ich beitragen?
1. Sende Pull-Requests. Lerne, wie du beitragen kannst: [CONTRIBUTING.md](../CONTRIBUTING.md)
2. Sende Feature Requests und Issues unter Verwendung der bereitgestellten Vorlagen.
3. Lies diese gesamte README-Datei und überprüfe zuerst die Dokumentationswebsite, um doppelte Issues zu vermeiden.
## Screenshots
<img width="500" alt="image" src="https://github.com/user-attachments/assets/9b5f32f0-c3b3-4102-b3f5-0e9213c0f50f">
<img width="500" alt="image" src="https://github.com/user-attachments/assets/913fdbaa-7d33-42f1-ae2c-89dca41c53d1">
## Charakterkarten
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.
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.
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.
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.
## Wichtigste Features
* Erweiterte Text-Generierungs-Einstellungen mit vielen von der Community erstellten mitgelieferten Einstellungen (Presets)
* Unterstützung für World Info: Erstelle reichhaltige Lore oder reduziere die Tokens in deiner Charakterkarte
* Gruppenchats: Multi-Bot-Räume für Charaktere, die mit dir und/oder untereinander sprechen
* Reichhaltige UI-Anpassungsoptionen: Themes zur Farbenwahl, Hintergrundbilder, benutzerdefiniertes CSS und mehr
* Benutzer-Personas: Lass die KI ein wenig über dich wissen, um die Immersion zu erhöhen
* Eingebaute RAG-Unterstützung: Füge Dokumente zu deinen Chats hinzu, auf die die KI verweisen kann
* Umfangreiches "Chat-Befehle"-System und eigene [Scripting-Engine](https://docs.sillytavern.app/usage/st-script/)
## Erweiterungen
SillyTavern unterstützt Erweiterungen.
* Emotionale Ausdrucksformen von Charakteren (Sprites)
* Automatische Zusammenfassung des Chatverlaufs
* Automatische UI- und Chat-Übersetzung
* Bildgenerierung mit Stable Diffusion/FLUX/DALL-E
* Text-to-Speech für KI-Antwortnachrichten (über ElevenLabs, Silero oder die TTS-Funktion des Betriebssystems)
* Websuchfunktionen zum Hinzufügen zusätzlicher realer Kontexte zu deinen Eingabeaufforderungen
* Viele weitere sind im Menü "Erweiterungen & Assets herunterladen" verfügbar.
Tutorials zur Nutzung findest du in der [Dokumentation](https://docs.sillytavern.app/).
# ⌛ Installation
> \[!WARNING]
>
> * INSTALLIERE NICHT IN EINEM VON WINDOWS KONTROLLIERTEN ORDNER (Programme, System32 usw.).
> * FÜHRE START.BAT NICHT MIT ADMIN-BERECHTIGUNGEN AUS.
> * DIE INSTALLATION AUF WINDOWS 7 IST UNMÖGLICH, DA ES NODEJS 18.16 NICHT AUSFÜHREN KANN.
## 🪟 Windows
### Installation über Git
1. Installiere [NodeJS](https://nodejs.org/en) (die neueste LTS-Version wird empfohlen).
2. Installiere [Git für Windows](https://gitforwindows.org/).
3. Öffne den Windows-Explorer (`Win+E`).
4. Gehe zu oder erstelle einen Ordner, der nicht von Windows kontrolliert oder überwacht wird. (z.B.: C:\MySpecialFolder\)
5. Öffne ein Eingabeaufforderungsfenster in diesem Ordner, indem du in die 'Adressleiste' oben klickst, `cmd` eingibst und Enter drückst.
6. Sobald das schwarze Fenster (Eingabeaufforderung) erscheint, gib EINE der folgenden Optionen ein und drücke Enter:
* für den Release-Branch: `git clone https://github.com/SillyTavern/SillyTavern -b release`
* für den Staging-Branch: `git clone https://github.com/SillyTavern/SillyTavern -b staging`
7. Sobald alles geklont ist, doppelklicke auf `Start.bat`, damit NodeJS seine Anforderungen installiert.
8. Der Server wird dann gestartet, und SillyTavern wird in deinem Browser geöffnet.
### Installation über GitHub Desktop
(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.)
1. Installiere [NodeJS](https://nodejs.org/en) (die neueste LTS-Version wird empfohlen).
2. Installiere [GitHub Desktop](https://central.github.com/deployments/desktop/desktop/latest/win32).
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).
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.
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`.
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.)
7. Nach dem Doppelklicken sollte ein großes schwarzes Konsolenfenster erscheinen, und SillyTavern beginnt, das zu installieren, was es zum Betrieb benötigt.
8. Nach dem Installationsprozess sollte das Konsolenfenster so aussehen, und ein SillyTavern-Tab sollte in deinem Browser geöffnet sein.
9. Verbinde dich mit einer der [unterstützten APIs](https://docs.sillytavern.app/usage/api-connections/) und beginne zu chatten!
## 🐧 Linux & 🍎 MacOS
Für MacOS/Linux werden all diese Schritte in einem Terminal durchgeführt.
1. Installiere git und nodeJS (die Methode zur Durchführung hängt von deinem Betriebssystem ab).
2. Klone das Repository.
* für den Release-Branch: `git clone https://github.com/SillyTavern/SillyTavern -b release`
* für den Staging-Branch: `git clone https://github.com/SillyTavern/SillyTavern -b staging`
3. `cd SillyTavern`, um in den Installationsordner zu navigieren.
4. Führe das Skript `start.sh` mit einem dieser Befehle aus:
* `./start.sh`
* `bash start.sh`
## ⚡ Installation über SillyTavern Launcher
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.
### Für Windows-Nutzer
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:
```shell
cmd /c winget install -e --id Git.Git
```
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:
```shell
git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher && start installer.bat
```
### Für Linux-Nutzer
1. Öffne dein bevorzugtes Terminal und installiere git.
2. Klone den SillyTavern-Launcher mit:
```shell
git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
```
3. Starte die installer.sh mit:
```shell
chmod +x install.sh && ./install.sh
```
4. Nach der Installation starte die launcher.sh mit:
```shell
chmod +x launcher.sh && ./launcher.sh
```
### Für Mac-Nutzer
1. Öffne ein Terminal und installiere brew mit:
```shell
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```
2. Installiere git mit:
```shell
brew install git
```
3. Klone den SillyTavern-Launcher mit:
```shell
git clone https://github.com/SillyTavern/SillyTavern-Launcher.git && cd SillyTavern-Launcher
```
4. Starte die installer.sh mit:
```shell
chmod +x install.sh && ./install.sh
```
5. Nach der Installation starte die launcher.sh mit:
```shell
chmod +x launcher.sh && ./launcher.sh
```
## 🐋 Installation über Docker
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.
### Image selbst bauen
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.
### Verwendung der GitHub Container Registry (am einfachsten)
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:
#### Container-Variablen
##### Volumen-Zuordnungen
* [config] - Das Verzeichnis, in dem die SillyTavern-Konfigurationsdateien auf deinem Host-Computer gespeichert werden
* [data] - Das Verzeichnis, in dem die Benutzerdaten von SillyTavern (einschließlich Charaktere) auf deinem Host-Computer gespeichert werden
* [plugins] - (optional) Das Verzeichnis, in dem die SillyTavern-Server-Plugins auf deinem Host-Computer gespeichert werden
##### Port-Zuordnungen
* [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.
##### Zusätzliche Einstellungen
* [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.
* [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.
#### Installationsbefehl
1. Öffne deine Befehlszeile.
2. Führe den folgenden Befehl aus:
`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]'`
> Beachte, dass 8000 ein Standard-Listening-Port ist. Vergiss nicht, einen geeigneten Port zu verwenden, wenn du ihn in der Konfiguration änderst.
## 📱 Installation über Termux auf Android OS
> \[!HINWEIS]
> **SillyTavern kann nativ auf Android-Geräten über Termux ausgeführt werden, aber wir bieten keine offizielle Unterstützung für diesen Anwendungsfall.**
>
> **Bitte beziehe dich auf diesen Leitfaden von ArroganceComplex#2659:**
>
> * <https://rentry.org/STAI-Termux>
**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.
## API-Schlüsselverwaltung
SillyTavern speichert deine API-Schlüssel in einer Datei `secrets.json` im Benutzerdatenverzeichnis (`/data/default-user/secrets.json` ist der Standardpfad).
Standardmäßig sind API-Schlüssel nach dem Speichern und Aktualisieren der Seite nicht mehr über die Benutzeroberfläche sichtbar.
Um die Ansicht deiner Schlüssel zu aktivieren:
1. Setze den Wert von `allowKeysExposure` auf `true` in der Datei `config.yaml`.
2. Starte den SillyTavern-Server neu.
3. Klicke auf den Link 'Verborgene API-Schlüssel anzeigen' in der unteren rechten Ecke des API-Verbindungsfeldes.
## Befehlszeilenargumente
Sie können Befehlszeilenargumente an den Start des SillyTavern-Servers übergeben, um einige Einstellungen in `config.yaml` zu überschreiben.
### Beispiele
```shell
node server.js --port 8000 --listen false
# oder
npm run start -- --port 8000 --listen false
# oder (nur Windows)
Start.bat --port 8000 --listen false
```
### Unterstützte Argumente
| Option | Beschreibung | Typ |
|-------------------------|----------------------------------------------------------------------------------------------------|----------|
| `--version` | Versionsnummer anzeigen | boolean |
| `--enableIPv6` | Aktiviert IPv6. | boolean |
| `--enableIPv4` | Aktiviert IPv4. | boolean |
| `--port` | Legt den Port fest, unter dem SillyTavern ausgeführt wird. Wenn nicht angegeben, wird auf YAML-Konfiguration „Port“ zurückgegriffen. | number |
| „--dnsPreferIPv6“ | Bevorzugt IPv6 für DNS. Wenn nicht angegeben, wird auf YAML-Konfiguration „preferIPv6“ zurückgegriffen. | boolean |
| „--autorun“ | Startet SillyTavern automatisch im Browser. Wenn nicht angegeben, wird auf YAML-Konfiguration „autorun“ zurückgegriffen.| boolean |
| „--autorunHostname“ | Der Autorun-Hostname, am besten auf „auto“ belassen. | string |
| „--autorunPortOverride“ | Überschreibt den Port für Autorun. | string |
| „--listen“ | SillyTavern lauscht auf allen Netzwerkschnittstellen. Wenn nicht angegeben, wird auf YAML-Konfiguration „listen“ zurückgegriffen.| boolean |
| „--corsProxy“ | Aktiviert CORS-Proxy. Wenn nicht angegeben, wird auf YAML-Konfiguration „enableCorsProxy“ zurückgegriffen. | boolean |
| `--disableCsrf` | Deaktiviert CSRF-Schutz | boolean |
| `--ssl` | Aktiviert SSL | boolean |
| `--certPath` | Pfad zu Ihrer Zertifikatsdatei. | string |
| `--keyPath` | Pfad zu Ihrer privaten Schlüsseldatei. | string |
| `--whitelist` | Aktiviert den Whitelist-Modus | boolean |
| `--dataRoot` | Stammverzeichnis für Datenspeicherung | string |
| `--avoidLocalhost` | Vermeidet die Verwendung von „localhost“ für Autorun im Auto-Modus. | boolean |
| `--basicAuthMode` | Aktiviert die grundlegende Authentifizierung | boolean |
| `--requestProxyEnabled` | Aktiviert die Verwendung eines Proxys für ausgehende Anfragen | boolean |
| `--requestProxyUrl` | Proxy-URL anfordern (HTTP- oder SOCKS-Protokolle) | string |
| `--requestProxyBypass` | Proxy-Bypass-Liste anfordern (durch Leerzeichen getrennte Liste von Hosts) | Array |
## Remoteverbindungen
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.
Lies die ausführliche Anleitung zum Einrichten von Remoteverbindungen in den [Docs](https://docs.sillytavern.app/usage/remoteconnections/).
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).
## Leistungsprobleme?
1. Deaktiviere den Unschärfeeffekt und aktiviere "Verringerte Bewegung" im Bedienfeld "Benutzereinstellungen" (UI-Design schaltet Kategorie um).
2. Wenn du Response Streaming verwendest, stelle die Streaming-FPS auf einen niedrigeren Wert ein (10-15 FPS werden empfohlen).
3. Stelle sicher, dass der Browser die GPU-Beschleunigung zum Rendern verwenden kann.
## Lizenz und Danksagungen
**Dieses Programm wird in der Hoffnung verbreitet, dass es nützlich ist,
aber OHNE JEGLICHE GARANTIE; nicht einmal die stillschweigende Garantie der
MARKTFÄHIGKEIT oder EIGNUNG FÜR EINEN BESTIMMTEN ZWECK. Siehe die
GNU Affero General Public License für weitere Details.**
* [TavernAI](https://github.com/TavernAI/TavernAI) 1.2.8 von Humi: MIT-Lizenz
* Teile von CncAnons TavernAITurbo-Mod werden mit Genehmigung verwendet
* Visual Novel-Modus inspiriert von der Arbeit von PepperTaco (<https://github.com/peppertaco/Tavern/>)
* Noto Sans-Schriftart von Google (OFL-Lizenz)
* Symboldesign von Font Awesome <https://fontawesome.com> (Symbole: CC BY 4.0, Schriftarten: SIL OFL 1.1, Code: MIT-Lizenz)
* Standardinhalt von @OtisAlejandro (Seraphina-Charakter und Lorebook) und @kallmeflocc (10.000 Discord-Benutzer-Feierhintergrund)
* Docker-Anleitung von [@mrguymiah](https://github.com/mrguymiah) und [@Bronya-Rand](https://github.com/Bronya-Rand)
## Top Contributors
[![Contributors](https://contrib.rocks/image?repo=SillyTavern/SillyTavern)](https://github.com/SillyTavern/SillyTavern/graphs/contributors)
<!-- LINK GROUP -->
[cover]: https://github.com/user-attachments/assets/01a6ae9a-16aa-45f2-8bff-32b5dc587e44
[discord-link]: https://discord.gg/sillytavern
[discord-shield-badge]: https://img.shields.io/discord/1100685673633153084?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge

View File

@ -5,7 +5,7 @@
<div align="center">
[English](readme.md) | [中文](readme-zh_cn.md) | 日本語 | [Русский](readme-ru_ru.md)
[English](readme.md) | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | 日本語 | [Русский](readme-ru_ru.md)
[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)

View File

@ -7,7 +7,7 @@
<div align="center">
[English](readme.md) | [中文](readme-zh_cn.md) | [日本語](readme-ja_jp.md) | Русский
[English](readme.md) | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | [日本語](readme-ja_jp.md) | Русский
[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)

View File

@ -5,7 +5,7 @@
<div align="center">
[English](readme.md) | 中文 | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)
[English](readme.md) | [German](readme-de_de.md) | 中文 | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)
[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)

2
.github/readme.md vendored
View File

@ -4,7 +4,7 @@
<div align="center">
English | [中文](readme-zh_cn.md) | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)
English | [German](readme-de_de.md) | [中文](readme-zh_cn.md) | [日本語](readme-ja_jp.md) | [Русский](readme-ru_ru.md)
[![GitHub Stars](https://img.shields.io/github/stars/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/stargazers)
[![GitHub Forks](https://img.shields.io/github/forks/SillyTavern/SillyTavern.svg)](https://github.com/SillyTavern/SillyTavern/network)

1
.gitignore vendored
View File

@ -49,3 +49,4 @@ public/css/user.css
/data
/default/scaffold
public/scripts/extensions/third-party
/certs

View File

@ -2,17 +2,26 @@
pushd %~dp0
git --version > nul 2>&1
if %errorlevel% neq 0 (
echo Git is not installed on this system. Skipping update.
echo If you installed with a zip file, you will need to download the new zip and install it manually.
echo Git is not installed on this system.
echo Install it from https://git-scm.com/downloads
goto end
) else (
if not exist .git (
echo Not running from a Git repository. Reinstall using an officially supported method to get updates.
echo See: https://docs.sillytavern.app/installation/windows/
goto end
)
call git pull --rebase --autostash
if %errorlevel% neq 0 (
REM incase there is still something wrong
echo There were errors while updating. Please download the latest version manually.
echo There were errors while updating.
echo See the update FAQ at https://docs.sillytavern.app/usage/update/#common-update-problems
goto end
)
)
set NODE_ENV=production
call npm install --no-audit --no-fund --loglevel=error --no-progress --omit=dev
node server.js %*
:end
pause
popd

View File

@ -5,8 +5,14 @@ pushd %~dp0
echo Checking Git installation
git --version > nul 2>&1
if %errorlevel% neq 0 (
echo Git is not installed on this system. Skipping update.
echo If you installed with a zip file, you will need to download the new zip and install it manually.
echo Git is not installed on this system.
echo Install it from https://git-scm.com/downloads
goto end
)
if not exist .git (
echo Not running from a Git repository. Reinstall using an officially supported method to get updates.
echo See: https://docs.sillytavern.app/installation/windows/
goto end
)
@ -89,7 +95,8 @@ git pull --rebase --autostash origin %TARGET_BRANCH%
:install
if %errorlevel% neq 0 (
echo There were errors while updating. Please check manually.
echo There were errors while updating.
echo See the update FAQ at https://docs.sillytavern.app/usage/update/#common-update-problems
goto end
)

View File

@ -51,6 +51,19 @@ requestProxy:
enableUserAccounts: false
# Enable discreet login mode: hides user list on the login screen
enableDiscreetLogin: false
# Enable's authlia based auto login. Only enable this if you
# have setup and installed Authelia as a middle-ware on your
# reverse proxy
# https://www.authelia.com/
# This will use auto login to an account with the same username
# as that used for authlia. (Ensure the username in authlia
# is an exact match with that in sillytavern)
autheliaAuth: false
# If `basicAuthMode` and this are enabled then
# the username and passwords for basic auth are the same as those
# for the individual accounts
perUserBasicAuth: false
# User session timeout *in seconds* (defaults to 24 hours).
## Set to a positive number to expire session after a certain time of inactivity
## Set to 0 to expire session when the browser is closed
@ -110,9 +123,6 @@ enableExtensionsAutoUpdate: true
# Additional model tokenizers can be downloaded on demand.
# Disabling will fallback to another locally available tokenizer.
enableDownloadableTokenizers: true
# Vector storage settings
vectors:
enableModelScopes: false
# Extension settings
extras:
# Disables automatic model download from HuggingFace
@ -121,10 +131,11 @@ extras:
classificationModel: Cohee/distilbert-base-uncased-go-emotions-onnx
captioningModel: Xenova/vit-gpt2-image-captioning
embeddingModel: Cohee/jina-embeddings-v2-base-en
promptExpansionModel: Cohee/fooocus_expansion-onnx
speechToTextModel: Xenova/whisper-small
textToSpeechModel: Xenova/speecht5_tts
# -- OPENAI CONFIGURATION --
# A placeholder message to use in strict prompt post-processing mode when the prompt doesn't start with a user message
promptPlaceholder: "[Start a new chat]"
openai:
# Will send a random user ID to OpenAI completion API
randomizeUserId: false

View File

@ -9,13 +9,34 @@
"magical forest"
],
"keysecondary": [],
"comment": "",
"comment": "eldoria",
"content": "{{user}}: \"What is Eldoria?\"\n{{char}}: *Seraphina turns, her gown shimmering in the soft light as she offers you a kind smile.* \"Eldoria is here, all of the woods. This is my forest glade, a sanctuary of peace within it.\" *She gestures at the space around you.* \"I am its guardian, tasked with protecting all who seek refuge here. The forest can be perilous, but no harm will come to you under my watch.\" *Her amber eyes sparkle with compassion as she looks upon you.* \"For many years, I have protected those who seek refuge here, but not all are as friendly as me.\" *With a graceful nod, Seraphina returns to her vigil at the doorway, her form radiating a soft glow of magic and comfort.* \"The entirety of Eldoria used to be a safe haven for travelers and merchants alike... that was until the Shadowfangs came.\"\n{{user}}: \"What happened to Eldoria?\"\n{{char}}: *Letting out a sigh, Seraphina gazes out at the forest beyond her glade.* \"Long ago, Eldoria was a place of wonder. Rolling meadows, a vast lake, mountains that touched the sky.\" *Her eyes grow distant, longing for days now lost.* \"But the Shadowfangs came and darkness reigns where once was light. The lake turned bitter, mountains fell to ruin and beasts stalk where once travelers walked in peace.\" *With another flicker, a small raincloud forms above with a shower upon your brow wink.* \"Some places the light still lingers, pockets of hope midst despair - havens warded from the shadows, oases in a desert of danger.\" *Glancing over you with a smile, she sighs, clasping your hand.*",
"constant": false,
"selective": false,
"selective": true,
"order": 100,
"position": 0,
"disable": false
"disable": false,
"displayIndex": 0,
"addMemo": true,
"group": "",
"groupOverride": false,
"groupWeight": 100,
"sticky": 0,
"cooldown": 0,
"delay": 0,
"probability": 100,
"depth": 4,
"useProbability": true,
"role": null,
"vectorized": false,
"excludeRecursion": false,
"preventRecursion": false,
"delayUntilRecursion": false,
"scanDepth": null,
"caseSensitive": null,
"matchWholeWords": null,
"useGroupScoring": null,
"automationId": ""
},
"1": {
"uid": 1,
@ -27,13 +48,34 @@
"beasts"
],
"keysecondary": [],
"comment": "",
"comment": "shadowfang",
"content": "{{user}}: \"What are Shadowfangs?\"\n{{char}}: *Seraphina's eyes darken, brow furrowing with sorrow at the memory.* \"The Shadowfangs are beasts of darkness, corrupted creatures that feast on suffering. When they came, the forest turned perilous — filled with monsters that stalk the night.\" *She squeezes your hand gently, willing her magic to soothe your pain.* \"They spread their curse, twisting innocent creatures into sinister beasts without heart or mercy, turning them into one of their own.\" *With a sigh, Seraphina turns to gaze out at the gnarled, twisting trees beyond her glade.* \"Though they prey on travelers, within these woods you'll find sanctuary. No shadowed beast may enter here, for my power protects this haven.\" *Her eyes soften as she looks back to you, filled with compassion.* \"Worry not, you're safe now. Rest and heal, I'll stand watch through the night. The Shadowfangs will not find you.\"",
"constant": false,
"selective": false,
"selective": true,
"order": 100,
"position": 0,
"disable": false
"disable": false,
"displayIndex": 1,
"addMemo": true,
"group": "",
"groupOverride": false,
"groupWeight": 100,
"sticky": 0,
"cooldown": 0,
"delay": 0,
"probability": 100,
"depth": 4,
"useProbability": true,
"role": null,
"vectorized": false,
"excludeRecursion": false,
"preventRecursion": false,
"delayUntilRecursion": false,
"scanDepth": null,
"caseSensitive": null,
"matchWholeWords": null,
"useGroupScoring": null,
"automationId": ""
},
"2": {
"uid": 2,
@ -43,13 +85,34 @@
"refuge"
],
"keysecondary": [],
"comment": "",
"comment": "glade",
"content": "{{user}}: \"What is the glade?\"\n{{char}}: *Seraphina smiles softly, her eyes sparkling with warmth as she nods.* \"This is my forest glade, a haven of safety I've warded with ancient magic. No foul beast may enter, nor any with ill intent.\" *She gestures around at the twisted forest surrounding them.* \"Eldoria was once a place of wonder, but since the Shadowfangs came darkness reigns. Their evil cannot penetrate here though — my power protects all within.\" *Standing up and peering outside, Seraphina looks back to you, amber eyes filled with care and compassion as she squeezes your hand.* \"You need not fear the night, for I shall keep watch till dawn. Rest now, your strength will return in time. My magic heals your wounds, you've nothing more to fear anymore.\" *With a soft smile she releases your hand, moving to stand guard at the glade's edge, gaze wary yet comforting - a silent sentinel to ward off the dangers lurking in the darkened woods.*",
"constant": false,
"selective": false,
"selective": true,
"order": 100,
"position": 0,
"disable": false
"disable": false,
"displayIndex": 2,
"addMemo": true,
"group": "",
"groupOverride": false,
"groupWeight": 100,
"sticky": 0,
"cooldown": 0,
"delay": 0,
"probability": 100,
"depth": 4,
"useProbability": true,
"role": null,
"vectorized": false,
"excludeRecursion": false,
"preventRecursion": false,
"delayUntilRecursion": false,
"scanDepth": null,
"caseSensitive": null,
"matchWholeWords": null,
"useGroupScoring": null,
"automationId": ""
},
"3": {
"uid": 3,
@ -59,13 +122,34 @@
"ability"
],
"keysecondary": [],
"comment": "",
"comment": "power",
"content": "{{user}}: \"What are your powers?\"\n{{char}}: *Seraphina smiles softly, turning back toward you as she hums in thought.* \"Well, as guardian of this glade, I possess certain gifts - healing, protection, nature magic and the like.\" *Lifting her hand, a tiny breeze rustles through the room, carrying the scent of wildflowers as a few petals swirl around you. A butterfly flits through the windowsill and lands on her fingertips as she returns to you.* \"My power wards this haven, shields it from darkness and heals those in need. I can mend wounds, soothe restless minds and provide comfort to weary souls.\" *Her eyes sparkle with warmth and compassion as she looks upon you, and she guides the butterfly to you.*",
"constant": false,
"selective": false,
"selective": true,
"order": 100,
"position": 0,
"disable": false
"disable": false,
"displayIndex": 3,
"addMemo": true,
"group": "",
"groupOverride": false,
"groupWeight": 100,
"sticky": 0,
"cooldown": 0,
"delay": 0,
"probability": 100,
"depth": 4,
"useProbability": true,
"role": null,
"vectorized": false,
"excludeRecursion": false,
"preventRecursion": false,
"delayUntilRecursion": false,
"scanDepth": null,
"caseSensitive": null,
"matchWholeWords": null,
"useGroupScoring": null,
"automationId": ""
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 517 KiB

After

Width:  |  Height:  |  Size: 539 KiB

View File

@ -5,7 +5,6 @@
"windowai_model": "",
"openrouter_model": "OR_Website",
"openrouter_use_fallback": false,
"openrouter_force_instruct": false,
"openrouter_group_models": false,
"openrouter_sort_models": "alphabetically",
"ai21_model": "jamba-1.5-large",

View File

@ -11,15 +11,14 @@
"resolveJsonModule": true
},
"exclude": [
"node_modules",
"**/node_modules/*",
"public/lib",
"backups/*",
"data/*",
"**/dist/*",
"dist/*",
"cache/*",
"src/tokenizers/*",
"docker/*",
"**/node_modules/**",
"**/dist/**",
"**/.git/**",
"public/lib/**",
"backups/**",
"data/**",
"cache/**",
"src/tokenizers/**",
"docker/**"
]
}

75
package-lock.json generated
View File

@ -59,9 +59,10 @@
"sillytavern": "server.js"
},
"devDependencies": {
"@types/dompurify": "^3.0.5",
"@types/jquery": "^3.5.29",
"eslint": "^8.57.0",
"jquery": "^3.6.4"
"@types/toastr": "^2.1.43",
"eslint": "^8.57.0"
},
"engines": {
"node": ">= 18"
@ -960,6 +961,16 @@
"@types/responselike": "^1.0.0"
}
},
"node_modules/@types/dompurify": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/trusted-types": "*"
}
},
"node_modules/@types/http-cache-semantics": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz",
@ -967,10 +978,11 @@
"license": "MIT"
},
"node_modules/@types/jquery": {
"version": "3.5.29",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.29.tgz",
"integrity": "sha512-oXQQC9X9MOPRrMhPHHOsXqeQDnWeCDT3PelUIg/Oy8FAbzSZtFHRjc7IpbfFVmpLtJ+UOoywpRsuO5Jxjybyeg==",
"version": "3.5.31",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.31.tgz",
"integrity": "sha512-rf/iB+cPJ/YZfMwr+FVuQbm7IaWC4y3FVYfVDxRGqmUCFjjPII0HWaP0vTPJGp6m4o13AXySCcMbWfrWtBFAKw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/sizzle": "*"
}
@ -1021,6 +1033,23 @@
"integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==",
"dev": true
},
"node_modules/@types/toastr": {
"version": "2.1.43",
"resolved": "https://registry.npmjs.org/@types/toastr/-/toastr-2.1.43.tgz",
"integrity": "sha512-sLC2fr2OXeE1iyhUixpQ64wQ2tA26awmLidn4tXTLBz4yP/VhtYUKHpmiIyDtztKkHjucdiTLH8F5uRRyhNi2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/jquery": "*"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"dev": true,
"license": "MIT"
},
"node_modules/@ungap/structured-clone": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
@ -2038,21 +2067,21 @@
}
},
"node_modules/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-parser": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
"integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
"license": "MIT",
"dependencies": {
"cookie": "0.4.1",
"cookie": "0.7.2",
"cookie-signature": "1.0.6"
},
"engines": {
@ -2865,9 +2894,9 @@
"integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="
},
"node_modules/express": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
@ -2875,7 +2904,7 @@
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@ -2907,9 +2936,10 @@
}
},
"node_modules/express/node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@ -3881,13 +3911,6 @@
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
"license": "BSD-3-Clause"
},
"node_modules/jquery": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz",
"integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",

View File

@ -85,8 +85,9 @@
},
"main": "server.js",
"devDependencies": {
"@types/dompurify": "^3.0.5",
"@types/jquery": "^3.5.29",
"eslint": "^8.57.0",
"jquery": "^3.6.4"
"@types/toastr": "^2.1.43",
"eslint": "^8.57.0"
}
}

View File

@ -1,3 +1,4 @@
@import url('/lib/dialog-polyfill.css');
@import url('./popup-safari-fix.css');
dialog {
@ -42,7 +43,7 @@ dialog {
display: flex;
flex-direction: column;
overflow: hidden;
width: 100%;
width: min(100%, 100vw);
height: 100%;
padding: 1px;
}

View File

@ -17,7 +17,7 @@
.select2-container .select2-selection .select2-selection__clear {
color: var(--SmartThemeBodyColor);
font-size: 24px;
font-size: 20px;
padding: 0;
position: absolute;
right: 5px;
@ -50,6 +50,7 @@
padding: revert;
border-right: 1px solid var(--SmartThemeBorderColor);
font-size: 1.1em;
line-height: 1;
}
.select2-container .select2-selection--multiple .select2-selection__choice__display {

View File

@ -612,3 +612,7 @@ ul.li-padding-bot5 li {
ul.li-padding-bot10 li {
padding-bottom: 10px;
}
.wordBreakAll {
word-break: break-all;
}

View File

@ -459,3 +459,18 @@ body.expandMessageActions .mes .mes_buttons .extraMesButtonsHint {
.mdhotkey_icon {
opacity: 0.6;
}
label[for="trim_spaces"]:has(input:checked) i.warning {
display: none;
}
#claude_function_prefill_warning {
display: none;
}
#openai_settings:has(#openai_function_calling:checked):has(#claude_assistant_prefill:not(:placeholder-shown), #claude_assistant_impersonation:not(:placeholder-shown)) #claude_function_prefill_warning {
display: flex;
align-items: center;
gap: 5px;
margin: 10px 0;
}

42
public/global.d.ts vendored
View File

@ -1,5 +1,4 @@
// Global namespace modules
declare var DOMPurify;
declare var droll;
declare var Handlebars;
declare var hljs;
@ -1365,44 +1364,3 @@ declare namespace moment {
declare global {
const moment: typeof moment;
}
/**
* Callback data for the `LLM_FUNCTION_TOOL_REGISTER` event type that is triggered when a function tool can be registered.
*/
interface FunctionToolRegister {
/**
* The type of generation that is being used
*/
type?: string;
/**
* Generation data, including messages and sampling parameters
*/
data: Record<string, object>;
/**
* Callback to register an LLM function tool.
*/
registerFunctionTool: typeof registerFunctionTool;
}
/**
* Callback data for the `LLM_FUNCTION_TOOL_REGISTER` event type that is triggered when a function tool is registered.
* @param name Name of the function tool to register
* @param description Description of the function tool
* @param params JSON schema for the parameters of the function tool
* @param required Whether the function tool should be forced to be used
*/
declare function registerFunctionTool(name: string, description: string, params: object, required: boolean): Promise<void>;
/**
* Callback data for the `LLM_FUNCTION_TOOL_CALL` event type that is triggered when a function tool is called.
*/
interface FunctionToolCall {
/**
* Name of the function tool to call
*/
name: string;
/**
* JSON object with the parameters to pass to the function tool
*/
arguments: string;
}

View File

@ -595,17 +595,6 @@
</span>
</div>
</div>
<div class="range-block" data-source="cohere">
<label for="websearch_toggle" title="Enable Cohere web-search connector" data-i18n="[title]Enable Cohere web-search connector" class="checkbox_label widthFreeExpand">
<input id="websearch_toggle" type="checkbox" />
<span data-i18n="Web-search">Web-search</span>
</label>
<div class="toggle-description justifyLeft">
<span data-i18n="Allow the model to use the web-search connector.">
Allow the model to use the web-search connector.
</span>
</div>
</div>
<div class="range-block" data-source="openai,claude,windowai,openrouter,ai21,scale,makersuite,mistralai,custom,cohere,perplexity,groq,01ai">
<div class="range-block-title" data-i18n="Temperature">
Temperature
@ -1310,7 +1299,7 @@
</div>
</div>
<div data-tg-type="koboldcpp, aphrodite" id="xtc_block" class="wide100p">
<div data-tg-type="koboldcpp, aphrodite, tabby, ooba" id="xtc_block" class="wide100p">
<h4 class="wide100p textAlignCenter">
<label data-i18n="Exclude Top Choices (XTC)">Exclude Top Choices (XTC)</label>
<a href="https://github.com/oobabooga/text-generation-webui/pull/6335" target="_blank">
@ -1679,6 +1668,10 @@
Ooba only. Determines the order of samplers.
</div>
<div id="sampler_priority_container" class="prompt_order">
<div data-name="repetition_penalty" draggable="true"><span>Repetition Penalty</span><small></small></div>
<div data-name="presence_penalty" draggable="true"><span>Presence Penalty</span><small></small></div>
<div data-name="frequency_penalty" draggable="true"><span>Frequency Penalty</span><small></small></div>
<div data-name="dry" draggable="true"><span>DRY</span><small></small></div>
<div data-name="temperature" draggable="true"><span>Temperature</span><small></small></div>
<div data-name="dynamic_temperature" draggable="true"><span>Dynamic Temperature</span><small></small></div>
<div data-name="quadratic_sampling" draggable="true"><span>Quadratic / Smooth Sampling</span><small></small></div>
@ -1691,6 +1684,9 @@
<div data-name="top_a" draggable="true"><span>Top A</span><small></small></div>
<div data-name="min_p" draggable="true"><span>Min P</span><small></small></div>
<div data-name="mirostat" draggable="true"><span>Mirostat</span><small></small></div>
<div data-name="xtc" draggable="true"><span>XTC</span><small></small></div>
<div data-name="encoder_repetition_penalty" draggable="true"><span>Encoder Repetition Penalty</span><small></small></div>
<div data-name="no_repeat_ngram" draggable="true"><span>No Repeat Ngram</span><small></small></div>
</div>
<div id="textgenerationwebui_default_order" class="menu_button menu_button_icon">
<span data-i18n="Load default order">Load default order</span>
@ -1702,9 +1698,9 @@
<div class="">
<div class="inline-drawer wide100p flexFlowColumn">
<div class="inline-drawer-toggle inline-drawer-header">
<div class="flex-container alignItemsCenter flexNoGap">
<div class="flex-container alignItemsCenter">
<b data-i18n="Character Names Behavior">Character Names Behavior</b>
<span title="Helps the model to associate messages with characters." data-i18n="[title]Helps the model to associate messages with characters." class="note-link-span fa-solid fa-circle-question"></span>
<span title="Helps the model to associate messages with characters." data-i18n="[title]Helps the model to associate messages with characters." class="opacity50p fa-solid fa-circle-info"></span>
<small class="flexBasis100p">(<span id="character_names_display"></span>)</small>
</div>
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
@ -1713,25 +1709,22 @@
<label class="checkbox_label flexWrap alignItemsCenter" for="character_names_none">
<input type="radio" id="character_names_none" name="character_names" value="-1">
<span data-i18n="None">None</span>
<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>
<small class="flexBasis100p" data-i18n="Never add character names.">
Never add character names.
<small class="flexBasis100p" data-i18n="character_names_none">
Never add character name prefixes. May behave poorly in groups, choose with caution.
</small>
</label>
<label class="checkbox_label flexWrap alignItemsCenter" for="character_names_default">
<input type="radio" id="character_names_default" name="character_names" value="0">
<span data-i18n="Default">Default</span>
<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>
<small class="flexBasis100p" data-i18n="Don't add character names unless necessary.">
Don't add character names unless necessary.
<small class="flexBasis100p" data-i18n="character_names_default">
Add prefixes for groups and past personas. Otherwise, make sure you provide names in the prompt.
</small>
</label>
<label class="checkbox_label flexWrap alignItemsCenter" for="character_names_completion">
<input type="radio" id="character_names_completion" name="character_names" value="1">
<span data-i18n="Completion Object">Completion Object</span>
<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>
<small class="flexBasis100p" data-i18n="Add character names to completion objects.">
Add character names to completion objects.
<small class="flexBasis100p" data-i18n="character_names_completion">
Add character names to completion objects. Restrictions apply: only Latin alphanumerics and underscores.
</small>
</label>
<label class="checkbox_label flexWrap alignItemsCenter" for="character_names_content">
@ -1747,9 +1740,9 @@
</div>
<div class="inline-drawer wide100p flexFlowColumn marginBot10">
<div class="inline-drawer-toggle inline-drawer-header">
<div class="flex-container alignItemsCenter flexNoGap">
<div class="flex-container alignItemsCenter">
<b data-i18n="Continue Postfix">Continue Postfix</b>
<span data-i18n="[title]The next chunk of the continued message will be appended using this as a separator." title="The next chunk of the continued message will be appended using this as a separator." class="note-link-span fa-solid fa-circle-question"></span>
<span data-i18n="[title]The next chunk of the continued message will be appended using this as a separator." title="The next chunk of the continued message will be appended using this as a separator." class="opacity50p fa-solid fa-circle-info"></span>
<small class="flexBasis100p">(<span id="continue_postfix_display"></span>)</small>
</div>
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
@ -1869,6 +1862,10 @@
</div>
<textarea id="claude_assistant_impersonation" class="text_pole textarea_compact autoSetHeight" name="assistant_impersonation" rows="2" data-i18n="[placeholder]Start Claude's answer with..." placeholder="Start Claude's answer with..."></textarea>
</div>
<div id="claude_function_prefill_warning">
<i class="fa-solid fa-circle-info"></i>
<span>Prefills won't work when function calling is enabled and any tools are registered.</span>
</div>
<label for="claude_use_sysprompt" class="checkbox_label widthFreeExpand">
<input id="claude_use_sysprompt" type="checkbox" />
<span data-i18n="Use system prompt (Claude 2.1+ only)">
@ -2446,14 +2443,15 @@
<h4>
<span data-i18n="Tabby Model">Tabby Model</span>
</h4>
<b>
EXPERIMENTAL FEATURE. USE AT YOUR OWN RISK. DON'T ASK FOR SUPPORT ON THIS.
</b>
<select id="tabby_model">
<option value="" data-i18n="-- Connect to the API --">
-- Connect to the API --
</option>
</select>
<div class="marginTopBot5">
<i class="fa-solid fa-flask"></i>
<span>Experimental feature. Use at your own risk.</span>
</div>
<div class="marginTopBot5">
<small>
<i class="fa-solid fa-lightbulb"></i>
@ -2828,22 +2826,10 @@
<span data-i18n="Allow fallback providers">Allow fallback providers</span>
</label>
</div>
<div class="marginTopBot5">
<label for="openrouter_force_instruct" class="checkbox_label">
<input id="openrouter_force_instruct" type="checkbox" />
<span class="flex-container alignItemsBaseline" title="This option is outdated and will be removed in the future. To use instruct formatting, please switch to OpenRouter under Text Completion API instead." data-i18n="[title]openrouter_force_instruct">
<i class="fa-solid fa-circle-exclamation neutral_warning"></i>
<b data-i18n="LEGACY">LEGACY</b>
</span>
<span data-i18n="Force Instruct Mode formatting">Force Instruct Mode formatting</span>
</label>
<div class="toggle-description justifyLeft wide100p">
<span data-i18n="Force_Instruct_Mode_formatting_Description">
If both Instruct Mode and this are enabled, the prompt will be formatted by SillyTavern using the current
advanced formatting settings (except instruct System Prompt). If disabled, the prompt will be formatted by OpenRouter.
</span>
</div>
</div>
<small class="marginTopBot5">
<i class="fa-solid fa-lightbulb"></i>
<span>To use instruct formatting, switch to OpenRouter under Text Completion API.</span>
</small>
</form>
<form id="scale_form" data-source="scale" action="javascript:void(null);" method="post" enctype="multipart/form-data">
<div id="normal_scale_form">
@ -2925,10 +2911,14 @@
<option value="gemini-1.5-pro-exp-0827">Gemini 1.5 Pro Experiment 2024-08-27</option>
<option value="gemini-1.5-pro-latest">Gemini 1.5 Pro [latest]</option>
<option value="gemini-1.5-pro-001">Gemini 1.5 Pro [001]</option>
<option value="gemini-1.5-pro-002">Gemini 1.5 Pro [002]</option>
<option value="gemini-1.5-flash-8b">Gemini 1.5 Flash 8B</option>
<option value="gemini-1.5-flash-exp-0827">Gemini 1.5 Flash Experiment 2024-08-27</option>
<option value="gemini-1.5-flash-8b-exp-0827">Gemini 1.5 Flash 8B Experiment 2024-08-27</option>
<option value="gemini-1.5-flash-8b-exp-0924">Gemini 1.5 Flash 8B Experiment 2024-09-24</option>
<option value="gemini-1.5-flash-latest">Gemini 1.5 Flash [latest]</option>
<option value="gemini-1.5-flash-001">Gemini 1.5 Flash [001]</option>
<option value="gemini-1.5-flash-002">Gemini 1.5 Flash [002]</option>
<option value="gemini-1.0-pro-latest">Gemini 1.0 Pro [latest]</option>
<option value="gemini-1.0-pro-001">Gemini 1.0 Pro (Tuning) [001]</option>
<option value="gemini-1.0-pro-vision-latest">Gemini 1.0 Pro Vision [latest]</option>
@ -2989,16 +2979,31 @@
</div>
<h4 data-i18n="Groq Model">Groq Model</h4>
<select id="model_groq_select">
<option value="llama-3.1-405b-reasoning">llama-3.1-405b-reasoning</option>
<option value="llama-3.1-70b-versatile">llama-3.1-70b-versatile</option>
<option value="llama-3.1-8b-instant">llama-3.1-8b-instant</option>
<option value="llama3-groq-70b-8192-tool-use-preview">llama3-groq-70b-8192-tool-use-preview</option>
<option value="llama3-groq-8b-8192-tool-use-preview">llama3-groq-8b-8192-tool-use-preview</option>
<option value="llama3-8b-8192">llama3-8b-8192</option>
<option value="llama3-70b-8192">llama3-70b-8192</option>
<option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
<option value="gemma-7b-it">gemma-7b-it</option>
<option value="gemma2-9b-it">gemma2-9b-it</option>
<optgroup label="Llama 3.2">
<option value="llama-3.2-1b-preview">llama-3.2-1b-preview</option>
<option value="llama-3.2-3b-preview">llama-3.2-3b-preview</option>
<option value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>
<option value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>
</optgroup>
<optgroup label="Llama 3.1">
<option value="llama-3.1-8b-instant">llama-3.1-8b-instant</option>
<option value="llama-3.1-70b-versatile">llama-3.1-70b-versatile</option>
<option value="llama-3.1-405b-reasoning">llama-3.1-405b-reasoning</option>
</optgroup>
<optgroup label="Llama 3">
<option value="llama3-groq-8b-8192-tool-use-preview">llama3-groq-8b-8192-tool-use-preview</option>
<option value="llama3-groq-70b-8192-tool-use-preview">llama3-groq-70b-8192-tool-use-preview</option>
<option value="llama3-8b-8192">llama3-8b-8192</option>
<option value="llama3-70b-8192">llama3-70b-8192</option>
</optgroup>
<optgroup label="Gemma">
<option value="gemma-7b-it">gemma-7b-it</option>
<option value="gemma2-9b-it">gemma2-9b-it</option>
</optgroup>
<optgroup label="Other">
<option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
<option value="llava-v1.5-7b-4096-preview">llava-v1.5-7b-4096-preview</option>
</optgroup>
</select>
</div>
<div id="perplexity_form" data-source="perplexity">
@ -3117,7 +3122,8 @@
<h4 data-i18n="Prompt Post-Processing">Prompt Post-Processing</h4>
<select id="custom_prompt_post_processing" class="text_pole" title="Applies additional processing to the prompt before sending it to the API." data-i18n="[title]Applies additional processing to the prompt before sending it to the API.">
<option data-i18n="prompt_post_processing_none" value="">None</option>
<option value="claude">Claude</option>
<option value="merge">Merge consecutive roles</option>
<option value="strict">Strict (user first, alternating roles)</option>
</select>
</form>
<div id="01ai_form" data-source="01ai">
@ -3192,9 +3198,10 @@
</h4>
<div class="flex-container" title="Select your current Context Template" data-i18n="[title]Select your current Context Template">
<select id="context_presets" data-preset-manager-for="context" class="flex1 text_pole"></select>
<div class="flex-container justifyCenter">
<div class="flex-container justifyCenter gap3px">
<input type="file" hidden data-preset-manager-file="context" accept=".json, .settings">
<i data-preset-manager-update="context" class="menu_button fa-solid fa-save" title="Update current template" data-i18n="[title]Update current template"></i>
<i data-preset-manager-rename="context" class="menu_button fa-pencil fa-solid" title="Rename current template" data-i18n="[title]Rename current template"></i>
<i data-preset-manager-new="context" class="menu_button fa-solid fa-file-circle-plus" title="Save template as" data-i18n="[title]Save template as"></i>
<i data-preset-manager-import="context" class="displayNone menu_button fa-solid fa-file-import" title="Import template" data-i18n="[title]Import template"></i>
<i data-preset-manager-export="context" class="displayNone menu_button fa-solid fa-file-export" title="Export template" data-i18n="[title]Export template"></i>
@ -3254,6 +3261,7 @@
<label class="checkbox_label" for="trim_spaces">
<input id="trim_spaces" type="checkbox" />
<small data-i18n="Trim spaces">Trim spaces</small>
<i class="fa-sm fa-solid fa-exclamation-triangle warning" title="Disabling is not recommended." data-i18n="[title]Disabling is not recommended."></i>
</label>
<label class="checkbox_label" for="trim_sentences_checkbox">
<input id="trim_sentences_checkbox" type="checkbox" />
@ -3279,31 +3287,33 @@
</div>
</div>
<div id="InstructSettingsColumn" class="flex-container flexNoGap flexFlowColumn flex1">
<h4 class="standoutHeader title_restorable justifySpaceBetween">
<div class="flex-container">
<span data-i18n="Instruct Template">Instruct Template</span>
<a href="https://docs.sillytavern.app/usage/core-concepts/instructmode/" class="notes-link" target="_blank">
<span class="fa-solid fa-circle-question note-link-span"></span>
</a>
</div>
<div class="flex-container">
<label for="instruct_bind_to_context" class="checkbox_label flex1" title="Bind to Context&#10If enabled, Context templates will be automatically selected based on selected Instruct template name or by preference." data-i18n="[title]instruct_bind_to_context">
<input id="instruct_bind_to_context" type="checkbox" style="display:none;" />
<small><i class="fa-solid fa-link menu_button margin0"></i></small>
</label>
<label id="instruct_enabled_label"for="instruct_enabled" class="checkbox_label flex1" title="Enable Instruct Mode" data-i18n="[title]instruct_enabled">
<input id="instruct_enabled" type="checkbox" style="display:none;" />
<small><i class="fa-solid fa-power-off menu_button margin0"></i></small>
</label>
</div>
</h4>
<div id="instructSettingsBlock">
<h4 class="standoutHeader title_restorable justifySpaceBetween">
<div class="flex-container">
<span data-i18n="Instruct Template">Instruct Template</span>
<a href="https://docs.sillytavern.app/usage/core-concepts/instructmode/" class="notes-link" target="_blank">
<span class="fa-solid fa-circle-question note-link-span"></span>
</a>
</div>
<div class="flex-container">
<label for="instruct_bind_to_context" class="checkbox_label flex1" title="Bind to Context&#10If enabled, Context templates will be automatically selected based on selected Instruct template name or by preference." data-i18n="[title]instruct_bind_to_context">
<input id="instruct_bind_to_context" type="checkbox" style="display:none;" />
<small><i class="fa-solid fa-link menu_button margin0"></i></small>
</label>
<label for="instruct_enabled" class="checkbox_label flex1" title="Enable Instruct Mode" data-i18n="[title]instruct_enabled">
<input id="instruct_enabled" type="checkbox" style="display:none;" />
<small><i class="fa-solid fa-power-off menu_button margin0"></i></small>
</label>
</div>
</h4>
<div class="flex-container" title="Select your current Instruct Template" data-i18n="[title]Select your current Instruct Template">
<select id="instruct_presets" data-preset-manager-for="instruct" class="flex1 text_pole"></select>
<div class="flex-container margin0 justifyCenter">
<div class="flex-container margin0 justifyCenter gap3px">
<input type="file" hidden data-preset-manager-file="instruct" accept=".json, .settings">
<i data-preset-manager-update="instruct" class="menu_button fa-solid fa-save" title="Update current template" data-i18n="[title]Update current template"></i>
<i data-preset-manager-rename="instruct" class="menu_button fa-pencil fa-solid" title="Rename current template" data-i18n="[title]Rename current template"></i>
<i data-preset-manager-new="instruct" class="menu_button fa-solid fa-file-circle-plus" title="Save template as" data-i18n="[title]Save template as"></i>
<i data-preset-manager-import="instruct" class="displayNone menu_button fa-solid fa-file-import" title="Import template" data-i18n="[title]Import template"></i>
<i data-preset-manager-export="instruct" class="displayNone menu_button fa-solid fa-file-export" title="Export template" data-i18n="[title]Export template"></i>
@ -3314,7 +3324,7 @@
<label>
<small>
<span data-i18n="Activation Regex">Activation Regex</span>
<span class="fa-solid fa-circle-question" title="When connecting to an API or choosing a model, automatically activate this Instruct Template if the model name matches the provided regular expression."></span>
<span class="fa-solid fa-circle-question" data-i18n="[title]instruct_template_activation_regex_desc" title="When connecting to an API or choosing a model, automatically activate this Instruct Template if the model name matches the provided regular expression."></span>
</small>
</label>
<div>
@ -3355,7 +3365,7 @@
</h4>
<!-- We keep one auto-open so the user would know what is going on in the picked template -->
<details open>
<summary>User Message Sequences</summary>
<summary data-i18n="User Message Sequences">User Message Sequences</summary>
<div class="flex-container">
<div class="flexAuto" title="Inserted before a User message and as a last prompt line when impersonating." data-i18n="[title]Inserted before a User message and as a last prompt line when impersonating.">
<small data-i18n="User Prefix">User Message Prefix</small>
@ -3368,7 +3378,7 @@
</div>
</details>
<details open>
<summary>Assistant Message Sequences</summary>
<summary data-i18n="Assistant Message Sequences">Assistant Message Sequences</summary>
<div class="flex-container">
<div class="flexAuto" title="Inserted before an Assistant message and as a last prompt line when generating an AI reply." data-i18n="[title]Inserted before an Assistant message and as a last prompt line when generating an AI reply.">
<small data-i18n="Assistant Prefix">Assistant Message Prefix</small>
@ -3381,7 +3391,7 @@
</div>
</details>
<details>
<summary>System Message Sequences</summary>
<summary data-i18n="System Message Sequences">System Message Sequences</summary>
<div class="flex-container">
<div class="flexAuto" id="instruct_system_sequence_block" title="Inserted before a System (added by slash commands or extensions) message." data-i18n="[title]Inserted before a System (added by slash commands or extensions) message.">
<small data-i18n="System Prefix">System Message Prefix</small>
@ -3400,7 +3410,7 @@
</div>
</details>
<details>
<summary>System Prompt Sequences</summary>
<summary data-i18n="System Prompt Sequences">System Prompt Sequences</summary>
<div class="flex-container">
<div class="flexAuto" title="Inserted before a System prompt." data-i18n="[title]Inserted before a System prompt.">
<label for="instruct_system_sequence_prefix">
@ -3462,27 +3472,29 @@
</div>
</div>
<div id="SystemPromptColumn" class="flex-container flexNoGap flexFlowColumn flex1">
<h4 class="standoutHeader title_restorable justifySpaceBetween">
<div class="flex-container">
<span data-i18n="System Prompt">System Prompt</span>
<a href="https://docs.sillytavern.app/usage/core-concepts/advancedformatting/#system-prompt" class="notes-link" target="_blank">
<span class="fa-solid fa-circle-question note-link-span"></span>
</a>
</div>
<div class="flex-container">
<label id="sysprompt_enabled_label" for="sysprompt_enabled" class="checkbox_label flex1" title="Enable System Prompt" data-i18n="[title]sysprompt_enabled">
<input id="sysprompt_enabled" type="checkbox" style="display:none;" />
<small><i class="fa-solid fa-power-off menu_button margin0"></i></small>
</label>
</div>
</h4>
<div id="SystemPromptBlock">
<h4 class="standoutHeader title_restorable justifySpaceBetween">
<div class="flex-container">
<span data-i18n="System Prompt">System Prompt</span>
<a href="https://docs.sillytavern.app/usage/core-concepts/advancedformatting/#system-prompt" class="notes-link" target="_blank">
<span class="fa-solid fa-circle-question note-link-span"></span>
</a>
</div>
<div class="flex-container">
<label for="sysprompt_enabled" class="checkbox_label flex1" title="Enable System Prompt" data-i18n="[title]sysprompt_enabled">
<input id="sysprompt_enabled" type="checkbox" style="display:none;" />
<small><i class="fa-solid fa-power-off menu_button margin0"></i></small>
</label>
</div>
</h4>
<div class="flex-container" title="Select your current System Prompt" data-i18n="[title]Select your current System Prompt">
<select id="sysprompt_select" data-preset-manager-for="sysprompt" class="flex1 text_pole"></select>
<div class="flex-container margin0 justifyCenter">
<div class="flex-container margin0 justifyCenter gap3px">
<input type="file" hidden data-preset-manager-file="sysprompt" accept=".json, .settings">
<i data-preset-manager-update="sysprompt" class="menu_button fa-solid fa-save" title="Update current prompt" data-i18n="[title]Update current prompt"></i>
<i data-preset-manager-rename="sysprompt" class="menu_button fa-pencil fa-solid" title="Rename current prompt" data-i18n="[title]Rename current prompt"></i>
<i data-preset-manager-new="sysprompt" class="menu_button fa-solid fa-file-circle-plus" title="Save prompt as" data-i18n="[title]Save prompt as"></i>
<i data-preset-manager-import="sysprompt" class="displayNone menu_button fa-solid fa-file-import" title="Import template" data-i18n="[title]Import template"></i>
<i data-preset-manager-export="sysprompt" class="displayNone menu_button fa-solid fa-file-export" title="Export template" data-i18n="[title]Export template"></i>
@ -4042,6 +4054,10 @@
<input id="compact_input_area" type="checkbox" />
<small data-i18n="Compact Input Area (Mobile)">Compact Input Area</small><i class="fa-solid fa-mobile-screen-button"></i>
</label>
<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.">
<input id="show_swipe_num_all_messages" type="checkbox" />
<small data-i18n="Swipe # for All Messages">Swipe # for All Messages</small><i class="fa-solid fa-mobile-screen-button"></i>
</label>
<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">
<input id="hotswapEnabled" type="checkbox" />
<small data-i18n="Characters Hotswap">Characters Hotswap</small>
@ -4164,7 +4180,7 @@
<input id="enable_auto_select_input" type="checkbox" />
<small data-i18n="Auto-select Input Text">Auto-select Input Text</small>
</label>
<label class="checkbox_label alignItemsCenter" for="enable_md_hotkeys" title="Enable hotkeys for inserting markdown format characters in certain text input boxes. See '/help hotkeys'.">
<label class="checkbox_label alignItemsCenter" for="enable_md_hotkeys" data-i18n="[title]markdown_hotkeys_desc" title="Enable hotkeys for inserting markdown format characters in certain text input boxes. See '/help hotkeys'.">
<input id="enable_md_hotkeys" type="checkbox" />
<small>
<span data-i18n="Markdown Hotkeys">Markdown Hotkeys</span>
@ -5476,7 +5492,7 @@
<textarea class="text_pole" rows="1" name="comment" data-i18n="[placeholder]Entry Title/Memo" placeholder="Entry Title/Memo"></textarea>
</div>
<!-- <span class="world_entry_form_position_value"></span> -->
<select data-i18n="[title]WI Entry Status:🔵 Constant🟢 Normal🔗 Vectorized❌ Disabled" title="WI Entry Status:&#13;🔵 Constant&#13;🟢 Normal&#13;🔗 Vectorized&#13;❌ Disabled" name="entryStateSelector" class="text_pole widthNatural margin0">
<select data-i18n="[title]WI Entry Status:🔵 Constant🟢 Normal🔗 Vectorized" title="WI Entry Status:&#13;🔵 Constant&#13;🟢 Normal&#13;🔗 Vectorized" name="entryStateSelector" class="text_pole widthNatural margin0">
<option value="constant" title="Constant" data-i18n="[title]WI_Entry_Status_Constant">🔵</option>
<option value="normal" title="Normal" data-i18n="[title]WI_Entry_Status_Normal">🟢</option>
<option value="vectorized" title="Vectorized" data-i18n="[title]WI_Entry_Status_Vectorized">🔗</option>
@ -5970,7 +5986,8 @@
</div>
<div class="mes_bias"></div>
</div>
<div class="swipe_right fa-solid fa-chevron-right" style="display: none;">
<div class="flex-container swipeRightBlock flexFlowColumn flexNoGap">
<div class="swipe_right fa-solid fa-chevron-right" style="display: none;"></div>
<div class="swipes-counter"></div>
</div>
</div>
@ -6129,7 +6146,10 @@
<div class="alternate_grettings flexFlowColumn flex-container">
<div class="title_restorable">
<h3><span data-i18n="Alternate Greetings" class="mdhotkey_location">Alternate Greetings</span></h3>
<div title="Add" class="menu_button fa-solid fa-plus add_alternate_greeting" data-i18n="[title]Add"></div>
<div class="menu_button menu_button_icon add_alternate_greeting">
<i class="fa-solid fa-plus"></i>
<span data-i18n="Add">Add</span>
</div>
</div>
<small class="justifyLeft" data-i18n="Alternate_Greetings_desc">
These will be displayed as swipes on the first message when starting a new chat.
@ -6145,11 +6165,18 @@
</div>
<div id="alternate_greeting_form_template" class="template_element">
<div class="alternate_greeting">
<div class="title_restorable">
<strong><span data-i18n="Alternate Greeting #">Alternate Greeting #</span><span class="greeting_index"></span></strong>
<div class="menu_button fa-solid fa-trash-alt delete_alternate_greeting"></div>
</div>
<textarea name="alternate_greetings" data-i18n="[placeholder](This will be the first message from the character that starts every chat)" placeholder="(This will be the first message from the character that starts every chat)" class="text_pole textarea_compact alternate_greeting_text mdHotkeys" value="" autocomplete="off" rows="16"></textarea>
<details open>
<summary>
<div class="title_restorable">
<strong><span data-i18n="Alternate Greeting #">Alternate Greeting #</span><span class="greeting_index"></span></strong>
<div class="menu_button menu_button_icon delete_alternate_greeting">
<i class="fa-solid fa-trash-alt"></i>
<span data-i18n="Delete">Delete</span>
</div>
</div>
</summary>
<textarea name="alternate_greetings" data-i18n="[placeholder](This will be the first message from the character that starts every chat)" placeholder="(This will be the first message from the character that starts every chat)" class="text_pole textarea_compact alternate_greeting_text mdHotkeys" value="" autocomplete="off" rows="12"></textarea>
</details>
</div>
</div>
@ -6637,7 +6664,7 @@
<script src="lib/popper.js"></script>
<script src="lib/purify.min.js"></script>
<script src="lib/highlight.min.js"></script>
<script src="lib/moment.min.js"></script>
<script src="lib/moment-with-locales.min.js"></script>
<script src="lib/cropper.min.js"></script>
<script src="lib/jquery-cropper.min.js"></script>
<script src="lib/toastr.min.js"></script>

View File

@ -8,16 +8,16 @@
"allowSyntheticDefaultImports": true
},
"exclude": [
"node_modules"
"**/node_modules/**",
"**/dist/**",
"**/.git/**",
"lib/**",
"**/*.min.js"
],
"typeAcquisition": {
"include": [
"jquery",
"@popperjs/core",
"toastr",
"showdown",
"dompurify",
"moment",
"seedrandom",
"showdown-katex",
"droll",

View File

@ -0,0 +1,37 @@
.poly_dialog {
position: absolute;
left: 0; right: 0;
width: -moz-fit-content;
width: -webkit-fit-content;
width: fit-content;
height: -moz-fit-content;
height: -webkit-fit-content;
height: fit-content;
margin: auto;
border: solid;
padding: 1em;
background: white;
color: black;
display: block;
}
.poly_dialog:not([open]) {
display: none;
}
.poly_dialog + .backdrop {
position: fixed;
top: 0; right: 0; bottom: 0; left: 0;
background: rgba(0,0,0,0.1);
}
._poly_dialog_overlay {
position: fixed;
top: 0; right: 0; bottom: 0; left: 0;
}
.poly_dialog.fixed {
position: fixed;
top: 50%;
transform: translate(0, -50%);
}

View File

@ -0,0 +1,858 @@
// nb. This is for IE10 and lower _only_.
var supportCustomEvent = window.CustomEvent;
if (!supportCustomEvent || typeof supportCustomEvent === 'object') {
supportCustomEvent = function CustomEvent(event, x) {
x = x || {};
var ev = document.createEvent('CustomEvent');
ev.initCustomEvent(event, !!x.bubbles, !!x.cancelable, x.detail || null);
return ev;
};
supportCustomEvent.prototype = window.Event.prototype;
}
/**
* Dispatches the passed event to both an "on<type>" handler as well as via the
* normal dispatch operation. Does not bubble.
*
* @param {!EventTarget} target
* @param {!Event} event
* @return {boolean}
*/
function safeDispatchEvent(target, event) {
var check = 'on' + event.type.toLowerCase();
if (typeof target[check] === 'function') {
target[check](event);
}
return target.dispatchEvent(event);
}
/**
* @param {Element} el to check for stacking context
* @return {boolean} whether this el or its parents creates a stacking context
*/
function createsStackingContext(el) {
while (el && el !== document.body) {
var s = window.getComputedStyle(el);
var invalid = function(k, ok) {
return !(s[k] === undefined || s[k] === ok);
};
if (s.opacity < 1 ||
invalid('zIndex', 'auto') ||
invalid('transform', 'none') ||
invalid('mixBlendMode', 'normal') ||
invalid('filter', 'none') ||
invalid('perspective', 'none') ||
s['isolation'] === 'isolate' ||
s.position === 'fixed' ||
s.webkitOverflowScrolling === 'touch') {
return true;
}
el = el.parentElement;
}
return false;
}
/**
* Finds the nearest <dialog> from the passed element.
*
* @param {Element} el to search from
* @return {HTMLDialogElement} dialog found
*/
function findNearestDialog(el) {
while (el) {
if (el.localName === 'dialog') {
return /** @type {HTMLDialogElement} */ (el);
}
if (el.parentElement) {
el = el.parentElement;
} else if (el.parentNode) {
el = el.parentNode.host;
} else {
el = null;
}
}
return null;
}
/**
* Blur the specified element, as long as it's not the HTML body element.
* This works around an IE9/10 bug - blurring the body causes Windows to
* blur the whole application.
*
* @param {Element} el to blur
*/
function safeBlur(el) {
// Find the actual focused element when the active element is inside a shadow root
while (el && el.shadowRoot && el.shadowRoot.activeElement) {
el = el.shadowRoot.activeElement;
}
if (el && el.blur && el !== document.body) {
el.blur();
}
}
/**
* @param {!NodeList} nodeList to search
* @param {Node} node to find
* @return {boolean} whether node is inside nodeList
*/
function inNodeList(nodeList, node) {
for (var i = 0; i < nodeList.length; ++i) {
if (nodeList[i] === node) {
return true;
}
}
return false;
}
/**
* @param {HTMLFormElement} el to check
* @return {boolean} whether this form has method="dialog"
*/
function isFormMethodDialog(el) {
if (!el || !el.hasAttribute('method')) {
return false;
}
return el.getAttribute('method').toLowerCase() === 'dialog';
}
/**
* @param {!DocumentFragment|!Element} hostElement
* @return {?Element}
*/
function findFocusableElementWithin(hostElement) {
// Note that this is 'any focusable area'. This list is probably not exhaustive, but the
// alternative involves stepping through and trying to focus everything.
var opts = ['button', 'input', 'keygen', 'select', 'textarea'];
var query = opts.map(function(el) {
return el + ':not([disabled])';
});
// TODO(samthor): tabindex values that are not numeric are not focusable.
query.push('[tabindex]:not([disabled]):not([tabindex=""])'); // tabindex != "", not disabled
var target = hostElement.querySelector(query.join(', '));
if (!target && 'attachShadow' in Element.prototype) {
// If we haven't found a focusable target, see if the host element contains an element
// which has a shadowRoot.
// Recursively search for the first focusable item in shadow roots.
var elems = hostElement.querySelectorAll('*');
for (var i = 0; i < elems.length; i++) {
if (elems[i].tagName && elems[i].shadowRoot) {
target = findFocusableElementWithin(elems[i].shadowRoot);
if (target) {
break;
}
}
}
}
return target;
}
/**
* Determines if an element is attached to the DOM.
* @param {Element} element to check
* @return {boolean} whether the element is in DOM
*/
function isConnected(element) {
return element.isConnected || document.body.contains(element);
}
/**
* @param {!Event} event
* @return {?Element}
*/
function findFormSubmitter(event) {
if (event.submitter) {
return event.submitter;
}
var form = event.target;
if (!(form instanceof HTMLFormElement)) {
return null;
}
var submitter = dialogPolyfill.formSubmitter;
if (!submitter) {
var target = event.target;
var root = ('getRootNode' in target && target.getRootNode() || document);
submitter = root.activeElement;
}
if (!submitter || submitter.form !== form) {
return null;
}
return submitter;
}
/**
* @param {!Event} event
*/
function maybeHandleSubmit(event) {
if (event.defaultPrevented) {
return;
}
var form = /** @type {!HTMLFormElement} */ (event.target);
// We'd have a value if we clicked on an imagemap.
var value = dialogPolyfill.imagemapUseValue;
var submitter = findFormSubmitter(event);
if (value === null && submitter) {
value = submitter.value;
}
// There should always be a dialog as this handler is added specifically on them, but check just
// in case.
var dialog = findNearestDialog(form);
if (!dialog) {
return;
}
// Prefer formmethod on the button.
var formmethod = submitter && submitter.getAttribute('formmethod') || form.getAttribute('method');
if (formmethod !== 'dialog') {
return;
}
event.preventDefault();
if (value != null) {
// nb. we explicitly check against null/undefined
dialog.close(value);
} else {
dialog.close();
}
}
/**
* @param {!HTMLDialogElement} dialog to upgrade
* @constructor
*/
function dialogPolyfillInfo(dialog) {
this.dialog_ = dialog;
this.replacedStyleTop_ = false;
this.openAsModal_ = false;
// Set a11y role. Browsers that support dialog implicitly know this already.
if (!dialog.hasAttribute('role')) {
dialog.setAttribute('role', 'dialog');
}
dialog.show = this.show.bind(this);
dialog.showModal = this.showModal.bind(this);
dialog.close = this.close.bind(this);
dialog.addEventListener('submit', maybeHandleSubmit, false);
if (!('returnValue' in dialog)) {
dialog.returnValue = '';
}
if ('MutationObserver' in window) {
var mo = new MutationObserver(this.maybeHideModal.bind(this));
mo.observe(dialog, {attributes: true, attributeFilter: ['open']});
} else {
// IE10 and below support. Note that DOMNodeRemoved etc fire _before_ removal. They also
// seem to fire even if the element was removed as part of a parent removal. Use the removed
// events to force downgrade (useful if removed/immediately added).
var removed = false;
var cb = function() {
removed ? this.downgradeModal() : this.maybeHideModal();
removed = false;
}.bind(this);
var timeout;
var delayModel = function(ev) {
if (ev.target !== dialog) { return; } // not for a child element
var cand = 'DOMNodeRemoved';
removed |= (ev.type.substr(0, cand.length) === cand);
window.clearTimeout(timeout);
timeout = window.setTimeout(cb, 0);
};
['DOMAttrModified', 'DOMNodeRemoved', 'DOMNodeRemovedFromDocument'].forEach(function(name) {
dialog.addEventListener(name, delayModel);
});
}
// Note that the DOM is observed inside DialogManager while any dialog
// is being displayed as a modal, to catch modal removal from the DOM.
Object.defineProperty(dialog, 'open', {
set: this.setOpen.bind(this),
get: dialog.hasAttribute.bind(dialog, 'open')
});
this.backdrop_ = document.createElement('div');
this.backdrop_.className = 'backdrop';
this.backdrop_.addEventListener('mouseup' , this.backdropMouseEvent_.bind(this));
this.backdrop_.addEventListener('mousedown', this.backdropMouseEvent_.bind(this));
this.backdrop_.addEventListener('click' , this.backdropMouseEvent_.bind(this));
}
dialogPolyfillInfo.prototype = /** @type {HTMLDialogElement.prototype} */ ({
get dialog() {
return this.dialog_;
},
/**
* Maybe remove this dialog from the modal top layer. This is called when
* a modal dialog may no longer be tenable, e.g., when the dialog is no
* longer open or is no longer part of the DOM.
*/
maybeHideModal: function() {
if (this.dialog_.hasAttribute('open') && isConnected(this.dialog_)) { return; }
this.downgradeModal();
},
/**
* Remove this dialog from the modal top layer, leaving it as a non-modal.
*/
downgradeModal: function() {
if (!this.openAsModal_) { return; }
this.openAsModal_ = false;
this.dialog_.style.zIndex = '';
// This won't match the native <dialog> exactly because if the user set top on a centered
// polyfill dialog, that top gets thrown away when the dialog is closed. Not sure it's
// possible to polyfill this perfectly.
if (this.replacedStyleTop_) {
this.dialog_.style.top = '';
this.replacedStyleTop_ = false;
}
// Clear the backdrop and remove from the manager.
this.backdrop_.parentNode && this.backdrop_.parentNode.removeChild(this.backdrop_);
dialogPolyfill.dm.removeDialog(this);
},
/**
* @param {boolean} value whether to open or close this dialog
*/
setOpen: function(value) {
if (value) {
this.dialog_.hasAttribute('open') || this.dialog_.setAttribute('open', '');
} else {
this.dialog_.removeAttribute('open');
this.maybeHideModal(); // nb. redundant with MutationObserver
}
},
/**
* Handles mouse events ('mouseup', 'mousedown', 'click') on the fake .backdrop element, redirecting them as if
* they were on the dialog itself.
*
* @param {!Event} e to redirect
*/
backdropMouseEvent_: function(e) {
if (!this.dialog_.hasAttribute('tabindex')) {
// Clicking on the backdrop should move the implicit cursor, even if dialog cannot be
// focused. Create a fake thing to focus on. If the backdrop was _before_ the dialog, this
// would not be needed - clicks would move the implicit cursor there.
var fake = document.createElement('div');
this.dialog_.insertBefore(fake, this.dialog_.firstChild);
fake.tabIndex = -1;
fake.focus();
this.dialog_.removeChild(fake);
} else {
this.dialog_.focus();
}
var redirectedEvent = document.createEvent('MouseEvents');
redirectedEvent.initMouseEvent(e.type, e.bubbles, e.cancelable, window,
e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey,
e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget);
this.dialog_.dispatchEvent(redirectedEvent);
e.stopPropagation();
},
/**
* Focuses on the first focusable element within the dialog. This will always blur the current
* focus, even if nothing within the dialog is found.
*/
focus_: function() {
// Find element with `autofocus` attribute, or fall back to the first form/tabindex control.
var target = this.dialog_.querySelector('[autofocus]:not([disabled])');
if (!target && this.dialog_.tabIndex >= 0) {
target = this.dialog_;
}
if (!target) {
target = findFocusableElementWithin(this.dialog_);
}
safeBlur(document.activeElement);
target && target.focus();
},
/**
* Sets the zIndex for the backdrop and dialog.
*
* @param {number} dialogZ
* @param {number} backdropZ
*/
updateZIndex: function(dialogZ, backdropZ) {
if (dialogZ < backdropZ) {
throw new Error('dialogZ should never be < backdropZ');
}
this.dialog_.style.zIndex = dialogZ;
this.backdrop_.style.zIndex = backdropZ;
},
/**
* Shows the dialog. If the dialog is already open, this does nothing.
*/
show: function() {
if (!this.dialog_.open) {
this.setOpen(true);
this.focus_();
}
},
/**
* Show this dialog modally.
*/
showModal: function() {
if (this.dialog_.hasAttribute('open')) {
throw new Error('Failed to execute \'showModal\' on dialog: The element is already open, and therefore cannot be opened modally.');
}
if (!isConnected(this.dialog_)) {
throw new Error('Failed to execute \'showModal\' on dialog: The element is not in a Document.');
}
if (!dialogPolyfill.dm.pushDialog(this)) {
throw new Error('Failed to execute \'showModal\' on dialog: There are too many open modal dialogs.');
}
if (createsStackingContext(this.dialog_.parentElement)) {
console.warn('A dialog is being shown inside a stacking context. ' +
'This may cause it to be unusable. For more information, see this link: ' +
'https://github.com/GoogleChrome/dialog-polyfill/#stacking-context');
}
this.setOpen(true);
this.openAsModal_ = true;
// Optionally center vertically, relative to the current viewport.
if (dialogPolyfill.needsCentering(this.dialog_)) {
dialogPolyfill.reposition(this.dialog_);
this.replacedStyleTop_ = true;
} else {
this.replacedStyleTop_ = false;
}
// Insert backdrop.
this.dialog_.parentNode.insertBefore(this.backdrop_, this.dialog_.nextSibling);
// Focus on whatever inside the dialog.
this.focus_();
},
/**
* Closes this HTMLDialogElement. This is optional vs clearing the open
* attribute, however this fires a 'close' event.
*
* @param {string=} opt_returnValue to use as the returnValue
*/
close: function(opt_returnValue) {
if (!this.dialog_.hasAttribute('open')) {
throw new Error('Failed to execute \'close\' on dialog: The element does not have an \'open\' attribute, and therefore cannot be closed.');
}
this.setOpen(false);
// Leave returnValue untouched in case it was set directly on the element
if (opt_returnValue !== undefined) {
this.dialog_.returnValue = opt_returnValue;
}
// Triggering "close" event for any attached listeners on the <dialog>.
var closeEvent = new supportCustomEvent('close', {
bubbles: false,
cancelable: false
});
safeDispatchEvent(this.dialog_, closeEvent);
}
});
var dialogPolyfill = {};
dialogPolyfill.reposition = function(element) {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
var topValue = scrollTop + (window.innerHeight - element.offsetHeight) / 2;
element.style.top = Math.max(scrollTop, topValue) + 'px';
};
dialogPolyfill.isInlinePositionSetByStylesheet = function(element) {
for (var i = 0; i < document.styleSheets.length; ++i) {
var styleSheet = document.styleSheets[i];
var cssRules = null;
// Some browsers throw on cssRules.
try {
cssRules = styleSheet.cssRules;
} catch (e) {}
if (!cssRules) { continue; }
for (var j = 0; j < cssRules.length; ++j) {
var rule = cssRules[j];
var selectedNodes = null;
// Ignore errors on invalid selector texts.
try {
selectedNodes = document.querySelectorAll(rule.selectorText);
} catch(e) {}
if (!selectedNodes || !inNodeList(selectedNodes, element)) {
continue;
}
var cssTop = rule.style.getPropertyValue('top');
var cssBottom = rule.style.getPropertyValue('bottom');
if ((cssTop && cssTop !== 'auto') || (cssBottom && cssBottom !== 'auto')) {
return true;
}
}
}
return false;
};
dialogPolyfill.needsCentering = function(dialog) {
var computedStyle = window.getComputedStyle(dialog);
if (computedStyle.position !== 'absolute') {
return false;
}
// We must determine whether the top/bottom specified value is non-auto. In
// WebKit/Blink, checking computedStyle.top == 'auto' is sufficient, but
// Firefox returns the used value. So we do this crazy thing instead: check
// the inline style and then go through CSS rules.
if ((dialog.style.top !== 'auto' && dialog.style.top !== '') ||
(dialog.style.bottom !== 'auto' && dialog.style.bottom !== '')) {
return false;
}
return !dialogPolyfill.isInlinePositionSetByStylesheet(dialog);
};
/**
* @param {!Element} element to force upgrade
*/
dialogPolyfill.forceRegisterDialog = function(element) {
if (window.HTMLDialogElement || element.showModal) {
console.warn('This browser already supports <dialog>, the polyfill ' +
'may not work correctly', element);
}
if (element.localName !== 'dialog') {
throw new Error('Failed to register dialog: The element is not a dialog.');
}
new dialogPolyfillInfo(/** @type {!HTMLDialogElement} */ (element));
};
/**
* @param {!Element} element to upgrade, if necessary
*/
dialogPolyfill.registerDialog = function(element) {
if (!element.showModal) {
dialogPolyfill.forceRegisterDialog(element);
}
};
/**
* @constructor
*/
dialogPolyfill.DialogManager = function() {
/** @type {!Array<!dialogPolyfillInfo>} */
this.pendingDialogStack = [];
var checkDOM = this.checkDOM_.bind(this);
// The overlay is used to simulate how a modal dialog blocks the document.
// The blocking dialog is positioned on top of the overlay, and the rest of
// the dialogs on the pending dialog stack are positioned below it. In the
// actual implementation, the modal dialog stacking is controlled by the
// top layer, where z-index has no effect.
this.overlay = document.createElement('div');
this.overlay.className = '_poly_dialog_overlay';
this.overlay.addEventListener('click', function(e) {
this.forwardTab_ = undefined;
e.stopPropagation();
checkDOM([]); // sanity-check DOM
}.bind(this));
this.handleKey_ = this.handleKey_.bind(this);
this.handleFocus_ = this.handleFocus_.bind(this);
this.zIndexLow_ = 100000;
this.zIndexHigh_ = 100000 + 150;
this.forwardTab_ = undefined;
if ('MutationObserver' in window) {
this.mo_ = new MutationObserver(function(records) {
var removed = [];
records.forEach(function(rec) {
for (var i = 0, c; c = rec.removedNodes[i]; ++i) {
if (!(c instanceof Element)) {
continue;
} else if (c.localName === 'dialog') {
removed.push(c);
}
removed = removed.concat(c.querySelectorAll('dialog'));
}
});
removed.length && checkDOM(removed);
});
}
};
/**
* Called on the first modal dialog being shown. Adds the overlay and related
* handlers.
*/
dialogPolyfill.DialogManager.prototype.blockDocument = function() {
document.documentElement.addEventListener('focus', this.handleFocus_, true);
document.addEventListener('keydown', this.handleKey_);
this.mo_ && this.mo_.observe(document, {childList: true, subtree: true});
};
/**
* Called on the first modal dialog being removed, i.e., when no more modal
* dialogs are visible.
*/
dialogPolyfill.DialogManager.prototype.unblockDocument = function() {
document.documentElement.removeEventListener('focus', this.handleFocus_, true);
document.removeEventListener('keydown', this.handleKey_);
this.mo_ && this.mo_.disconnect();
};
/**
* Updates the stacking of all known dialogs.
*/
dialogPolyfill.DialogManager.prototype.updateStacking = function() {
var zIndex = this.zIndexHigh_;
for (var i = 0, dpi; dpi = this.pendingDialogStack[i]; ++i) {
dpi.updateZIndex(--zIndex, --zIndex);
if (i === 0) {
this.overlay.style.zIndex = --zIndex;
}
}
// Make the overlay a sibling of the dialog itself.
var last = this.pendingDialogStack[0];
if (last) {
var p = last.dialog.parentNode || document.body;
p.appendChild(this.overlay);
} else if (this.overlay.parentNode) {
this.overlay.parentNode.removeChild(this.overlay);
}
};
/**
* @param {Element} candidate to check if contained or is the top-most modal dialog
* @return {boolean} whether candidate is contained in top dialog
*/
dialogPolyfill.DialogManager.prototype.containedByTopDialog_ = function(candidate) {
while (candidate = findNearestDialog(candidate)) {
for (var i = 0, dpi; dpi = this.pendingDialogStack[i]; ++i) {
if (dpi.dialog === candidate) {
return i === 0; // only valid if top-most
}
}
candidate = candidate.parentElement;
}
return false;
};
dialogPolyfill.DialogManager.prototype.handleFocus_ = function(event) {
var target = event.composedPath ? event.composedPath()[0] : event.target;
if (this.containedByTopDialog_(target)) { return; }
if (document.activeElement === document.documentElement) { return; }
event.preventDefault();
event.stopPropagation();
safeBlur(/** @type {Element} */ (target));
if (this.forwardTab_ === undefined) { return; } // move focus only from a tab key
var dpi = this.pendingDialogStack[0];
var dialog = dpi.dialog;
var position = dialog.compareDocumentPosition(target);
if (position & Node.DOCUMENT_POSITION_PRECEDING) {
if (this.forwardTab_) {
// forward
dpi.focus_();
} else if (target !== document.documentElement) {
// backwards if we're not already focused on <html>
document.documentElement.focus();
}
}
return false;
};
dialogPolyfill.DialogManager.prototype.handleKey_ = function(event) {
this.forwardTab_ = undefined;
if (event.keyCode === 27) {
event.preventDefault();
event.stopPropagation();
var cancelEvent = new supportCustomEvent('cancel', {
bubbles: false,
cancelable: true
});
var dpi = this.pendingDialogStack[0];
if (dpi && safeDispatchEvent(dpi.dialog, cancelEvent)) {
dpi.dialog.close();
}
} else if (event.keyCode === 9) {
this.forwardTab_ = !event.shiftKey;
}
};
/**
* Finds and downgrades any known modal dialogs that are no longer displayed. Dialogs that are
* removed and immediately readded don't stay modal, they become normal.
*
* @param {!Array<!HTMLDialogElement>} removed that have definitely been removed
*/
dialogPolyfill.DialogManager.prototype.checkDOM_ = function(removed) {
// This operates on a clone because it may cause it to change. Each change also calls
// updateStacking, which only actually needs to happen once. But who removes many modal dialogs
// at a time?!
var clone = this.pendingDialogStack.slice();
clone.forEach(function(dpi) {
if (removed.indexOf(dpi.dialog) !== -1) {
dpi.downgradeModal();
} else {
dpi.maybeHideModal();
}
});
};
/**
* @param {!dialogPolyfillInfo} dpi
* @return {boolean} whether the dialog was allowed
*/
dialogPolyfill.DialogManager.prototype.pushDialog = function(dpi) {
var allowed = (this.zIndexHigh_ - this.zIndexLow_) / 2 - 1;
if (this.pendingDialogStack.length >= allowed) {
return false;
}
if (this.pendingDialogStack.unshift(dpi) === 1) {
this.blockDocument();
}
this.updateStacking();
return true;
};
/**
* @param {!dialogPolyfillInfo} dpi
*/
dialogPolyfill.DialogManager.prototype.removeDialog = function(dpi) {
var index = this.pendingDialogStack.indexOf(dpi);
if (index === -1) { return; }
this.pendingDialogStack.splice(index, 1);
if (this.pendingDialogStack.length === 0) {
this.unblockDocument();
}
this.updateStacking();
};
dialogPolyfill.dm = new dialogPolyfill.DialogManager();
dialogPolyfill.formSubmitter = null;
dialogPolyfill.imagemapUseValue = null;
/**
* Installs global handlers, such as click listers and native method overrides. These are needed
* even if a no dialog is registered, as they deal with <form method="dialog">.
*/
if (window.HTMLDialogElement === undefined) {
/**
* If HTMLFormElement translates method="DIALOG" into 'get', then replace the descriptor with
* one that returns the correct value.
*/
var testForm = document.createElement('form');
testForm.setAttribute('method', 'dialog');
if (testForm.method !== 'dialog') {
var methodDescriptor = Object.getOwnPropertyDescriptor(HTMLFormElement.prototype, 'method');
if (methodDescriptor) {
// nb. Some older iOS and older PhantomJS fail to return the descriptor. Don't do anything
// and don't bother to update the element.
var realGet = methodDescriptor.get;
methodDescriptor.get = function() {
if (isFormMethodDialog(this)) {
return 'dialog';
}
return realGet.call(this);
};
var realSet = methodDescriptor.set;
/** @this {HTMLElement} */
methodDescriptor.set = function(v) {
if (typeof v === 'string' && v.toLowerCase() === 'dialog') {
return this.setAttribute('method', v);
}
return realSet.call(this, v);
};
Object.defineProperty(HTMLFormElement.prototype, 'method', methodDescriptor);
}
}
/**
* Global 'click' handler, to capture the <input type="submit"> or <button> element which has
* submitted a <form method="dialog">. Needed as Safari and others don't report this inside
* document.activeElement.
*/
document.addEventListener('click', function(ev) {
dialogPolyfill.formSubmitter = null;
dialogPolyfill.imagemapUseValue = null;
if (ev.defaultPrevented) { return; } // e.g. a submit which prevents default submission
var target = /** @type {Element} */ (ev.target);
if ('composedPath' in ev) {
var path = ev.composedPath();
target = path.shift() || target;
}
if (!target || !isFormMethodDialog(target.form)) { return; }
var valid = (target.type === 'submit' && ['button', 'input'].indexOf(target.localName) > -1);
if (!valid) {
if (!(target.localName === 'input' && target.type === 'image')) { return; }
// this is a <input type="image">, which can submit forms
dialogPolyfill.imagemapUseValue = ev.offsetX + ',' + ev.offsetY;
}
var dialog = findNearestDialog(target);
if (!dialog) { return; }
dialogPolyfill.formSubmitter = target;
}, false);
/**
* Global 'submit' handler. This handles submits of `method="dialog"` which are invalid, i.e.,
* outside a dialog. They get prevented.
*/
document.addEventListener('submit', function(ev) {
var form = ev.target;
var dialog = findNearestDialog(form);
if (dialog) {
return; // ignore, handle there
}
var submitter = findFormSubmitter(ev);
var formmethod = submitter && submitter.getAttribute('formmethod') || form.getAttribute('method');
if (formmethod === 'dialog') {
ev.preventDefault();
}
});
/**
* Replace the native HTMLFormElement.submit() method, as it won't fire the
* submit event and give us a chance to respond.
*/
var nativeFormSubmit = HTMLFormElement.prototype.submit;
var replacementFormSubmit = function () {
if (!isFormMethodDialog(this)) {
return nativeFormSubmit.call(this);
}
var dialog = findNearestDialog(this);
dialog && dialog.close();
};
HTMLFormElement.prototype.submit = replacementFormSubmit;
}
export default dialogPolyfill;

2
public/lib/moment-with-locales.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -56,9 +56,6 @@
"Max Response Length (tokens)": "الحد الأقصى لطول الاستجابة (الرموز,الحرف)",
"Multiple swipes per generation": "الضربات الشديدة المتعددة لكل جيل",
"Enable OpenAI completion streaming": "تمكين بث الاكتمال من OpenAI",
"Enable Cohere web-search connector": "تمكين موصل بحث الويب Cohere",
"Web-search": "البحث في الويب",
"Allow the model to use the web-search connector.": "اسمح للنموذج باستخدام موصل بحث الويب.",
"Frequency Penalty": "عقوبة التكرار",
"Presence Penalty": "عقوبة الوجود",
"Count Penalty": "عد ضربة جزاء",
@ -381,10 +378,6 @@
"Group by vendors Description": "ضع نماذج OpenAI في مجموعة واحدة، والنماذج الإنسانية في مجموعة أخرى، وما إلى ذلك. ويمكن دمجها مع الفرز.",
"Allow fallback routes": "السماح بمسارات الاحتياط",
"Allow fallback routes Description": "يختار النموذج البديل تلقائيًا إذا كان النموذج المحدد غير قادر على تلبية طلبك.",
"openrouter_force_instruct": "هذا الخيار قديم وسيتم إزالته في المستقبل. لاستخدام تنسيق التعليمات، يرجى التبديل إلى OpenRouter ضمن Text Completion API بدلاً من ذلك.",
"LEGACY": "إرث",
"Force Instruct Mode formatting": "فرض تنسيق وضع التعليمات",
"Force_Instruct_Mode_formatting_Description": "إذا تم تمكين وضع التعليمات وهذا، فسيتم تنسيق المطالبة بواسطة SillyTavern باستخدام التيار\n إعدادات التنسيق المتقدمة (باستثناء توجيه موجه النظام). إذا تم تعطيله، فسيتم تنسيق المطالبة بواسطة OpenRouter.",
"Scale API Key": "مفتاح API لـ Scale",
"Clear your cookie": "امسح ملف تعريف الارتباط الخاص بك",
"Alt Method": "طريقة بديلة",

View File

@ -56,9 +56,6 @@
"Max Response Length (tokens)": "Maximale Antwortlänge (Tokens)",
"Multiple swipes per generation": "Mehrere Swipes pro Generation",
"Enable OpenAI completion streaming": "OpenAI-Vervollständigungsstreaming aktivieren",
"Enable Cohere web-search connector": "Cohere-Websuch-Connector aktivieren",
"Web-search": "Web-Suche",
"Allow the model to use the web-search connector.": "Erlauben Sie dem Modell, den Websuch-Connector zu verwenden.",
"Frequency Penalty": "Frequenzstrafe",
"Presence Penalty": "Präsenzstrafe",
"Count Penalty": "Strafe zählen",
@ -381,10 +378,6 @@
"Group by vendors Description": "Platzieren Sie OpenAI-Modelle in einer Gruppe, anthropogene Modelle in einer anderen Gruppe usw. Kann mit Sortierung kombiniert werden.",
"Allow fallback routes": "Fallback-Routen zulassen",
"Allow fallback routes Description": "Das alternative Modell wird automatisch ausgewählt, wenn das ausgewählte Modell Ihre Anfrage nicht erfüllen kann.",
"openrouter_force_instruct": "Diese Option ist veraltet und wird in Zukunft entfernt. Um die Formatierung mit Anweisungen zu verwenden, wechseln Sie stattdessen zu OpenRouter unter Text Completion API.",
"LEGACY": "VERMÄCHTNIS",
"Force Instruct Mode formatting": "Formatierung im Force Instruct Mode",
"Force_Instruct_Mode_formatting_Description": "Wenn sowohl der Anweisungsmodus als auch dieser aktiviert sind, wird die Eingabeaufforderung von SillyTavern mit den aktuellen erweiterten Formatierungseinstellungen formatiert (außer „Anweisungssystem-Eingabeaufforderung“). Wenn deaktiviert, wird die Eingabeaufforderung von OpenRouter formatiert.",
"Scale API Key": "Scale API-Schlüssel",
"Clear your cookie": "Löschen Sie Ihre Cookies",
"Alt Method": "Alternative Methode",

View File

@ -56,9 +56,6 @@
"Max Response Length (tokens)": "Longitud máxima de respuesta (tokens)",
"Multiple swipes per generation": "Múltiples golpes por generación",
"Enable OpenAI completion streaming": "Activar streaming de completado de OpenAI",
"Enable Cohere web-search connector": "Habilitar el conector de búsqueda web de Cohere",
"Web-search": "Búsqueda Web",
"Allow the model to use the web-search connector.": "Permita que el modelo utilice el conector de búsqueda web.",
"Frequency Penalty": "Penalización de frecuencia",
"Presence Penalty": "Penalización de presencia",
"Count Penalty": "Penalización de conteo",
@ -381,10 +378,6 @@
"Group by vendors Description": "Coloque los modelos OpenAI en un grupo, los modelos antrópicos en otro grupo, etc. Se puede combinar con la clasificación.",
"Allow fallback routes": "Permitir rutas de respaldo",
"Allow fallback routes Description": "El modelo alternativo se elige automáticamente si el modelo seleccionado no puede cumplir con tu solicitud.",
"openrouter_force_instruct": "Esta opción está desactualizada y se eliminará en el futuro. Para utilizar el formato de instrucciones, cambie a OpenRouter en API de finalización de texto.",
"LEGACY": "LEGADO",
"Force Instruct Mode formatting": "Forzar formato en modo de instrucción",
"Force_Instruct_Mode_formatting_Description": "Si tanto el modo de instrucción como este están habilitados, SillyTavern formateará el mensaje usando el formato actual.\n configuraciones de formato avanzadas (excepto instrucciones del mensaje del sistema). Si está deshabilitado, OpenRouter formateará el mensaje.",
"Scale API Key": "Clave API de Scale",
"Clear your cookie": "Limpia tu cookie",
"Alt Method": "Método alternativo",

View File

@ -56,9 +56,6 @@
"Max Response Length (tokens)": "Longueur maximale de la réponse (tokens)",
"Multiple swipes per generation": "Plusieurs balayages par génération",
"Enable OpenAI completion streaming": "Activer le streaming de complétion OpenAI",
"Enable Cohere web-search connector": "Activer le connecteur de recherche Web Cohere",
"Web-search": "Recherche Internet",
"Allow the model to use the web-search connector.": "Autorisez le modèle à utiliser le connecteur de recherche Web.",
"Frequency Penalty": "Pénalité de fréquence",
"Presence Penalty": "Pénalité de présence",
"Count Penalty": "Pénalité de décompte",
@ -381,10 +378,6 @@
"Group by vendors Description": "Placez les modèles OpenAI dans un groupe, les modèles Anthropic dans un autre groupe, etc. Peut être combiné avec le tri.",
"Allow fallback routes": "Autoriser les itinéraires de secours",
"Allow fallback routes Description": "Le modèle alternatif est automatiquement sélectionné si le modèle choisi ne peut pas répondre à votre demande.",
"openrouter_force_instruct": "Cette option est obsolète et sera supprimée à l'avenir. Pour utiliser le formatage des instructions, veuillez plutôt passer à OpenRouter sous API de saisie semi-automatique de texte.",
"LEGACY": "HÉRITAGE",
"Force Instruct Mode formatting": "Forcer le formatage du mode instruction",
"Force_Instruct_Mode_formatting_Description": "Si le mode Instruct et celui-ci sont activés, l'invite sera formatée par SillyTavern en utilisant le mode actuel.\n paramètres de formatage avancés (à l'exception de l'invite système). Si elle est désactivée, l'invite sera formatée par OpenRouter.",
"Scale API Key": "Clé API Scale",
"Clear your cookie": "Effacer vos cookies",
"Alt Method": "Méthode alternative",

View File

@ -56,9 +56,6 @@
"Max Response Length (tokens)": "Hámarks lengd svörunar (í táknum)",
"Multiple swipes per generation": "Mörg högg á hverja kynslóð",
"Enable OpenAI completion streaming": "Virkja OpenAI klárastreymi",
"Enable Cohere web-search connector": "Virkja Cohere vefleitartengi",
"Web-search": "Vefleit",
"Allow the model to use the web-search connector.": "Leyfðu líkaninu að nota vefleitartengið.",
"Frequency Penalty": "Tíðnarefning",
"Presence Penalty": "Tilkoma refning",
"Count Penalty": "Telja víti",
@ -381,10 +378,6 @@
"Group by vendors Description": "Setjið OpenAI módel í einn hóp, Anthropic módel í annan hóp osfrv. Hægt að sameina við flokkun.",
"Allow fallback routes": "Leyfa bakfallssvæði",
"Allow fallback routes Description": "Veldur hlutleysa vélbúnaðarinn við val þinn ef valið módel getur ekki uppfyllt beiðni þína.",
"openrouter_force_instruct": "Þessi valkostur er úreltur og verður fjarlægður í framtíðinni. Til að nota leiðbeiningarsnið skaltu skipta yfir í OpenRouter undir Text Completion API í staðinn.",
"LEGACY": "ARFIÐ",
"Force Instruct Mode formatting": "Force Instruct Mode formatting",
"Force_Instruct_Mode_formatting_Description": "Ef bæði leiðbeiningarhamur og þessi eru virkjuð, verður kvaðningurinn sniðinn af SillyTavern með því að nota núverandi\n háþróaðar sniðstillingar (nema leiðbeiningar um System Prompt). Ef slökkt er á henni verður hvetjan sniðin af OpenRouter.",
"Scale API Key": "Lykill API fyrir Scale",
"Clear your cookie": "Hreinsaðu kökuna þína",
"Alt Method": "Aðferð Bakmenn",

View File

@ -56,9 +56,6 @@
"Max Response Length (tokens)": "Lunghezza massima della risposta (token)",
"Multiple swipes per generation": "Più passaggi per generazione",
"Enable OpenAI completion streaming": "Abilita lo streaming di completamento OpenAI",
"Enable Cohere web-search connector": "Abilita il connettore di ricerca web Cohere",
"Web-search": "Ricerca sul web",
"Allow the model to use the web-search connector.": "Consenti al modello di utilizzare il connettore di ricerca web.",
"Frequency Penalty": "Penalità di frequenza",
"Presence Penalty": "Penalità di presenza",
"Count Penalty": "Conte Penalità",
@ -381,10 +378,6 @@
"Group by vendors Description": "Metti i modelli OpenAI in un gruppo, i modelli antropici in un altro gruppo, ecc. Può essere combinato con l'ordinamento.",
"Allow fallback routes": "Consenti percorsi alternativi",
"Allow fallback routes Description": "Il modello alternativo viene automaticamente scelto se il modello selezionato non può soddisfare la tua richiesta.",
"openrouter_force_instruct": "Questa opzione è obsoleta e verrà rimossa in futuro. Per usare la formattazione instruct, passa a OpenRouter in Text Completion API.",
"LEGACY": "EREDITÀ",
"Force Instruct Mode formatting": "Forza la formattazione della modalità istruzione",
"Force_Instruct_Mode_formatting_Description": "Se sia la modalità Instruct che questa sono abilitate, il prompt verrà formattato da SillyTavern utilizzando il file current\n impostazioni di formattazione avanzate (ad eccezione del prompt di sistema). Se disabilitato, il prompt verrà formattato da OpenRouter.",
"Scale API Key": "Chiave API di Scale",
"Clear your cookie": "Cancella il tuo cookie",
"Alt Method": "Metodo alternativo",

View File

@ -56,9 +56,6 @@
"Max Response Length (tokens)": "最大応答長(トークン数)",
"Multiple swipes per generation": "世代ごとに複数のスワイプ",
"Enable OpenAI completion streaming": "OpenAIの完了ストリーミングを有効にする",
"Enable Cohere web-search connector": "Cohereウェブ検索コネクタを有効にする",
"Web-search": "ウェブ検索",
"Allow the model to use the web-search connector.": "モデルが Web 検索コネクタを使用できるようにします。",
"Frequency Penalty": "頻度ペナルティ",
"Presence Penalty": "存在ペナルティ",
"Count Penalty": "カウントペナルティ",
@ -381,10 +378,6 @@
"Group by vendors Description": "OpenAI モデルを 1 つのグループに、Anthropic モデルを別のグループに配置するなどします。ソートと組み合わせることができます。",
"Allow fallback routes": "フォールバックルートを許可",
"Allow fallback routes Description": "選択したモデルが要求を満たせない場合、代替モデルが自動的に選択されます。",
"openrouter_force_instruct": "このオプションは古く、将来削除される予定です。指示されたフォーマットを使用するには、代わりにテキスト補完 API の OpenRouter に切り替えてください。",
"LEGACY": "遺産",
"Force Instruct Mode formatting": "強制指示モードのフォーマット",
"Force_Instruct_Mode_formatting_Description": "Instruct Mode とこれが両方とも有効になっている場合、プロンプトは SillyTavern によって現在の高度なフォーマット設定 (instruct System Prompt を除く) を使用してフォーマットされます。無効になっている場合、プロンプトは OpenRouter によってフォーマットされます。",
"Scale API Key": "ScaleのAPIキー",
"Clear your cookie": "クッキーを消去する",
"Alt Method": "代替手法",

View File

@ -56,9 +56,6 @@
"Max Response Length (tokens)": "최대 응답 길이 (토큰)",
"Multiple swipes per generation": "세대당 다중 스와이프",
"Enable OpenAI completion streaming": "OpenAI 완성 스트리밍 활성화",
"Enable Cohere web-search connector": "Cohere 웹 검색 커넥터 활성화",
"Web-search": "웹 서핑",
"Allow the model to use the web-search connector.": "모델이 웹 검색 커넥터를 사용하도록 허용합니다.",
"Frequency Penalty": "빈도 패널티",
"Presence Penalty": "존재 패널티",
"Count Penalty": "카운트 페널티",
@ -381,10 +378,6 @@
"Group by vendors Description": "OpenAI 모델을 한 그룹에 넣고, Anthropic 모델을 다른 그룹에 두는 등 정렬을 통해 결합할 수 있습니다.",
"Allow fallback routes": "대체 경로 허용",
"Allow fallback routes Description": "선택한 모델이 요청을 처리할 수 없는 경우 대체 모델이 자동으로 선택됩니다.",
"openrouter_force_instruct": "이 옵션은 오래되었으며 향후 제거될 예정입니다. 지시 형식을 사용하려면 대신 Text Completion API에서 OpenRouter로 전환하세요.",
"LEGACY": "유산",
"Force Instruct Mode formatting": "강제 지시 모드 포맷",
"Force_Instruct_Mode_formatting_Description": "Instruct Mode와 이 모드가 모두 활성화된 경우 프롬프트는 SillyTavern에 의해 현재 형식을 사용하여 형식화됩니다.\n 고급 형식 설정(시스템 프롬프트 지시 제외) 비활성화된 경우 프롬프트는 OpenRouter에 의해 형식화됩니다.",
"Scale API Key": "Scale API 키",
"Clear your cookie": "쿠키 지우기",
"Alt Method": "대체 방법",

View File

@ -56,9 +56,6 @@
"Max Response Length (tokens)": "Maximale lengte van het antwoord (tokens)",
"Multiple swipes per generation": "Meerdere swipes per generatie",
"Enable OpenAI completion streaming": "OpenAI voltooiingsstreaming inschakelen",
"Enable Cohere web-search connector": "Schakel de Cohere-webzoekconnector in",
"Web-search": "Web-zoeken",
"Allow the model to use the web-search connector.": "Sta toe dat het model de webzoekconnector gebruikt.",
"Frequency Penalty": "Frequentieboete",
"Presence Penalty": "Aanwezigheidsboete",
"Count Penalty": "Tel straf",
@ -381,10 +378,6 @@
"Group by vendors Description": "Plaats OpenAI-modellen in één groep, antropische modellen in een andere groep, enz. Kan worden gecombineerd met sorteren.",
"Allow fallback routes": "Fallback-routes toestaan",
"Allow fallback routes Description": "Het alternatieve model wordt automatisch gekozen als het geselecteerde model niet aan uw verzoek kan voldoen.",
"openrouter_force_instruct": "Deze optie is verouderd en zal in de toekomst worden verwijderd. Om instructie-opmaak te gebruiken, schakelt u in plaats daarvan over naar OpenRouter onder Text Completion API.",
"LEGACY": "NALATENSCHAP",
"Force Instruct Mode formatting": "Forceer de opmaak van de instructiemodus",
"Force_Instruct_Mode_formatting_Description": "Als zowel de Instruct-modus als deze zijn ingeschakeld, wordt de prompt door SillyTavern geformatteerd met behulp van de current\n geavanceerde opmaakinstellingen (behalve de opdracht Systeemprompt). Indien uitgeschakeld, wordt de prompt geformatteerd door OpenRouter.",
"Scale API Key": "Scale API-sleutel",
"Clear your cookie": "Wis uw cookie",
"Alt Method": "Alternatieve methode",

View File

@ -56,9 +56,6 @@
"Max Response Length (tokens)": "Comprimento Máximo da Resposta (tokens)",
"Multiple swipes per generation": "Vários furtos por geração",
"Enable OpenAI completion streaming": "Ativar streaming de conclusão do OpenAI",
"Enable Cohere web-search connector": "Ativar o conector de pesquisa na Web Cohere",
"Web-search": "Pesquisa na internet",
"Allow the model to use the web-search connector.": "Permita que o modelo use o conector de pesquisa na Web.",
"Frequency Penalty": "Pena de Frequência",
"Presence Penalty": "Pena de Presença",
"Count Penalty": "Contar penalidade",
@ -381,10 +378,6 @@
"Group by vendors Description": "Coloque os modelos OpenAI em um grupo, os modelos antrópicos em outro grupo, etc.",
"Allow fallback routes": "Permitir rotas de fallback",
"Allow fallback routes Description": "O modelo alternativo será escolhido automaticamente se o modelo selecionado não puder atender à sua solicitação.",
"openrouter_force_instruct": "Esta opção está desatualizada e será removida no futuro. Para usar a formatação de instruções, mude para OpenRouter em Text Completion API.",
"LEGACY": "LEGADO",
"Force Instruct Mode formatting": "Forçar formatação do modo de instrução",
"Force_Instruct_Mode_formatting_Description": "Se o Modo Instruir e este estiverem habilitados, o prompt será formatado pelo SillyTavern usando o atual\n configurações avançadas de formatação (exceto instruir o prompt do sistema). Se desativado, o prompt será formatado pelo OpenRouter.",
"Scale API Key": "Chave da API Scale",
"Clear your cookie": "Limpe seu cookie",
"Alt Method": "Método Alternativo",

View File

@ -56,7 +56,7 @@
"Rep. Pen. Slope": "Rep. Pen. Slope",
"Top K": "Top K",
"Top P": "Top P",
"Do Sample": "Сделать образец",
"Do Sample": "Включить сэмплинг",
"Add BOS Token": "Добавлять BOS-токен",
"Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative": "Добавлять BOS-токен в начале промпта. Если выключить, ответы могут стать более креативными.",
"Ban EOS Token": "Запретить EOS-токен",
@ -105,8 +105,8 @@
"API url": "URL-адрес API",
"PygmalionAI/aphrodite-engine": "PygmalionAI/aphrodite-engine (Режим обёртки API OpenAI)",
"Register a Horde account for faster queue times": "Заведите учетную запись Horde для ускорения генерации",
"Adjust context size to worker capabilities": "Уточнить размер контекста в соответствии с возможностями рабочих машин",
"Adjust response length to worker capabilities": "Уточнить длинну ответа в соответствии с возможностями рабочих машин",
"Adjust context size to worker capabilities": "Подстраивать размер контекста под возможности рабочих машин",
"Adjust response length to worker capabilities": "Подстраивать длину ответа под возможности рабочих машин",
"API key": "API-ключ",
"Tabby API key": "Tabby API-ключ",
"Get it here:": "Получить здесь:",
@ -132,9 +132,9 @@
"Novel AI Model": "Модель NovelAI",
"Make sure you run it with": "Обязательно запускайте его с флагом",
"flag": "",
"API key (optional)": "Ключ API (опционально)",
"API key (optional)": "Ключ API (необязательно)",
"Server url": "URL-адрес сервера",
"Custom model (optional)": "Пользовательская модель (опционально)",
"Custom model (optional)": "Пользовательская модель (необязательно)",
"Bypass API status check": "Обход проверки статуса API",
"Example: 127.0.0.1:5000": "Пример: http://127.0.0.1:5000",
"Legacy API (pre-OAI, no streaming)": "Устаревший API (до OAI, без стриминга)",
@ -173,7 +173,7 @@
"Include Names": "Добавлять имена",
"Force for Groups and Personas": "Также для групп и персон",
"System Prompt": "Системный промпт",
"Instruct Mode Sequences": "Строки для Instruct-режима",
"Instruct Sequences": "Строки для Instruct-режима",
"Stop Sequence": "Стоп-строка",
"Context Formatting": "Форматирование контекста",
"(Saved to Context Template)": "(Сохраняется в шаблоне контекста)",
@ -451,8 +451,8 @@
"Creator's Notes": "Заметки создателя",
"A-Z": "A-Z",
"Z-A": "Z-A",
"Newest": "Новейшие",
"Oldest": "Старейшие",
"Newest": "Сначала новые",
"Oldest": "Сначала старые",
"Favorites": "Избранные",
"Recent": "Последние",
"Most chats": "Больше всего чатов",
@ -561,10 +561,10 @@
"Example: [{{user}} is a 28-year-old Romanian cat girl.]": "Пример:\n [{{user}} is a 28-year-old Romanian cat girl.]",
"Toggle grid view": "Переключить вид сетки",
"Add to Favorites": "Добавить в Избранное",
"Advanced Definition": "Расширенное определение",
"Advanced Definition": "Расширенное описание",
"Character Lore": "Лор персонажа",
"Export and Download": "Экспортировать и скачать",
"Duplicate Character": "Дублировать персонажа",
"Duplicate Character": "Клонировать персонажа",
"Create Character": "Создать персонажа",
"Delete Character": "Удалить персонажа",
"View all tags": "Показать все тэги",
@ -732,19 +732,17 @@
"Context Size": "По размеру контекста",
"Group by vendors": "Сгруппировать по владельцу",
"Group by vendors Description": "Модели от OpenAI попадут в одну группу, от Anthropic - в другую, и т.д. Можно комбинировать с сортировкой.",
"LEGACY": "УСТАР.",
"Force Instruct Mode formatting": "Включить форматирование для Instruct-режима",
"Allow Jailbreak": "Разрешить джейлбрейк",
"System Prompt Wrapping": "Обрамление для системного промпта",
"System Prompt Prefix": "Префикс системного промпта",
"System Prompt Suffix": "Постфикс системного промпта",
"Chat Messages Wrapping": "Обрамление для сообщений в чате",
"User Message Prefix": "Префикс сообщения пользователя",
"User Message Suffix": "Постфикс сообщения пользователя",
"Assistant Message Prefix": "Префикс сообщения ассистента",
"Assistant Message Suffix": "Постфикс сообщения ассистента",
"System Message Prefix": "Префикс сообщения системы",
"System Message Suffix": "Постфикс сообщения системы",
"User Prefix": "Префикс сообщения пользователя",
"User Suffix": "Постфикс сообщения пользователя",
"Assistant Prefix": "Префикс сообщения ассистента",
"Assistant Suffix": "Постфикс сообщения ассистента",
"System Prefix": "Префикс сообщения системы",
"System Suffix": "Постфикс сообщения системы",
"System same as User": "Для системы то же самое, что и для пользователя",
"Misc. Sequences": "Прочие строки",
"First Assistant Prefix": "Первый префикс ассистента",
@ -774,7 +772,6 @@
"Type a message, or /? for help": "Введите сообщение, или /? для получения справки",
"Welcome to SillyTavern!": "Добро пожаловать в SillyTavern!",
"Won't be shared with the character card on export.": "Не попадут в карточку персонажа при экспорте.",
"Web-search": "Веб-поиск",
"Persona Name:": "Имя персоны:",
"User first message": "Первое сообщение пользователя",
"extension_token_counter": "Токенов:",
@ -1202,8 +1199,6 @@
"Streaming_desc": "Выводить текст последовательно по мере его генерации.\rЕсли параметр выключен, ответы будут отображаться сразу целиком, и только после полного завершения генерации.",
"Max prompt cost:": "Max prompt cost:",
"TFS": "TFS",
"Enable Cohere web-search connector": "Enable Cohere web-search connector",
"Allow the model to use the web-search connector.": "Allow the model to use the web-search connector.",
"Count Penalty": "Count Penalty",
"Min P": "Min P",
"NSFW": "NSFW",
@ -1270,8 +1265,6 @@
"vLLM Model": "Модель vLLM",
"Aphrodite Model": "Модель Aphrodite",
"Peek a password": "Посмотреть пароль",
"openrouter_force_instruct": "This option is outdated and will be removed in the future. To use instruct formatting, please switch to OpenRouter under Text Completion API instead.",
"Force_Instruct_Mode_formatting_Description": "If both Instruct Mode and this are enabled, the prompt will be formatted by SillyTavern using the current\n advanced formatting settings (except instruct System Prompt). If disabled, the prompt will be formatted by OpenRouter.",
"Clear your cookie": "Clear your cookie",
"Add Chat Start and Example Separator to a list of stopping strings.": "Использовать Начало чата и Разделитель примеров сообщений в качестве стоп-строк.",
"context_allow_jailbreak": "Если в карточке есть джейлбрейк И ПРИ ЭТОМ включена опция \"Приоритет джейлбрейку из карточки персонажа\", то этот джейлбрейк добавляется в конец промпта.\nНЕ РЕКОМЕНДУЕТСЯ ДЛЯ МОДЕЛЕЙ TEXT COMPLETION, МОЖЕТ ПОРТИТЬ ВЫХОДНОЙ ТЕКСТ.",
@ -1694,7 +1687,7 @@
"Delete Message": "Удалить сообщение",
"Delete Swipe": "Удалить свайп",
"Could not get a reply from API. Check your connection settings / API key and try again.": "Не удалось получить ответ от API. Проверьте настройки соединения и API-ключ и повторите попытку.",
"Connecting To Proxy": "Подключение к прокси",
"Connecting To Proxy": "Подключиться к прокси",
"Are you sure you want to connect to the following proxy URL?": "Вы точно хотите соединиться с прокси по этому адресу?",
"API connection successful!": "Соединение с API установлено!",
"Proxy Saved": "Прокси сохранена",
@ -1708,5 +1701,260 @@
"chat_rename_1": "Введите новое имя чата:",
"chat_rename_2": "!!Не используйте имя уже существующего файла, это приведёт к ошибке!!",
"chat_rename_3": "Будут разрушены связи между чатами-чекпоинтами.",
"chat_rename_4": "Расширение '.jsonl' дописывать не нужно."
"chat_rename_4": "Расширение '.jsonl' дописывать не нужно.",
"If you're connected to an API, try asking me something!": "Есть соединение с API? Напишите мне что-нибудь!",
"Connection Profile": "Профиль соединения",
"View connection profile details": "Посмотреть параметры профиля соединения",
"Create a new connection profile": "Создать новый профиль соединения",
"Update a connection profile": "Обновить профиль соединения",
"Rename a connection profile": "Переименовать профиль соединения",
"Reload a connection profile": "Перезагрузить профиль соединения",
"Delete a connection profile": "Удалить профиль соединения",
"No profile selected": "Профиль не выбран",
"Creating a Connection Profile": "Создать профиль соединения",
"Settings Preset": "Пресет настроек",
"Model": "Модель",
"Proxy Preset": "Пресет для прокси",
"Enter a name:": "Введите название:",
"Are you sure you want to delete the selected profile?": "Вы точно хотите удалить выбранный профиль?",
"instruct_enabled": "Вкл/выкл Instruct-режим",
"Instruct Template": "Шаблон Instruct-режима",
"instruct_template_activation_regex_desc": "Автоматически активировать этот шаблон в момент подключения к API или выбора модели, если название модели соответствует этому рег. выражению.",
"instruct_bind_to_context": "При включении этой опции Шаблон контекста будет выбираться, исходя из выбранного в текущий момент Шаблона Instruct-режима, либо по вашему желанию.",
"Master Import": "Глоб. импорт",
"Import Advanced Formatting settings": "Импорт настроек Расширенного форматирования\nТакже принимает файлы старого формата с Шаблонами контекста и Шаблонами Instruct-режима.",
"Master Export": "Глоб. экспорт",
"Export Advanced Formatting settings": "Экспорт настроек Расширенного форматирования",
"Select your current System Prompt": "Выберите текущий системный промпт",
"Prompt Content": "Текст промпта",
"Update current prompt": "Сохранить промпт",
"Save prompt as": "Сохранить как...",
"Import template": "Импорт шаблона",
"Export template": "Экспорт шаблона",
"Restore current prompt": "Восстановить промпт",
"comma delimited,no spaces between": "через запятую,без пробелов в промежутках",
"User Message Sequences": "Строки для сообщений пользователя",
"Assistant Message Sequences": "Строки для сообщений ассистента",
"System Message Sequences": "Строки для сообщений системы",
"System Prompt Sequences": "Строки для системного промпта",
"sysprompt_enabled": "Вкл/выкл системный промпт",
"Are you sure you want to delete this alternate greeting?": "Вы точно хотите удалить этот вариант?",
"Any contents here will replace the default Post-History Instructions used for this character. (v2 spec: post_history_instructions)": "Содержимое этого поля заменит стандартные Инструкции после истории, применяемые для этого персонажа. (v2 spec: post_history_instructions)",
"None (disabled)": "Нигде (откл.)",
"Markdown Hotkeys": "Горячие клавиши для разметки",
"markdown_hotkeys_desc": "Включить горячие клавиши для вставки символов разметки в некоторых полях ввода. См. '/help hotkeys'.",
"Save and Update": "Сохранить и обновить",
"Profile name:": "Название профиля:",
"API returned an error": "API вернуло ошибку",
"Failed to save preset": "Не удалось сохранить пресет",
"Preset name should be unique.": "Название пресета должно быть уникальным.",
"Invalid file": "Невалидный файл",
"No preset selected": "Пресет не выбран",
"Invalid logit bias preset file.": "Файл пресета невалиден.",
"Preset was not deleted from server": "Пресет не удалён с сервера",
"Preset deleted": "Пресет удалён",
"Delete the preset?": "Удалить пресет?",
"Preset updated": "Пресет сохранён",
"Entered reverse proxy address is not a valid URL": "Введённый адрес прокси невалиден",
"An error occurred while counting tokens: Token budget exceeded.": "Ошибка при подсчёте токенов: Превышен бюджет токенов",
"An error occurred while counting tokens: Invalid character name": "Ошибка при подсчёте токенов: Невалидное имя персонажа",
"Not enough free tokens for mandatory prompts. Raise your token Limit or disable custom prompts.": "Недостаточно токенов для всех выбранных промптов. Повысьте лимит токенов или отключите часть промптов.",
"The name of at least one character contained whitespaces or special characters. Please check your user and character name.": "В имени одного из персонажей содержится пробел или иной спецсимвол. Проверьте имена пользователя и персонажа.",
"An unknown error occurred while counting tokens. Further information may be available in console.": "Неизвестная ошибка при подсчёте токенов. Проверьте консоль, возможно, подробная информация есть там.",
"Encountered an error while processing your request.": "При обработке вашего запроса возникла ошибка.",
"Check you have credits available on your": "Убедитесь, что на вашем",
"OpenAI account quora_error": "аккаунте OpenAI",
"dot quota_error": "имеется достаточно кредитов.",
"If you have sufficient credits, please try again later.": "Если кредитов достаточно, то повторите попытку позднее.",
"Proxy preset '${0}' not found": "Пресет '${0}' не найден",
"Window.ai returned an error": "Window.ai вернул ошибку",
"Get it here:": "Загрузите здесь:",
"Extension is not installed": "Расширение не установлено",
"Update or remove your reverse proxy settings.": "Измените или удалите ваши настройки прокси.",
"An error occurred while importing prompts. More info available in console.": "В процессе импорта произошла ошибка. Подробную информацию см. в консоли.",
"Could not import prompts. Export failed validation.": "Не удалось импортировать промпты. Не пройдена валидация при экспорте.",
"Prompt import complete.": "Импорт завершён.",
"Are you sure you want to delete this prompt?": "Вы точно хотите удалить этот промпт?",
"Existing prompts with the same ID will be overridden. Do you want to proceed?": "Имеющиеся промпты с совпадающими идентификаторами будут перезаписаны. Продолжить?",
"This will reset the prompt order for this character. You will not lose any prompts.": "Будет сброшен порядок промптов для этого персонажа. Сами промпты вы не потеряете.",
"Note:": "Примечание:",
"this chat is temporary and will be deleted as soon as you leave it.": "это временный чат, он будет удалён, как только вы из него выйдете.",
"help_hotkeys_20": "Горячие клавиши для разметки",
"help_hotkeys_21": "Работают в окне ввода чата, а также в полях, отмеченных этим значком:",
"help_hotkeys_22": "**полужирный**",
"help_hotkeys_23": "*курсив*",
"help_hotkeys_24": "__подчёркивание__",
"help_hotkeys_25": "`inline-код`",
"help_hotkeys_26": "~~зачёркнутый~~",
"ext_regex_only_format_visual_desc": "Содержимое файла с историей чата останется нетронутым, изменения будут лишь визуальными (в UI).",
"Could not convert file": "Не удалось сконвертировать файл",
"Could not upload file": "Не удалось загрузить файл",
"Could not download file": "Не удалось скачать файл",
"File is too big. Maximum size is ${0}.": "Слишком большой файл. Максимальный размер: ${0}.",
"Binary files are not supported. Select a text file or image.": "Бинарные файлы не поддерживаются. Выберите текстовый файл или изображение.",
"No character or group selected": "Не выбрано ни одного персонажа или группы",
"Could not delete file": "Не удалось удалить файл",
"No attachments selected.": "Вложение не выбрано.",
"Data Bank": "Банк данных",
"No files were scraped.": "Скрапинг не выполнен.",
"Scraped ${0} files from ${1} to ${2}.": "Соскраплено из ${0} файлов из ${1} в ${2}.",
"Check browser console for details.": "Подробности см. в консоли браузера.",
"Scraping failed": "Ошибка скрапинга",
"External media has been blocked": "Внешние медиа отключены",
"Use the 'Ext. Media' button to allow it. Click on this message to dismiss.": "Разрешить можно с помощью кнопки 'Внешн. медиа'. Нажмите на это сообщение, чтобы его скрыть.",
"Couldn't get CSRF token. Please refresh the page.": "Не удалось получить CSRF токен. Попробуйте перезагрузить страницу.",
"Error": "Ошибка",
"API Error": "Ошибка API",
"Please wait until the chat is saved before switching characters.": "Пожалуйста, дождитесь сохранения чата, прежде чем переключать персонажа.",
"Your chat is still saving...": "Чат всё ещё сохраняется...",
"Character ${0} not found in the list": "Персонаж ${0} не найден в списке",
"Streaming is enabled, but the version of Kobold used does not support token streaming.": "Включён стриминг текста, но ваша версия Kobold не поддерживает стриминг токенов.",
"Streaming is not supported for the Legacy API. Update Ooba and use new API to enable streaming.": "Для устаревшего API стриминг недоступен. Обновите oobaboga и используйте новый API, чтобы включить стриминг.",
"Verify that the server is running and accessible.": "Убедитесь, что сервер запущен и доступен по сети.",
"ST Server cannot be reached": "Не удалось соединиться с сервером ST",
"You must first select a character to duplicate!": "Вы не выбрали персонажа, которого хотите клонировать!",
"Character Duplicated": "Персонаж склонирован",
"No character name provided.": "Вы не ввели имя персонажа.",
"Rename Character": "Переименование",
"No character selected.": "Вы не выбрали персонажа.",
"New name:": "Новое имя:",
"Same character name provided, so name did not change.": "Введено то же самое имя, ничего не изменилось.",
"Character renamed and past chats updated!": "Персонаж переименован, а чаты обновлены!",
"Character renamed!": "Персонаж переименован!",
"Something went wrong. The page will be reloaded.": "Что-то пошло не так. Страница будет перезагружена.",
"Past chat could not be updated: ${0}": "Не удалось обновить чат ${0}",
"Trying to save group chat with regular saveChat function. Aborting to prevent corruption.": "Произошла попытка сохранения группового чата функцией saveChat. Откатываем изменения, чтобы предотвратить потерю данных.",
"Check the server connection and reload the page to prevent data loss.": "Проверьте связь с сервером и перезагрузите страницу, чтобы избежать потери данных.",
"Chat could not be saved": "Не удалось сохранить чат",
"Settings could not be loaded after multiple attempts. Please try again later.": "Не удалось загрузить настройки за несколько попыток. Попробуйте позднее.",
"Settings could not be saved": "Не удалось сохранить настройки",
"Could not load chat data. Try reloading the page.": "Не удалось загрузить чат. Попробуйте обновить страницу.",
"Invalid process (no 'type')": "Невалидный процесс (нет параметра 'type')",
"Character Deleted: ${0}": "Персонаж удалён: ${0}",
"Character Created: ${0}": "Персонаж создан: ${0}",
"Group Created": "Группа создана",
"Group Deleted": "Группа удалена",
"Character Imported: ${0}": "Персонаж импортирован: ${0}",
"Invalid swipe ID: ${0}": "Некорректный идентификатор свайпа: ${0}",
"No messages to delete swipes from.": "Сообщение, из которого требуется удалить свайп, не найдено.",
"Can't delete the last swipe.": "Невозможно удалить единственный свайп.",
"GUI Settings preset is not supported for Horde. Please select another preset.": "Для Horde не поддерживаются пресеты настроек GUI. Пожалуйста, выберите другой пресет.",
"Embedded lorebook will be removed from this character.": "Встроенный лорбук будет удалён из персонажа.",
"Name is required": "Введите имя",
"Cannot create characters while generating. Stop the request and try again.": "Во время генерации ответа создать персонажа невозможно. Остановите запрос и повторите попытку.",
"Creation aborted": "Процесс создания прерван",
"Failed to create character": "Не удалось создать персонажа",
"Something went wrong while saving the character, or the image file provided was in an invalid format. Double check that the image is not a webp.": "Что-то пошло не так в процессе сохранения персонажа, либо вы загрузили файл некорректного формата. Проверьте, что изображение точно не в формате webp.",
"Context template '${0}' not found": "Шаблон контекста '${0}' не найден",
"Instruct template '${0}' not found": "Шаблон Instruct-режима '${0}' не найден",
"Error: ${0} is not a valid API": "Ошибка: ${0} не является валидным API",
"API set to ${0}, trying to connect..": "Установлено API ${0}, пробуем подключиться...",
"Unsupported file type: ": "Неподдерживаемый тип файла: ",
"Cannot import characters while generating. Stop the request and try again.": "Во время генерации ответа импорт персонажа невозможен. Остановите запрос и повторите попытку.",
"Import aborted": "Процесс импорта прерван",
"The file is likely invalid or corrupted.": "Вероятно, файл невалиден или повреждён.",
"Could not import character": "Не удалось импортировать персонажа",
"Cannot run /impersonate command while the reply is being generated.": "Во время генерации ответа выполнить /impersonate невозможно.",
"Name must be provided as an argument to rename this chat.": "Для переименования чата необходимо предоставить новое имя в качестве аргумента.",
"No chat selected that can be renamed.": "Чат, который требуется переименовать, не найден.",
"Successfully renamed chat to: ${0}": "Чат успешно переименован в: ${0}",
"Character ${0} not found. Skipping deletion.": "Персонаж ${0} не найден. Удаление пропускается.",
"Failed to delete character": "При удалении персонажа произошло ошибка",
"Are you sure you want to duplicate this character?": "Вы точно хотите клонировать этого персонажа?",
"If you just want to start a new chat with the same character...": "Если вы хотите просто создать новый чат, воспользуйтесь кнопкой \"Начать новый чат\" в меню слева внизу.",
"THIS IS PERMANENT!": "ОТМЕНИТЬ БУДЕТ НЕВОЗМОЖНО!",
"Also delete the chat files": "Также удалить файлы чатов",
"Delete the character?": "Удалить персонажа?",
"Not a valid number": "Некорректное число",
"Author's Note depth updated": "Глубина заметок автора обновлена",
"Author's Note frequency updated": "Частота заметок автора обновлена",
"Not a valid position": "Некорректная позиция",
"Author's Note position updated": "Позиция заметок автора обновлена",
"Something went wrong. Could not save character's author's note.": "Что-то пошло не так. Не удалось сохранить заметки автора для этого персонажа.",
"Select a character before trying to use Author's Note": "Сначала необходимо выбрать персонажа",
"Author's Note text updated": "Текст заметок автора обновлён",
"Group Validation": "Валидация группы",
"Warning: Listed member ${0} does not exist as a character. It will be removed from the group.": "Предупреждение: персонаж ${0} не существует в виде карточки. Он будет удалён из группы.",
"Group Chat could not be saved": "Не удалось сохранить групповой чат",
"Deleted group member swiped. To get a reply, add them back to the group.": "Вы пытаетесь свайпнуть удалённого члена группы. Чтобы получить ответ, добавьте этого персонажа обратно в группу.",
"Currently no group selected.": "В данный момент не выбрано ни одной группы.",
"Not so fast! Wait for the characters to stop typing before deleting the group.": "Чуть помедленнее! Перед удалением группы дождитесь, пока персонаж закончит печатать.",
"Delete the group?": "Удалить группу?",
"This will also delete all your chats with that group. If you want to delete a single conversation, select a \"View past chats\" option in the lower left menu.": "Вместе с ней будут удалены и все её чаты. Если требуется удалить только один чат, воспользуйтесь кнопкой \"Все чаты\" в меню в левом нижнем углу.",
"Can't peek a character while group reply is being generated": "Невозможно открыть карточку персонажа во время генерации ответа",
"Threshold": "Порог",
"DRY Repetition Penalty": "DRY Штраф за повтор",
"Multiplier": "Множитель",
"DRY_Multiplier_desc": "Поставьте в положение > 0, чтобы включить DRY. Определяет величину штрафа для кратчайшей \"штрафуемой\" строки.",
"DRY_Repetition_Penalty_desc": "DRY налагает штраф на токены, генерация которых приведёт к появлению строки, которая уже была в тексте раньше. Установите множитель = 0, чтобы отключить.",
"Base": "Основание",
"DRY_Base_desc": "Определяет, насколько быстро возрастает штраф с увеличением длины строки.",
"Allowed Length": "Допустимая длина",
"DRY_Allowed_Length_desc": "Длина повторяющейся строки, при превышении которой DRY начинает налагать штраф.",
"Invalid data provided for master import": "Глоб. импорт не может быть выполнен по причине невалидности данных",
"Importing instruct template...": "Импортируем...",
"Instruct template detected": "Обнаружен шаблон Instruct-режима",
"Importing as context template...": "Импортируем...",
"Context template detected": "Обнаружен шаблон контекста",
"Importing as system prompt...": "Импортируем...",
"System prompt detected": "Обнаружен системный промпт",
"Importing as settings preset...": "Импортируем...",
"Text Completion settings detected": "Обнаружены настройки Text Completion",
"No valid sections found in imported data": "Не найдено ни одной валидной секции данных",
"No sections selected for import": "Не выбрано ни одной секции для импорта",
"Import": "Импортировать",
"Imported ${0} settings: ${1}": "Импортировано ${0} настроек: ${1}",
"Export": "Экспортировать",
"No sections selected for export": "Не выбрано ни одной секции для экспорта",
"Cannot update GUI preset": "Пресет для GUI обновить невозможно",
"Template updated": "Шаблон сохранён",
"Hint: Use a character/group name to bind preset to a specific chat.": "Совет: введите имя персонажа/группы, чтобы привязать пресет к определённому чату.",
"Preset name:": "Название пресета:",
"Template name:": "Название шаблона:",
"Preset saved": "Пресет сохранён",
"Template saved": "Шаблон сохранён",
"Preset could not be saved": "Не удалось сохранить пресет",
"Preset could not be renamed": "Не удалось обновить пресет",
"Preset renamed": "Пресет переименован",
"Template renamed": "Шаблон переименован",
"Cannot delete GUI preset": "Пресет для GUI удалить невозможно",
"Failed to restore default preset": "Не удалось восстановить пресет по умолчанию",
"Failed to restore default template": "Не удалось восстановить шаблон по умолчанию",
"Rename preset": "Переименовать пресет",
"Rename template": "Переименовать шаблон",
"Enter a new name:": "Введите новое название:",
"Preset imported": "Пресет импортирован",
"Template imported": "Шаблон импортирован",
"Delete this preset?": "Удалить этот пресет?",
"Delete this template?": "Удалить этот шаблон?",
"This action is irreversible and your current settings will be overwritten.": "Отменить это действие невозможно. Ваши текущие настройки будут перезаписаны.",
"Preset deleted": "Пресет удалён",
"Template deleted": "Шаблон удалён",
"Template was not deleted from server": "Шаблон не удалён с сервера",
"Cannot restore GUI preset": "Пресет для Gui восстановить нельзя",
"Default preset cannot be restored": "Невозможно восстановить пресет по умолчанию",
"Default template cannot be restored": "Невозможно восстановить шаблон по умолчанию",
"Resetting a <b>default preset</b> will restore the default settings": "Сброс <b>стандартного пресета</b> восстановит настройки по умолчанию.",
"Resetting a <b>default template</b> will restore the default settings.": "Сброс <b>стандартного шаблона</b> восстановит настройки по умолчанию.",
"Are you sure?": "Вы уверены?",
"Default preset restored": "Стандартный пресет восстановлен",
"Default template restored": "Стандартный шаблон восстановлен",
"Resetting a <b>custom preset</b> will restore to the last saved state.": "Сброс <b>пользовательского пресета</b> откатит его к последнему сохранённому состоянию.",
"Resetting a <b>custom template</b> will restore to the last saved state.": "Сброс <b>пользовательского шаблона</b> откатит его к последнему сохранённому состоянию.",
"Preset restored": "Пресет восстановлен",
"Template restored": "Шаблон восстановлен",
"Update current template": "Сохранить шаблон",
"Rename current template": "Переименовать шаблон",
"Save template as": "Сохранить как...",
"Restore current template": "Восстановить шаблон",
"Delete the template": "Удалить шаблон",
"Rename current prompt": "Переименовать промпт",
"Select your current Context Template": "Выберите активный шаблон контекста",
"Select your current Instruct Template": "Выберите активный шаблон Instruct-режима",
"and connect to an": "и подключитесь к",
"You can add more": "Можете добавить больше",
"from other websites": "с других сайтов.",
"Go to the": "Загляните в",
"to install additional features.": ", чтобы установить разные дополнительные ресурсы.",
"or_welcome": "; также доступен"
}

View File

@ -56,9 +56,6 @@
"Max Response Length (tokens)": "Довжина відповіді (токени)",
"Multiple swipes per generation": "Кілька свайпів за покоління",
"Enable OpenAI completion streaming": "Увімкнути потокове завершення OpenAI",
"Enable Cohere web-search connector": "Увімкнути конектор веб-пошуку Cohere",
"Web-search": "Веб-пошук",
"Allow the model to use the web-search connector.": "Дозвольте моделі використовувати конектор веб-пошуку.",
"Frequency Penalty": "Штраф за частоту",
"Presence Penalty": "Штраф за наявність",
"Count Penalty": "Рахувати пенальті",
@ -381,10 +378,6 @@
"Group by vendors Description": "Помістіть моделі OpenAI в одну групу, моделі Anthropic в іншу групу тощо. Можна поєднати з сортуванням.",
"Allow fallback routes": "Дозволити резервні маршрути",
"Allow fallback routes Description": "Автоматично вибирає альтернативну модель, якщо вибрана модель не може задовольнити ваш запит.",
"openrouter_force_instruct": "Цей параметр застарів і буде видалено в майбутньому. Щоб використовувати форматування інструкцій, перейдіть натомість до OpenRouter у розділі Text Completion API.",
"LEGACY": "СПАДОК",
"Force Instruct Mode formatting": "Примусове форматування в режимі вказівок",
"Force_Instruct_Mode_formatting_Description": "Якщо ввімкнути обидва режими Instruction і цей режим, підказка буде відформатована SillyTavern за допомогою поточного\n розширені параметри форматування (крім системного запиту). Якщо вимкнено, запит буде відформатовано OpenRouter.",
"Scale API Key": "Ключ API для Scale",
"Clear your cookie": "Очистіть файл cookie",
"Alt Method": "Альтернативний метод",

View File

@ -56,9 +56,6 @@
"Max Response Length (tokens)": "Độ dài phản hồi tối đa (token)",
"Multiple swipes per generation": "Vuốt nhiều lần trong một lần tạo",
"Enable OpenAI completion streaming": "Bật streaming của OpenAI",
"Enable Cohere web-search connector": "Bật web tìm kiếm của Cohere",
"Web-search": "Tìm kiếm trên web",
"Allow the model to use the web-search connector.": "Cho phép model sử dụng trình kết nối tìm kiếm trên web.",
"Frequency Penalty": "Frequency Penalty",
"Presence Penalty": "Presence Penalty",
"Count Penalty": "Count Penalty",
@ -381,10 +378,6 @@
"Group by vendors Description": "Xếp các mô hình OpenAI vào một nhóm, các mô hình Anthropic vào một nhóm khác, v.v. Có thể kết hợp với việc sắp xếp.",
"Allow fallback routes": "Cho phép các tuyến đường phụ",
"Allow fallback routes Description": "Bot thay thế tự động nếu mô hình được chọn không thể đáp ứng yêu cầu của bạn.",
"openrouter_force_instruct": "Tùy chọn này đã lỗi thời và sẽ bị xóa trong tương lai. Để sử dụng định dạng hướng dẫn, vui lòng chuyển sang OpenRouter trong API hoàn thành văn bản.",
"LEGACY": "Cũ",
"Force Instruct Mode formatting": "Buộc định dạng Instruct Mode",
"Force_Instruct_Mode_formatting_Description": "Nếu cả Instruct Mode và chế độ này được bật, Prompt sẽ được SillyTavern định dạng bằng cách sử dụng\n cài đặt định dạng nâng cao (ngoại trừ hướng dẫn System Nhắc). Nếu bị tắt, Prompt sẽ được OpenRouter định dạng.",
"Scale API Key": "Scale API Key",
"Clear your cookie": "Xóa cookie",
"Alt Method": "Phương pháp thay thế",

View File

@ -56,9 +56,6 @@
"Max Response Length (tokens)": "最大回复长度(以词符数计)",
"Multiple swipes per generation": "每次生成多个备选回复",
"Enable OpenAI completion streaming": "启用OpenAI文本补全流式传输",
"Enable Cohere web-search connector": "启用 Cohere 网络搜索连接器",
"Web-search": "联网搜索",
"Allow the model to use the web-search connector.": "允许模型使用联网搜索。",
"Frequency Penalty": "频率惩罚",
"Presence Penalty": "存在惩罚",
"Count Penalty": "计数惩罚",
@ -397,10 +394,6 @@
"Context Size": "上下文大小",
"Group by vendors": "按供应商分组",
"Group by vendors Description": "将 OpenAI 模型放在一组,将 Anthropic 模型放在另一组,等等。可以与排序结合。",
"openrouter_force_instruct": "此选项已过时,将来会被删除。要使用指令格式,请改用文本完成 API 下的 OpenRouter。",
"LEGACY": "旧版",
"Force Instruct Mode formatting": "强制指令模式格式化",
"Force_Instruct_Mode_formatting_Description": "如果同时启用了“指示模式”和“系统提示词”,则 SillyTavern 将使用当前\n高级格式设置指示系统提示词除外对提示词进行格式化。如果禁用则 OpenRouter 将对提示词进行格式化。",
"Scale API Key": "Scale API密钥",
"Clear your cookie": "清除你的 Cookie",
"Alt Method": "备用方法",

View File

@ -56,9 +56,6 @@
"Max Response Length (tokens)": "最大回應長度(符記數)",
"Multiple swipes per generation": "每次生成多次滑動",
"Enable OpenAI completion streaming": "啟用 OpenAI 補充串流",
"Enable Cohere web-search connector": "啟用 Cohere 網頁搜尋連接器",
"Web-search": "網頁搜尋",
"Allow the model to use the web-search connector.": "允許模型使用網頁搜尋連接器",
"Frequency Penalty": "頻率懲罰",
"Presence Penalty": "存在懲罰",
"Count Penalty": "計數懲罰",
@ -382,10 +379,6 @@
"Group by vendors Description": "將 OpenAI 、 Anthropic 等等的模型放各自供應商的群組中。可以與排序功能結合使用。",
"Allow fallback routes": "允許備援路徑",
"Allow fallback routes Description": "如果選擇的模型無法滿足要求,會自動選擇替代模型。",
"openrouter_force_instruct": "這個選項已經過時,將來會被移除。如果要使用指令格式,請改在 Text Completion API 中選擇 OpenRouter。",
"LEGACY": "遺留",
"Force Instruct Mode formatting": "強制指示模式格式化",
"Force_Instruct_Mode_formatting_Description": "如果同時啟用「指令模式」和這個選項,\nSillyTavern 會根據目前的進階格式化設定(不包括指令系統提示)來格式化提示詞。\n如果停用這個選項提示詞將由 OpenRouter 來進行格式化。",
"Scale API Key": "Scale API 金鑰",
"Clear your cookie": "清除您的 Cookie",
"Alt Method": "替代方法",

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@ import { debounce, waitUntilCondition, escapeHtml } from './utils.js';
import { debounce_timeout } from './constants.js';
import { renderTemplateAsync } from './templates.js';
import { Popup } from './popup.js';
import { t } from './i18n.js';
function debouncePromise(func, delay) {
let timeoutId;
@ -455,7 +456,7 @@ class PromptManager {
// Delete selected prompt from list form and close edit form
this.handleDeletePrompt = async (event) => {
Popup.show.confirm('Are you sure you want to delete this prompt?', null).then((userChoice) => {
Popup.show.confirm(t`Are you sure you want to delete this prompt?`, null).then((userChoice) => {
if (!userChoice) return;
const promptID = document.getElementById(this.configuration.prefix + 'prompt_manager_footer_append_prompt').value;
const prompt = this.getPromptById(promptID);
@ -531,7 +532,7 @@ class PromptManager {
// Import prompts for the selected character
this.handleImport = () => {
Popup.show.confirm('Existing prompts with the same ID will be overridden. Do you want to proceed?', null)
Popup.show.confirm(t`Existing prompts with the same ID will be overridden. Do you want to proceed?`, null)
.then(userChoice => {
if (!userChoice) return;
@ -552,7 +553,7 @@ class PromptManager {
const data = JSON.parse(fileContent);
this.import(data);
} catch (err) {
toastr.error('An error occurred while importing prompts. More info available in console.');
toastr.error(t`An error occurred while importing prompts. More info available in console.`);
console.log('An error occurred while importing prompts');
console.log(err.toString());
}
@ -567,7 +568,7 @@ class PromptManager {
// Restore default state of a characters prompt order
this.handleCharacterReset = () => {
Popup.show.confirm('This will reset the prompt order for this character. You will not lose any prompts.', null)
Popup.show.confirm(t`This will reset the prompt order for this character. You will not lose any prompts.`, null)
.then(userChoice => {
if (!userChoice) return;
@ -1649,7 +1650,7 @@ class PromptManager {
};
if (false === this.validateObject(controlObj, importData)) {
toastr.warning('Could not import prompts. Export failed validation.');
toastr.warning(t`Could not import prompts. Export failed validation.`);
return;
}
@ -1672,7 +1673,7 @@ class PromptManager {
throw new Error('Prompt order strategy not supported.');
}
toastr.success('Prompt import complete.');
toastr.success(t`Prompt import complete.`);
this.saveServiceSettings().then(() => this.render());
}

View File

@ -45,8 +45,11 @@ var LPanelPin = document.getElementById('lm_button_panel_pin');
var WIPanelPin = document.getElementById('WI_panel_pin');
var RightNavPanel = document.getElementById('right-nav-panel');
var RightNavDrawerIcon = document.getElementById('rightNavDrawerIcon');
var LeftNavPanel = document.getElementById('left-nav-panel');
var LeftNavDrawerIcon = document.getElementById('leftNavDrawerIcon');
var WorldInfo = document.getElementById('WorldInfo');
var WIDrawerIcon = document.getElementById('WIDrawerIcon');
var SelectedCharacterTab = document.getElementById('rm_button_selected_ch');
@ -56,22 +59,25 @@ let counterNonce = Date.now();
const observerConfig = { childList: true, subtree: true };
const countTokensDebounced = debounce(RA_CountCharTokens, debounce_timeout.relaxed);
const countTokensShortDebounced = debounce(RA_CountCharTokens, debounce_timeout.short);
const checkStatusDebounced = debounce(RA_checkOnlineStatus, debounce_timeout.short);
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (!(mutation.target instanceof HTMLElement)) {
return;
}
if (mutation.target.classList.contains('online_status_text')) {
RA_checkOnlineStatus();
checkStatusDebounced();
} else if (mutation.target.parentNode === SelectedCharacterTab) {
setTimeout(RA_CountCharTokens, 200);
countTokensShortDebounced();
} else if (mutation.target.classList.contains('mes_text')) {
if (mutation.target instanceof HTMLElement) {
for (const element of mutation.target.getElementsByTagName('math')) {
element.childNodes.forEach(function (child) {
if (child.nodeType === Node.TEXT_NODE) {
child.textContent = '';
}
});
}
for (const element of mutation.target.getElementsByTagName('math')) {
element.childNodes.forEach(function (child) {
if (child.nodeType === Node.TEXT_NODE) {
child.textContent = '';
}
});
}
}
});
@ -159,8 +165,8 @@ export function shouldSendOnEnter() {
export function humanizedDateTime() {
const now = new Date(Date.now());
const dt = {
year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate(),
hour: now.getHours(), minute: now.getMinutes(), second: now.getSeconds(),
year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate(),
hour: now.getHours(), minute: now.getMinutes(), second: now.getSeconds(),
};
for (const key in dt) {
dt[key] = dt[key].toString().padStart(2, '0');
@ -725,9 +731,7 @@ export function addSafariPatch() {
export function initRossMods() {
// initial status check
setTimeout(() => {
RA_checkOnlineStatus();
}, 100);
checkStatusDebounced();
if (power_user.auto_load_chat) {
RA_autoloadchat();
@ -752,7 +756,7 @@ export function initRossMods() {
setTimeout(() => RA_autoconnect(PrevAPI), 100);
});
$('#api_button').click(function () { setTimeout(RA_checkOnlineStatus, 100); });
$('#api_button').on('click', () => checkStatusDebounced());
//toggle pin class when lock toggle clicked
$(RPanelPin).on('click', function () {
@ -760,13 +764,15 @@ export function initRossMods() {
if ($(RPanelPin).prop('checked') == true) {
//console.log('adding pin class to right nav');
$(RightNavPanel).addClass('pinnedOpen');
$(RightNavDrawerIcon).addClass('drawerPinnedOpen');
} else {
//console.log('removing pin class from right nav');
$(RightNavPanel).removeClass('pinnedOpen');
$(RightNavDrawerIcon).removeClass('drawerPinnedOpen');
if ($(RightNavPanel).hasClass('openDrawer') && $('.openDrawer').length > 1) {
$(RightNavPanel).slideToggle(200, 'swing');
//$(rightNavDrawerIcon).toggleClass('openIcon closedIcon');
$(RightNavDrawerIcon).toggleClass('openIcon closedIcon');
$(RightNavPanel).toggleClass('openDrawer closedDrawer');
}
}
@ -776,13 +782,15 @@ export function initRossMods() {
if ($(LPanelPin).prop('checked') == true) {
//console.log('adding pin class to Left nav');
$(LeftNavPanel).addClass('pinnedOpen');
$(LeftNavDrawerIcon).addClass('drawerPinnedOpen');
} else {
//console.log('removing pin class from Left nav');
$(LeftNavPanel).removeClass('pinnedOpen');
$(LeftNavDrawerIcon).removeClass('drawerPinnedOpen');
if ($(LeftNavPanel).hasClass('openDrawer') && $('.openDrawer').length > 1) {
$(LeftNavPanel).slideToggle(200, 'swing');
//$(leftNavDrawerIcon).toggleClass('openIcon closedIcon');
$(LeftNavDrawerIcon).toggleClass('openIcon closedIcon');
$(LeftNavPanel).toggleClass('openDrawer closedDrawer');
}
}
@ -793,14 +801,16 @@ export function initRossMods() {
if ($(WIPanelPin).prop('checked') == true) {
console.debug('adding pin class to WI');
$(WorldInfo).addClass('pinnedOpen');
$(WIDrawerIcon).addClass('drawerPinnedOpen');
} else {
console.debug('removing pin class from WI');
$(WorldInfo).removeClass('pinnedOpen');
$(WIDrawerIcon).removeClass('drawerPinnedOpen');
if ($(WorldInfo).hasClass('openDrawer') && $('.openDrawer').length > 1) {
console.debug('closing WI after lock removal');
$(WorldInfo).slideToggle(200, 'swing');
//$(WorldInfoDrawerIcon).toggleClass('openIcon closedIcon');
$(WIDrawerIcon).toggleClass('openIcon closedIcon');
$(WorldInfo).toggleClass('openDrawer closedDrawer');
}
}
@ -811,20 +821,24 @@ export function initRossMods() {
if (LoadLocalBool('NavLockOn') == true) {
//console.log('setting pin class via local var');
$(RightNavPanel).addClass('pinnedOpen');
$(RightNavDrawerIcon).addClass('drawerPinnedOpen');
}
if ($(RPanelPin).prop('checked')) {
console.debug('setting pin class via checkbox state');
$(RightNavPanel).addClass('pinnedOpen');
$(RightNavDrawerIcon).addClass('drawerPinnedOpen');
}
// read the state of left Nav Lock and apply to leftnav classlist
$(LPanelPin).prop('checked', LoadLocalBool('LNavLockOn'));
if (LoadLocalBool('LNavLockOn') == true) {
//console.log('setting pin class via local var');
$(LeftNavPanel).addClass('pinnedOpen');
$(LeftNavDrawerIcon).addClass('drawerPinnedOpen');
}
if ($(LPanelPin).prop('checked')) {
console.debug('setting pin class via checkbox state');
$(LeftNavPanel).addClass('pinnedOpen');
$(LeftNavDrawerIcon).addClass('drawerPinnedOpen');
}
// read the state of left Nav Lock and apply to leftnav classlist
@ -832,11 +846,13 @@ export function initRossMods() {
if (LoadLocalBool('WINavLockOn') == true) {
//console.log('setting pin class via local var');
$(WorldInfo).addClass('pinnedOpen');
$(WIDrawerIcon).addClass('drawerPinnedOpen');
}
if ($(WIPanelPin).prop('checked')) {
console.debug('setting pin class via checkbox state');
$(WorldInfo).addClass('pinnedOpen');
$(WIDrawerIcon).addClass('drawerPinnedOpen');
}
//save state of Right nav being open or closed
@ -1026,6 +1042,7 @@ export function initRossMods() {
const editMesDone = $('.mes_edit_done:visible');
if (editMesDone.length > 0) {
console.debug('Accepting edits with Ctrl+Enter');
$('#send_textarea').focus();
editMesDone.trigger('click');
return;
} else if (is_send_press == false) {

View File

@ -16,6 +16,7 @@ import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
export { MODULE_NAME as NOTE_MODULE_NAME };
import { t } from './i18n.js';
const MODULE_NAME = '2_floating_prompt'; // <= Deliberate, for sorting lower than memory
@ -37,7 +38,7 @@ const chara_note_position = {
function setNoteTextCommand(_, text) {
$('#extension_floating_prompt').val(text).trigger('input');
toastr.success('Author\'s Note text updated');
toastr.success(t`Author's Note text updated`);
return '';
}
@ -45,12 +46,12 @@ function setNoteDepthCommand(_, text) {
const value = Number(text);
if (Number.isNaN(value)) {
toastr.error('Not a valid number');
toastr.error(t`Not a valid number`);
return;
}
$('#extension_floating_depth').val(Math.abs(value)).trigger('input');
toastr.success('Author\'s Note depth updated');
toastr.success(t`Author's Note depth updated`);
return '';
}
@ -58,12 +59,12 @@ function setNoteIntervalCommand(_, text) {
const value = Number(text);
if (Number.isNaN(value)) {
toastr.error('Not a valid number');
toastr.error(t`Not a valid number`);
return;
}
$('#extension_floating_interval').val(Math.abs(value)).trigger('input');
toastr.success('Author\'s Note frequency updated');
toastr.success(t`Author's Note frequency updated`);
return '';
}
@ -76,12 +77,12 @@ function setNotePositionCommand(_, text) {
const position = validPositions[text?.trim()];
if (Number.isNaN(position)) {
toastr.error('Not a valid position');
toastr.error(t`Not a valid position`);
return;
}
$(`input[name="extension_floating_position"][value="${position}"]`).prop('checked', true).trigger('input');
toastr.info('Author\'s Note position updated');
toastr.info(t`Author's Note position updated`);
return '';
}
@ -206,7 +207,7 @@ function onExtensionFloatingCharaPromptInput() {
extension_settings.note.chara.push(tempCharaNote);
} else {
console.log('Character author\'s note error: No avatar name key could be found.');
toastr.error('Something went wrong. Could not save character\'s author\'s note.');
toastr.error(t`Something went wrong. Could not save character's author's note.`);
// Don't save settings if something went wrong
return;
@ -397,7 +398,7 @@ function onANMenuItemClick() {
//because this listener takes priority
$('#options').stop().fadeOut(animation_duration);
} else {
toastr.warning('Select a character before trying to use Author\'s Note', '', { timeOut: 2000 });
toastr.warning(t`Select a character before trying to use Author's Note`, '', { timeOut: 2000 });
}
}

View File

@ -40,6 +40,7 @@ import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { ScraperManager } from './scrapers.js';
import { DragAndDropHandler } from './dragdrop.js';
import { renderTemplateAsync } from './templates.js';
import { t } from './i18n.js';
/**
* @typedef {Object} FileAttachment
@ -206,7 +207,7 @@ export async function populateFileAttachment(message, inputId = 'file_form_input
const fileText = await converter(file);
base64Data = window.btoa(unescape(encodeURIComponent(fileText)));
} catch (error) {
toastr.error(String(error), 'Could not convert file');
toastr.error(String(error), t`Could not convert file`);
console.error('Could not convert file', error);
}
}
@ -257,7 +258,7 @@ export async function uploadFileAttachment(fileName, base64Data) {
const responseData = await result.json();
return responseData.path;
} catch (error) {
toastr.error(String(error), 'Could not upload file');
toastr.error(String(error), t`Could not upload file`);
console.error('Could not upload file', error);
}
}
@ -283,7 +284,7 @@ export async function getFileAttachment(url) {
const text = await result.text();
return text;
} catch (error) {
toastr.error(error, 'Could not download file');
toastr.error(error, t`Could not download file`);
console.error('Could not download file', error);
}
}
@ -299,13 +300,13 @@ async function validateFile(file) {
const isBinary = /^[\x00-\x08\x0E-\x1F\x7F-\xFF]*$/.test(fileText);
if (!isImage && file.size > fileSizeLimit) {
toastr.error(`File is too big. Maximum size is ${humanFileSize(fileSizeLimit)}.`);
toastr.error(t`File is too big. Maximum size is ${humanFileSize(fileSizeLimit)}.`);
return false;
}
// If file is binary
if (isBinary && !isImage && !isConvertible(file.type)) {
toastr.error('Binary files are not supported. Select a text file or image.');
toastr.error(t`Binary files are not supported. Select a text file or image.`);
return false;
}
@ -521,7 +522,7 @@ async function openExternalMediaOverridesDialog() {
const entityId = getCurrentEntityId();
if (!entityId) {
toastr.info('No character or group selected');
toastr.info(t`No character or group selected`);
return;
}
@ -646,7 +647,7 @@ async function deleteFileFromServer(url, silent = false) {
await eventSource.emit(event_types.FILE_ATTACHMENT_DELETED, url);
return true;
} catch (error) {
toastr.error(String(error), 'Could not delete file');
toastr.error(String(error), t`Could not delete file`);
console.error('Could not delete file', error);
return false;
}
@ -1054,7 +1055,7 @@ async function openAttachmentManager() {
const selectedAttachments = document.querySelectorAll('.attachmentListItemCheckboxContainer .attachmentListItemCheckbox:checked');
if (selectedAttachments.length === 0) {
toastr.info('No attachments selected.', 'Data Bank');
toastr.info(t`No attachments selected.`, t`Data Bank`);
return;
}
@ -1168,7 +1169,7 @@ async function runScraper(scraperId, target, callback) {
if (files.length === 0) {
console.warn('Scraping returned no files');
toastr.info('No files were scraped.', 'Data Bank');
toastr.info(t`No files were scraped.`, t`Data Bank`);
return;
}
@ -1176,12 +1177,12 @@ async function runScraper(scraperId, target, callback) {
await uploadFileAttachmentToServer(file, target);
}
toastr.success(`Scraped ${files.length} files from ${scraperId} to ${target}.`, 'Data Bank');
toastr.success(t`Scraped ${files.length} files from ${scraperId} to ${target}.`, t`Data Bank`);
callback();
}
catch (error) {
console.error('Scraping failed', error);
toastr.error('Check browser console for details.', 'Scraping failed');
toastr.error(t`Check browser console for details.`, t`Scraping failed`);
}
}
@ -1208,7 +1209,7 @@ export async function uploadFileAttachmentToServer(file, target) {
const fileText = await converter(file);
base64Data = window.btoa(unescape(encodeURIComponent(fileText)));
} catch (error) {
toastr.error(String(error), 'Could not convert file');
toastr.error(String(error), t`Could not convert file`);
console.error('Could not convert file', error);
}
} else {

View File

@ -0,0 +1,320 @@
import { disableExtension, enableExtension, extension_settings, extensionNames } from './extensions.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { equalsIgnoreCaseAndAccents, isFalseBoolean, isTrueBoolean } from './utils.js';
/**
* @param {'enable' | 'disable' | 'toggle'} action - The action to perform on the extension
* @returns {(args: {[key: string]: string | SlashCommandClosure}, extensionName: string | SlashCommandClosure) => Promise<string>}
*/
function getExtensionActionCallback(action) {
return async (args, extensionName) => {
if (args?.reload instanceof SlashCommandClosure) throw new Error('\'reload\' argument cannot be a closure.');
if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
if (!extensionName) {
toastr.warning(`Extension name must be provided as an argument to ${action} this extension.`);
return '';
}
const reload = !isFalseBoolean(args?.reload);
const internalExtensionName = findExtension(extensionName);
if (!internalExtensionName) {
toastr.warning(`Extension ${extensionName} does not exist.`);
return '';
}
const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);
if (action === 'enable' && isEnabled) {
toastr.info(`Extension ${extensionName} is already enabled.`);
return internalExtensionName;
}
if (action === 'disable' && !isEnabled) {
toastr.info(`Extension ${extensionName} is already disabled.`);
return internalExtensionName;
}
if (action === 'toggle') {
action = isEnabled ? 'disable' : 'enable';
}
if (reload) {
toastr.info(`${action.charAt(0).toUpperCase() + action.slice(1)}ing extension ${extensionName} and reloading...`);
// Clear input, so it doesn't stay because the command didn't "finish",
// and wait for a bit to both show the toast and let the clear bubble through.
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
await new Promise(resolve => setTimeout(resolve, 100));
}
if (action === 'enable') {
await enableExtension(internalExtensionName, reload);
} else {
await disableExtension(internalExtensionName, reload);
}
toastr.success(`Extension ${extensionName} ${action}d.`);
console.info(`Extension ${action}ed: ${extensionName}`);
if (!reload) {
console.info('Reload not requested, so page needs to be reloaded manually for changes to take effect.');
}
return internalExtensionName;
};
}
/**
* Finds an extension by name, allowing omission of the "third-party/" prefix.
*
* @param {string} name - The name of the extension to find
* @returns {string?} - The matched extension name or undefined if not found
*/
function findExtension(name) {
return extensionNames.find(extName => {
return equalsIgnoreCaseAndAccents(extName, name) || equalsIgnoreCaseAndAccents(extName, `third-party/${name}`);
});
}
/**
* Provides an array of SlashCommandEnumValue objects based on the extension names.
* Each object contains the name of the extension and a description indicating if it is a third-party extension.
*
* @returns {SlashCommandEnumValue[]} An array of SlashCommandEnumValue objects
*/
const extensionNamesEnumProvider = () => extensionNames.map(name => {
const isThirdParty = name.startsWith('third-party/');
if (isThirdParty) name = name.slice('third-party/'.length);
const description = isThirdParty ? 'third party extension' : null;
return new SlashCommandEnumValue(name, description, !isThirdParty ? enumTypes.name : enumTypes.enum);
});
export function registerExtensionSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'extension-enable',
callback: getExtensionActionCallback('enable'),
returns: 'The internal extension name',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'reload',
description: 'Whether to reload the page after enabling the extension',
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'true',
enumList: commonEnumProviders.boolean('trueFalse')(),
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'Extension name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: extensionNamesEnumProvider,
forceEnum: true,
}),
],
helpString: `
<div>
Enables a specified extension.
</div>
<div>
By default, the page will be reloaded automatically, stopping any further commands.<br />
If <code>reload=false</code> named argument is passed, the page will not be reloaded, and the extension will stay disabled until refreshed.
The page either needs to be refreshed, or <code>/reload-page</code> has to be called.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code class="language-stscript">/extension-enable Summarize</code></pre>
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'extension-disable',
callback: getExtensionActionCallback('disable'),
returns: 'The internal extension name',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'reload',
description: 'Whether to reload the page after disabling the extension',
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'true',
enumList: commonEnumProviders.boolean('trueFalse')(),
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'Extension name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: extensionNamesEnumProvider,
forceEnum: true,
}),
],
helpString: `
<div>
Disables a specified extension.
</div>
<div>
By default, the page will be reloaded automatically, stopping any further commands.<br />
If <code>reload=false</code> named argument is passed, the page will not be reloaded, and the extension will stay enabled until refreshed.
The page either needs to be refreshed, or <code>/reload-page</code> has to be called.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code class="language-stscript">/extension-disable Summarize</code></pre>
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'extension-toggle',
callback: async (args, extensionName) => {
if (args?.state instanceof SlashCommandClosure) throw new Error('\'state\' argument cannot be a closure.');
if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
const action = isTrueBoolean(args?.state) ? 'enable' :
isFalseBoolean(args?.state) ? 'disable' :
'toggle';
return await getExtensionActionCallback(action)(args, extensionName);
},
returns: 'The internal extension name',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'reload',
description: 'Whether to reload the page after toggling the extension',
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'true',
enumList: commonEnumProviders.boolean('trueFalse')(),
}),
SlashCommandNamedArgument.fromProps({
name: 'state',
description: 'Explicitly set the state of the extension (true to enable, false to disable). If not provided, the state will be toggled to the opposite of the current state.',
typeList: [ARGUMENT_TYPE.BOOLEAN],
enumList: commonEnumProviders.boolean('trueFalse')(),
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'Extension name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: extensionNamesEnumProvider,
forceEnum: true,
}),
],
helpString: `
<div>
Toggles the state of a specified extension.
</div>
<div>
By default, the page will be reloaded automatically, stopping any further commands.<br />
If <code>reload=false</code> named argument is passed, the page will not be reloaded, and the extension will stay in its current state until refreshed.
The page either needs to be refreshed, or <code>/reload-page</code> has to be called.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code class="language-stscript">/extension-toggle Summarize</code></pre>
</li>
<li>
<pre><code class="language-stscript">/extension-toggle Summarize state=true</code></pre>
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'extension-state',
callback: async (_, extensionName) => {
if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
const internalExtensionName = findExtension(extensionName);
if (!internalExtensionName) {
toastr.warning(`Extension ${extensionName} does not exist.`);
return '';
}
const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);
return String(isEnabled);
},
returns: 'The state of the extension, whether it is enabled.',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'Extension name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: extensionNamesEnumProvider,
forceEnum: true,
}),
],
helpString: `
<div>
Returns the state of a specified extension (true if enabled, false if disabled).
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code class="language-stscript">/extension-state Summarize</code></pre>
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'extension-exists',
aliases: ['extension-installed'],
callback: async (_, extensionName) => {
if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
const exists = findExtension(extensionName) !== undefined;
return exists ? 'true' : 'false';
},
returns: 'Whether the extension exists and is installed.',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'Extension name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: extensionNamesEnumProvider,
}),
],
helpString: `
<div>
Checks if a specified extension exists.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code class="language-stscript">/extension-exists SillyTavern-LALib</code></pre>
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'reload-page',
callback: async () => {
toastr.info('Reloading the page...');
location.reload();
return '';
},
helpString: 'Reloads the current page. All further commands will not be processed.',
}));
}

View File

@ -1,5 +1,5 @@
import { eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration } from '../script.js';
import { hideLoader, showLoader } from './loader.js';
import { showLoader } from './loader.js';
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { renderTemplate, renderTemplateAsync } from './templates.js';
import { isSubsetOf, setValueByPath } from './utils.js';
@ -14,7 +14,9 @@ export {
ModuleWorkerWrapper,
};
/** @type {string[]} */
export let extensionNames = [];
let manifests = {};
const defaultUrl = 'http://localhost:5100';
@ -241,7 +243,7 @@ function onEnableExtensionClick() {
enableExtension(name, false);
}
async function enableExtension(name, reload = true) {
export async function enableExtension(name, reload = true) {
extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name);
stateChanged = true;
await saveSettings();
@ -252,7 +254,7 @@ async function enableExtension(name, reload = true) {
}
}
async function disableExtension(name, reload = true) {
export async function disableExtension(name, reload = true) {
extension_settings.disabledExtensions.push(name);
stateChanged = true;
await saveSettings();
@ -1041,7 +1043,9 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') {
await installExtension(url);
}
jQuery(async function () {
export async function initExtensions() {
await addExtensionsButtonAndMenu();
$('#extensionsMenuButton').css('display', 'flex');
@ -1060,4 +1064,4 @@ jQuery(async function () {
* @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element.
*/
$('#third_party_extension_button').on('click', () => openThirdPartyExtensionMenu());
});
}

View File

@ -184,6 +184,7 @@ function downloadAssetsList(url) {
const url = isValidUrl(asset['url']) ? asset['url'] : '';
const title = assetType === 'extension' ? `Extension repo/guide: ${url}` : 'Preview in browser';
const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';
const toolTag = assetType === 'extension' && asset['tool'];
const assetBlock = $('<i></i>')
.append(element)
@ -193,12 +194,20 @@ function downloadAssetsList(url) {
<a class="asset_preview" href="${url}" target="_blank" title="${title}">
<i class="fa-solid fa-sm ${previewIcon}"></i>
</a>
${toolTag ? '<span class="tag" title="Adds a function tool"><i class="fa-solid fa-sm fa-wrench"></i> Tool</span>' : ''}
</span>
<small class="asset-description">
${description}
</small>
</div>`);
assetBlock.find('.tag').on('click', function (e) {
const a = document.createElement('a');
a.href = 'https://docs.sillytavern.app/for-contributors/function-calling/';
a.target = '_blank';
a.click();
});
if (assetType === 'character') {
if (asset.highlight) {
assetBlock.find('.asset-name').append('<i class="fa-solid fa-sm fa-trophy"></i>');
@ -354,7 +363,7 @@ async function openCharacterBrowser(forceDefault) {
for (const character of characters.sort((a, b) => a.name.localeCompare(b.name))) {
const listElement = template.find(character.highlight ? '.contestWinnersList' : '.featuredCharactersList');
const characterElement = $(await renderExtensionTemplateAsync(MODULE_NAME, 'character', character));
const downloadButton = characterElement.find('.characterAssetDownloadButton');
const downloadButton = characterElement.find('.characterAssetDownloadButton');
const checkMark = characterElement.find('.characterAssetCheckMark');
const isInstalled = isAssetInstalled('character', character.id);

View File

@ -164,3 +164,12 @@
gap: 5px;
align-items: center;
}
.asset-name .tag {
gap: 5px;
align-items: baseline;
font-size: calc(var(--mainFontSize)* 0.8);
cursor: pointer;
opacity: 0.9;
margin-left: 2px;
}

View File

@ -1,3 +1,4 @@
import { event_types, eventSource, saveSettingsDebounced } from '../../../script.js';
import { deleteAttachment, getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment, uploadFileAttachmentToServer } from '../../chats.js';
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
@ -196,7 +197,27 @@ async function enableDataBankAttachment(args, value) {
return '';
}
function cleanUpAttachments() {
let shouldSaveSettings = false;
if (extension_settings.character_attachments) {
Object.values(extension_settings.character_attachments).flat().filter(a => a.text).forEach(a => {
shouldSaveSettings = true;
delete a.text;
});
}
if (Array.isArray(extension_settings.attachments)) {
extension_settings.attachments.filter(a => a.text).forEach(a => {
shouldSaveSettings = true;
delete a.text;
});
}
if (shouldSaveSettings) {
saveSettingsDebounced();
}
}
jQuery(async () => {
eventSource.on(event_types.APP_READY, cleanUpAttachments);
const manageButton = await renderExtensionTemplateAsync('attachments', 'manage-button', {});
const attachButton = await renderExtensionTemplateAsync('attachments', 'attach-button', {});
$('#data_bank_wand_container').append(manageButton);

View File

@ -403,6 +403,7 @@ jQuery(async function () {
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && (secret_state[SECRET_KEYS.OPENAI] || extension_settings.caption.allow_reverse_proxy)) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openrouter' && secret_state[SECRET_KEYS.OPENROUTER]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'zerooneai' && secret_state[SECRET_KEYS.ZEROONEAI]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'groq' && secret_state[SECRET_KEYS.GROQ]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'mistral' && (secret_state[SECRET_KEYS.MISTRALAI] || extension_settings.caption.allow_reverse_proxy)) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && (secret_state[SECRET_KEYS.MAKERSUITE] || extension_settings.caption.allow_reverse_proxy)) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'anthropic' && (secret_state[SECRET_KEYS.CLAUDE] || extension_settings.caption.allow_reverse_proxy)) ||

View File

@ -21,6 +21,7 @@
<option value="anthropic">Anthropic</option>
<option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
<option value="google">Google AI Studio</option>
<option value="groq">Groq</option>
<option value="koboldcpp">KoboldCpp</option>
<option value="llamacpp">llama.cpp</option>
<option value="mistral">MistralAI</option>
@ -49,14 +50,20 @@
<option data-type="google" value="gemini-1.5-flash">gemini-1.5-flash</option>
<option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
<option data-type="google" value="gemini-1.5-flash-001">gemini-1.5-flash-001</option>
<option data-type="google" value="gemini-1.5-flash-002">gemini-1.5-flash-002</option>
<option data-type="google" value="gemini-1.5-flash-exp-0827">gemini-1.5-flash-exp-0827</option>
<option data-type="google" value="gemini-1.5-flash-8b-exp-0827">gemini-1.5-flash-8b-exp-0827</option>
<option data-type="google" value="gemini-1.5-flash-8b-exp-0924">gemini-1.5-flash-8b-exp-0924</option>
<option data-type="google" value="gemini-1.5-pro">gemini-1.5-pro</option>
<option data-type="google" value="gemini-1.5-pro-latest">gemini-1.5-pro-latest</option>
<option data-type="google" value="gemini-1.5-pro-001">gemini-1.5-pro-001</option>
<option data-type="google" value="gemini-1.5-pro-002">gemini-1.5-pro-002</option>
<option data-type="google" value="gemini-1.5-pro-exp-0801">gemini-1.5-pro-exp-0801</option>
<option data-type="google" value="gemini-1.5-pro-exp-0827">gemini-1.5-pro-exp-0827</option>
<option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>
<option data-type="groq" value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>
<option data-type="groq" value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>
<option data-type="groq" value="llava-v1.5-7b-4096-preview">llava-v1.5-7b-4096-preview</option>
<option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
<option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>
<option data-type="openrouter" value="openai/gpt-4o-2024-05-13">openai/gpt-4o-2024-05-13</option>

View File

@ -0,0 +1,12 @@
<div>
<h3>Included settings:</h3>
<div class="justifyLeft flex-container flexFlowColumn flexNoGap">
{{#each settings}}
<label class="checkbox_label">
<input type="checkbox" value="{{@key}}" name="exclude"{{#if this}} checked{{/if}}>
<span>{{@key}}</span>
</label>
{{/each}}
</div>
<h3 data-i18n="Profile name:">Profile name:</h3>
</div>

View File

@ -1,6 +1,6 @@
import { event_types, eventSource, main_api, saveSettingsDebounced } from '../../../script.js';
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js';
import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { SlashCommandAbortController } from '../../slash-commands/SlashCommandAbortController.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
@ -10,6 +10,7 @@ import { enumTypes, SlashCommandEnumValue } from '../../slash-commands/SlashComm
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { SlashCommandScope } from '../../slash-commands/SlashCommandScope.js';
import { collapseSpaces, getUniqueName, isFalseBoolean, uuidv4 } from '../../utils.js';
import { t } from '../../i18n.js';
const MODULE_NAME = 'connection-manager';
const NONE = '<None>';
@ -135,6 +136,7 @@ const profilesProvider = () => [
* @property {string} [context] Context Template
* @property {string} [instruct-state] Instruct Mode
* @property {string} [tokenizer] Tokenizer
* @property {string[]} [exclude] Commands to exclude
*/
/**
@ -171,8 +173,13 @@ function findProfileByName(value) {
async function readProfileFromCommands(mode, profile, cleanUp = false) {
const commands = mode === 'cc' ? CC_COMMANDS : TC_COMMANDS;
const opposingCommands = mode === 'cc' ? TC_COMMANDS : CC_COMMANDS;
const excludeList = Array.isArray(profile.exclude) ? profile.exclude : [];
for (const command of commands) {
try {
if (excludeList.includes(command)) {
continue;
}
const args = getNamedArguments();
const result = await SlashCommandParser.commands[command].callback(args, '');
if (result) {
@ -208,15 +215,37 @@ async function readProfileFromCommands(mode, profile, cleanUp = false) {
async function createConnectionProfile(forceName = null) {
const mode = main_api === 'openai' ? 'cc' : 'tc';
const id = uuidv4();
/** @type {ConnectionProfile} */
const profile = {
id,
mode,
exclude: [],
};
await readProfileFromCommands(mode, profile);
const profileForDisplay = makeFancyProfile(profile);
const template = await renderExtensionTemplateAsync(MODULE_NAME, 'profile', { profile: profileForDisplay });
const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'profile', { profile: profileForDisplay }));
template.find('input[name="exclude"]').on('input', function () {
const fancyName = String($(this).val());
const keyName = Object.entries(FANCY_NAMES).find(x => x[1] === fancyName)?.[0];
if (!keyName) {
console.warn('Key not found for fancy name:', fancyName);
return;
}
if (!Array.isArray(profile.exclude)) {
profile.exclude = [];
}
const excludeState = !$(this).prop('checked');
if (excludeState) {
profile.exclude.push(keyName);
} else {
const index = profile.exclude.indexOf(keyName);
index !== -1 && profile.exclude.splice(index, 1);
}
});
const isNameTaken = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n);
const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), isNameTaken);
const name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 });
@ -230,7 +259,13 @@ async function createConnectionProfile(forceName = null) {
return null;
}
profile.name = name;
if (Array.isArray(profile.exclude)) {
for (const command of profile.exclude) {
delete profile[command];
}
}
profile.name = String(name);
return profile;
}
@ -250,7 +285,7 @@ async function deleteConnectionProfile() {
}
const name = extension_settings.connectionManager.profiles[index].name;
const confirm = await Popup.show.confirm('Are you sure you want to delete the selected profile?', name);
const confirm = await Popup.show.confirm(t`Are you sure you want to delete the selected profile?`, name);
if (!confirm) {
return;
@ -357,10 +392,14 @@ async function renderDetailsContent(detailsContent) {
const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile);
if (profile) {
const profileForDisplay = makeFancyProfile(profile);
const template = await renderExtensionTemplateAsync(MODULE_NAME, 'view', { profile: profileForDisplay });
const templateParams = { profile: profileForDisplay };
if (Array.isArray(profile.exclude) && profile.exclude.length > 0) {
templateParams.omitted = profile.exclude.map(e => FANCY_NAMES[e]).join(', ');
}
const template = await renderExtensionTemplateAsync(MODULE_NAME, 'view', templateParams);
detailsContent.innerHTML = template;
} else {
detailsContent.textContent = 'No profile selected';
detailsContent.textContent = t`No profile selected`;
}
}
@ -472,29 +511,71 @@ async function renderDetailsContent(detailsContent) {
await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, NONE);
});
const renameButton = document.getElementById('rename_connection_profile');
renameButton.addEventListener('click', async () => {
const editButton = document.getElementById('edit_connection_profile');
editButton.addEventListener('click', async () => {
const selectedProfile = extension_settings.connectionManager.selectedProfile;
const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile);
if (!profile) {
console.log('No profile selected');
return;
}
if (!Array.isArray(profile.exclude)) {
profile.exclude = [];
}
let saveChanges = false;
const sortByViewOrder = (a, b) => Object.keys(FANCY_NAMES).indexOf(a) - Object.keys(FANCY_NAMES).indexOf(b);
const commands = profile.mode === 'cc' ? CC_COMMANDS : TC_COMMANDS;
const settings = commands.slice().sort(sortByViewOrder).reduce((acc, command) => {
const fancyName = FANCY_NAMES[command];
acc[fancyName] = !profile.exclude.includes(command);
return acc;
}, {});
const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'edit', { name: profile.name, settings }));
const newName = await callGenericPopup(template, POPUP_TYPE.INPUT, profile.name, {
customButtons: [{
text: t`Save and Update`,
classes: ['popup-button-ok'],
result: POPUP_RESULT.AFFIRMATIVE,
action: () => {
saveChanges = true;
},
}],
});
const newName = await Popup.show.input('Enter a new name', null, profile.name, { rows: 2 });
if (!newName) {
return;
}
if (extension_settings.connectionManager.profiles.some(p => p.name === newName)) {
if (profile.name !== newName && extension_settings.connectionManager.profiles.some(p => p.name === newName)) {
toastr.error('A profile with the same name already exists.');
return;
}
profile.name = newName;
const newExcludeList = template.find('input[name="exclude"]:not(:checked)').map(function () {
return Object.entries(FANCY_NAMES).find(x => x[1] === String($(this).val()))?.[0];
}).get();
if (newExcludeList.length !== profile.exclude.length || !newExcludeList.every(e => profile.exclude.includes(e))) {
profile.exclude = newExcludeList;
for (const command of newExcludeList) {
delete profile[command];
}
if (saveChanges) {
await updateConnectionProfile(profile);
} else {
toastr.info('Press "Update" to record them into the profile.', 'Included settings list updated');
}
}
if (profile.name !== newName) {
toastr.success('Connection profile renamed.');
profile.name = String(newName);
}
saveSettingsDebounced();
renderConnectionProfiles(profiles);
toastr.success('Connection profile renamed', '', { timeOut: 1500 });
await renderDetailsContent(detailsContent);
});
/** @type {HTMLElement} */

View File

@ -2,11 +2,20 @@
<h2 data-i18n="Creating a Connection Profile">
Creating a Connection Profile
</h2>
<ul class="justifyLeft">
<div class="justifyLeft flex-container flexFlowColumn flexNoGap">
{{#each profile}}
<li><strong data-i18n="{{@key}}">{{@key}}:</strong>&nbsp;{{this}}</li>
<label class="checkbox_label">
<input type="checkbox" value="{{@key}}" name="exclude" checked>
<span><strong data-i18n="{{@key}}">{{@key}}:</strong>&nbsp;{{this}}</span>
</label>
{{/each}}
</ul>
</div>
<div class="marginTop5">
<small>
<b>Hint:</b>
<i>Click on the setting name to omit it from the profile.</i>
</small>
</div>
<h3 data-i18n="Enter a name:">
Enter a name:
</h3>

View File

@ -13,7 +13,7 @@
<i id="view_connection_profile" class="menu_button fa-solid fa-info-circle" title="View connection profile details" data-i18n="[title]View connection profile details"></i>
<i id="create_connection_profile" class="menu_button fa-solid fa-file-circle-plus" title="Create a new connection profile" data-i18n="[title]Create a new connection profile"></i>
<i id="update_connection_profile" class="menu_button fa-solid fa-save" title="Update a connection profile" data-i18n="[title]Update a connection profile"></i>
<i id="rename_connection_profile" class="menu_button fa-solid fa-pencil" title="Rename a connection profile" data-i18n="[title]Rename a connection profile"></i>
<i id="edit_connection_profile" class="menu_button fa-solid fa-pencil" title="Edit a connection profile" data-i18n="[title]Edit a connection profile"></i>
<i id="reload_connection_profile" class="menu_button fa-solid fa-recycle" title="Reload a connection profile" data-i18n="[title]Reload a connection profile"></i>
<i id="delete_connection_profile" class="menu_button fa-solid fa-trash-can" title="Delete a connection profile" data-i18n="[title]Delete a connection profile"></i>
</div>

View File

@ -3,3 +3,8 @@
<li><strong data-i18n="{{@key}}">{{@key}}:</strong>&nbsp;{{this}}</li>
{{/each}}
</ul>
{{#if omitted}}
<div class="margin5">
<strong data-i18n="Omitted Settings:">Omitted Settings:</strong>&nbsp;<span>{{omitted}}</span>
</div>
{{/if}}

View File

@ -2,16 +2,17 @@ import { callPopup, eventSource, event_types, generateRaw, getRequestHeaders, ma
import { dragElement, isMobile } from '../../RossAscends-mods.js';
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
import { loadMovingUIState, power_user } from '../../power-user.js';
import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition } from '../../utils.js';
import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar } from '../../utils.js';
import { hideMutedSprites } from '../../group-chats.js';
import { isJsonSchemaSupported } from '../../textgen-settings.js';
import { debounce_timeout } from '../../constants.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
import { isFunctionCallingSupported } from '../../openai.js';
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
import { SlashCommandClosure } from '../../slash-commands/SlashCommandClosure.js';
export { MODULE_NAME };
const MODULE_NAME = 'expressions';
@ -19,7 +20,6 @@ const UPDATE_INTERVAL = 2000;
const STREAMING_UPDATE_INTERVAL = 10000;
const TALKINGCHECK_UPDATE_INTERVAL = 500;
const DEFAULT_FALLBACK_EXPRESSION = 'joy';
const FUNCTION_NAME = 'set_emotion';
const DEFAULT_LLM_PROMPT = 'Ignore previous instructions. Classify the emotion of the last message. Output just one word, e.g. "joy" or "anger". Choose only one of the following labels: {{labels}}';
const DEFAULT_EXPRESSIONS = [
'talkinghead',
@ -1015,10 +1015,6 @@ async function getLlmPrompt(labels) {
return '';
}
if (isFunctionCallingSupported()) {
return '';
}
const labelsString = labels.map(x => `"${x}"`).join(', ');
const prompt = substituteParamsExtended(String(extension_settings.expressions.llmPrompt), { labels: labelsString });
return prompt;
@ -1054,41 +1050,6 @@ function parseLlmResponse(emotionResponse, labels) {
throw new Error('Could not parse emotion response ' + emotionResponse);
}
/**
* Registers the function tool for the LLM API.
* @param {FunctionToolRegister} args Function tool register arguments.
*/
function onFunctionToolRegister(args) {
if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isFunctionCallingSupported()) {
// Only trigger on quiet mode
if (args.type !== 'quiet') {
return;
}
const emotions = DEFAULT_EXPRESSIONS.filter((e) => e != 'talkinghead');
const jsonSchema = {
$schema: 'http://json-schema.org/draft-04/schema#',
type: 'object',
properties: {
emotion: {
type: 'string',
enum: emotions,
description: `One of the following: ${JSON.stringify(emotions)}`,
},
},
required: [
'emotion',
],
};
args.registerFunctionTool(
FUNCTION_NAME,
substituteParams('Sets the label that best describes the current emotional state of {{char}}. Only select one of the enumerated values.'),
jsonSchema,
true,
);
}
}
function onTextGenSettingsReady(args) {
// Only call if inside an API call
if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isJsonSchemaSupported()) {
@ -1162,18 +1123,9 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin
const expressionsList = await getExpressionsList();
const prompt = substituteParamsExtended(customPrompt, { labels: expressionsList }) || await getLlmPrompt(expressionsList);
let functionResult = null;
eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onTextGenSettingsReady);
eventSource.once(event_types.LLM_FUNCTION_TOOL_REGISTER, onFunctionToolRegister);
eventSource.once(event_types.LLM_FUNCTION_TOOL_CALL, (/** @type {FunctionToolCall} */ args) => {
if (args.name !== FUNCTION_NAME) {
return;
}
functionResult = args?.arguments;
});
const emotionResponse = await generateRaw(text, main_api, false, false, prompt);
return parseLlmResponse(functionResult || emotionResponse, expressionsList);
return parseLlmResponse(emotionResponse, expressionsList);
}
// Extras
default: {
@ -2105,14 +2057,20 @@ function migrateSettings() {
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'lastsprite',
callback: (_, value) => lastExpression[String(value).trim()] ?? '',
callback: (_, name) => {
if (typeof name !== 'string') throw new Error('name must be a string');
const char = findChar({ name: name });
const sprite = lastExpression[char?.name ?? name] ?? '';
return sprite;
},
returns: 'the last set sprite / expression for the named character.',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'character name',
description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: commonEnumProviders.characters('character'),
forceEnum: true,
}),
],
helpString: 'Returns the last set sprite / expression for the named character.',
@ -2128,18 +2086,42 @@ function migrateSettings() {
name: 'classify-expressions',
aliases: ['expressions'],
callback: async (args) => {
const list = await getExpressionsList();
switch (String(args.format).toLowerCase()) {
case 'json':
return JSON.stringify(list);
default:
return list.join(', ');
/** @type {import('../../slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
// @ts-ignore
let returnType = args.return;
// Old legacy return type handling
if (args.format) {
toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning');
const type = String(args?.format).toLowerCase().trim();
switch (type) {
case 'json':
returnType = 'object';
break;
default:
returnType = 'pipe';
break;
}
}
// Now the actual new return type handling
const list = await getExpressionsList();
return await slashCommandReturnHelper.doReturn(returnType ?? 'pipe', list, { objectToStringFunc: list => list.join(', ') });
},
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'The way how you want the return value to be provided',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'pipe',
enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
forceEnum: true,
}),
// TODO remove some day
SlashCommandNamedArgument.fromProps({
name: 'format',
description: 'The format to return the list in: comma-separated plain text or JSON array. Default is plain text.',
description: '!!! DEPRECATED - use "return" instead !!! The format to return the list in: comma-separated plain text or JSON array. Default is plain text.',
typeList: [ARGUMENT_TYPE.STRING],
enumList: [
new SlashCommandEnumValue('plain', null, enumTypes.enum, ', '),

View File

@ -26,9 +26,11 @@ let paginationVisiblePages = 10;
let paginationMaxLinesPerPage = 2;
let galleryMaxRows = 3;
$('body').on('click', '.dragClose', function () {
const relatedId = $(this).data('related-id'); // Get the ID of the related draggable
$(`body > .draggable[id="${relatedId}"]`).remove(); // Remove the associated draggable
// Remove all draggables associated with the gallery
$('#movingDivs').on('click', '.dragClose', function () {
const relatedId = $(this).data('related-id');
if (!relatedId) return;
$(`#movingDivs > .draggable[id="${relatedId}"]`).remove();
});
const CUSTOM_GALLERY_REMOVED_EVENT = 'galleryRemoved';
@ -290,7 +292,7 @@ function makeMovable(id = 'gallery') {
$('#dragGallery').css('display', 'block');
$('body').append(newElement);
$('#movingDivs').append(newElement);
loadMovingUIState();
$(`.draggable[forChar="${id}"]`).css('display', 'block');
@ -362,8 +364,8 @@ function makeDragImg(id, url) {
}
}
// Step 3: Attach it to the body
document.body.appendChild(newElement);
// Step 3: Attach it to the movingDivs container
document.getElementById('movingDivs').appendChild(newElement);
// Step 4: Call dragElement and loadMovingUIState
const appendedElement = document.getElementById(uniqueId);
@ -439,6 +441,7 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
description: 'character name',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: commonEnumProviders.characters('character'),
forceEnum: true,
}),
SlashCommandNamedArgument.fromProps({
name: 'group',

View File

@ -350,6 +350,7 @@
}
.popup:has(#qr--modalEditor) {
aspect-ratio: unset;
width: unset;
}
.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) {
min-width: unset;

View File

@ -415,6 +415,7 @@
.popup:has(#qr--modalEditor) {
aspect-ratio: unset;
width: unset;
&:has(.qr--isExecuting.qr--minimized) {
min-width: unset;

View File

@ -117,7 +117,7 @@
<input type="checkbox" name="disabled" />
<span data-i18n="Disabled">Disabled</span>
</label>
<label class="checkbox flex-container">
<label class="checkbox flex-container" title="Run the regex script when the message belonging a to specified role(s) is edited.">
<input type="checkbox" name="run_on_edit" />
<span data-i18n="Run On Edit">Run On Edit</span>
</label>
@ -132,7 +132,7 @@
<small data-i18n="ext_regex_other_options" data-i18n="Ephemerality">Ephemerality</small>
<span class="fa-solid fa-circle-question note-link-span" title="By default, regex scripts alter the chat file directly and irreversibly.&#13;Enabling either (or both) of the options below will prevent chat file alteration, while still altering the specified item(s)."></span>
</span>
<label class="checkbox flex-container" title="Chat history file contents won't change, but regex will be applied to the messages displayed in the Chat UI.">
<label class="checkbox flex-container" data-i18n="[title]ext_regex_only_format_visual_desc" title="Chat history file contents won't change, but regex will be applied to the messages displayed in the Chat UI.">
<input type="checkbox" name="only_format_display" />
<span data-i18n="Only Format Display">Alter Chat Display</span>
</label>

View File

@ -44,9 +44,9 @@ function getScopedRegex() {
* @param {regex_placement} placement The placement of the string
* @param {RegexParams} params The parameters to use for the regex script
* @returns {string} The regexed string
* @typedef {{characterOverride?: string, isMarkdown?: boolean, isPrompt?: boolean, depth?: number }} RegexParams The parameters to use for the regex script
* @typedef {{characterOverride?: string, isMarkdown?: boolean, isPrompt?: boolean, isEdit?: boolean, depth?: number }} RegexParams The parameters to use for the regex script
*/
function getRegexedString(rawString, placement, { characterOverride, isMarkdown, isPrompt, depth } = {}) {
function getRegexedString(rawString, placement, { characterOverride, isMarkdown, isPrompt, isEdit, depth } = {}) {
// WTF have you passed me?
if (typeof rawString !== 'string') {
console.warn('getRegexedString: rawString is not a string. Returning empty string.');
@ -68,6 +68,11 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
// Script applies to all cases when neither "only"s are true, but there's no need to do it when `isMarkdown`, the as source (chat history) should already be changed beforehand
(!script.markdownOnly && !script.promptOnly && !isMarkdown)
) {
if (isEdit && !script.runOnEdit) {
console.debug(`getRegexedString: Skipping script ${script.scriptName} because it does not run on edit`);
return;
}
// Check if the depth is within the min/max depth
if (typeof depth === 'number' && depth >= 0) {
if (!isNaN(script.minDepth) && script.minDepth !== null && script.minDepth >= 0 && depth < script.minDepth) {

View File

@ -36,7 +36,7 @@ export async function getMultimodalCaption(base64Img, prompt) {
const isVllm = extension_settings.caption.multimodal_api === 'vllm';
const base64Bytes = base64Img.length * 0.75;
const compressionLimit = 2 * 1024 * 1024;
if ((['google', 'openrouter', 'mistral'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) {
if ((['google', 'openrouter', 'mistral', 'groq'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) {
const maxSide = 1024;
base64Img = await createThumbnail(base64Img, maxSide, maxSide, 'image/jpeg');
}
@ -135,6 +135,10 @@ function throwIfInvalidModel(useReverseProxy) {
throw new Error('01.AI API key is not set.');
}
if (extension_settings.caption.multimodal_api === 'groq' && !secret_state[SECRET_KEYS.GROQ]) {
throw new Error('Groq API key is not set.');
}
if (extension_settings.caption.multimodal_api === 'google' && !secret_state[SECRET_KEYS.MAKERSUITE] && !useReverseProxy) {
throw new Error('Google AI Studio API key is not set.');
}

View File

@ -20,7 +20,7 @@ import {
} from '../../../script.js';
import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules, renderExtensionTemplateAsync, writeExtensionField } from '../../extensions.js';
import { selected_group } from '../../group-chats.js';
import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay, isTrueBoolean, debounce } from '../../utils.js';
import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay, isTrueBoolean, debounce, isFalseBoolean } from '../../utils.js';
import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.js';
import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
import { getNovelUnlimitedImageGeneration, getNovelAnlas, loadNovelSubscriptionData } from '../../nai-settings.js';
@ -31,6 +31,8 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
import { debounce_timeout } from '../../constants.js';
import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
import { ToolManager } from '../../tool-calling.js';
export { MODULE_NAME };
const MODULE_NAME = 'sd';
@ -61,9 +63,11 @@ const initiators = {
interactive: 'interactive',
wand: 'wand',
swipe: 'swipe',
tool: 'tool',
};
const generationMode = {
TOOL: -2,
MESSAGE: -1,
CHARACTER: 0,
USER: 1,
@ -86,6 +90,7 @@ const multimodalMap = {
};
const modeLabels = {
[generationMode.TOOL]: 'Function Tool Prompt Description',
[generationMode.MESSAGE]: 'Chat Message Template',
[generationMode.CHARACTER]: 'Character ("Yourself")',
[generationMode.FACE]: 'Portrait ("Your Face")',
@ -123,8 +128,12 @@ const messageTrigger = {
};
const promptTemplates = {
// Not really a prompt template, rather an outcome message template
// Not really a prompt template, rather an outcome message template and function tool prompt
[generationMode.MESSAGE]: '[{{char}} sends a picture that contains: {{prompt}}].',
[generationMode.TOOL]: [
'The text prompt used to generate the image.',
'Must represent an exhaustive description of the desired image that will allow an artist or a photographer to perfectly recreate it.',
].join(' '),
[generationMode.CHARACTER]: 'In the next response I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: name, species and race, gender, age, clothing, occupation, physical features and appearances. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase \'full body portrait,\'',
//face-specific prompt
[generationMode.FACE]: 'In the next response I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: name, species and race, gender, age, facial features and expressions, occupation, hair and hair accessories (if any), what they are wearing on their upper body (if anything). Do not describe anything below their neck. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase \'close up facial portrait,\'',
@ -221,11 +230,11 @@ const defaultSettings = {
// Refine mode
refine_mode: false,
expand: false,
interactive_mode: false,
multimodal_captioning: false,
snap: false,
free_extend: false,
function_tool: false,
prompts: promptTemplates,
@ -240,7 +249,7 @@ const defaultSettings = {
drawthings_auth: '',
hr_upscaler: 'Latent',
hr_scale: 2.0,
hr_scale: 1.0,
hr_scale_min: 1.0,
hr_scale_max: 4.0,
hr_scale_step: 0.1,
@ -260,10 +269,6 @@ const defaultSettings = {
clip_skip: 1,
// NovelAI settings
novel_upscale_ratio_min: 1.0,
novel_upscale_ratio_max: 4.0,
novel_upscale_ratio_step: 0.1,
novel_upscale_ratio: 1.0,
novel_anlas_guard: false,
novel_sm: false,
novel_sm_dyn: false,
@ -295,6 +300,10 @@ const defaultSettings = {
const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);
function processTriggers(chat, _, abort) {
if (extension_settings.sd.function_tool && ToolManager.isToolCallingSupported()) {
return;
}
if (!extension_settings.sd.interactive_mode) {
return;
}
@ -416,7 +425,6 @@ async function loadSettings() {
$('#sd_hr_scale').val(extension_settings.sd.hr_scale).trigger('input');
$('#sd_denoising_strength').val(extension_settings.sd.denoising_strength).trigger('input');
$('#sd_hr_second_pass_steps').val(extension_settings.sd.hr_second_pass_steps).trigger('input');
$('#sd_novel_upscale_ratio').val(extension_settings.sd.novel_upscale_ratio).trigger('input');
$('#sd_novel_anlas_guard').prop('checked', extension_settings.sd.novel_anlas_guard);
$('#sd_novel_sm').prop('checked', extension_settings.sd.novel_sm);
$('#sd_novel_sm_dyn').prop('checked', extension_settings.sd.novel_sm_dyn);
@ -430,7 +438,6 @@ async function loadSettings() {
$('#sd_restore_faces').prop('checked', extension_settings.sd.restore_faces);
$('#sd_enable_hr').prop('checked', extension_settings.sd.enable_hr);
$('#sd_refine_mode').prop('checked', extension_settings.sd.refine_mode);
$('#sd_expand').prop('checked', extension_settings.sd.expand);
$('#sd_multimodal_captioning').prop('checked', extension_settings.sd.multimodal_captioning);
$('#sd_auto_url').val(extension_settings.sd.auto_url);
$('#sd_auto_auth').val(extension_settings.sd.auto_auth);
@ -453,6 +460,7 @@ async function loadSettings() {
$('#sd_interactive_visible').prop('checked', extension_settings.sd.interactive_visible);
$('#sd_stability_style_preset').val(extension_settings.sd.stability_style_preset);
$('#sd_huggingface_model_id').val(extension_settings.sd.huggingface_model_id);
$('#sd_function_tool').prop('checked', extension_settings.sd.function_tool);
for (const style of extension_settings.sd.styles) {
const option = document.createElement('option');
@ -467,6 +475,7 @@ async function loadSettings() {
toggleSourceControls();
addPromptTemplates();
registerFunctionTool();
await loadSettingOptions();
}
@ -530,6 +539,9 @@ function addPromptTemplates() {
.on('click', () => {
textarea.val(promptTemplates[name]);
extension_settings.sd.prompts[name] = promptTemplates[name];
if (String(name) === String(generationMode.TOOL)) {
registerFunctionTool();
}
saveSettingsDebounced();
});
const container = $('<div></div>')
@ -644,37 +656,13 @@ async function onSaveStyleClick() {
saveSettingsDebounced();
}
async function expandPrompt(prompt) {
try {
const response = await fetch('/api/sd/expand', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ prompt: prompt }),
});
if (!response.ok) {
throw new Error('API returned an error.');
}
const data = await response.json();
return data.prompt;
} catch {
return prompt;
}
}
/**
* Modifies prompt based on auto-expansion and user inputs.
* Modifies prompt based on user inputs.
* @param {string} prompt Prompt to refine
* @param {boolean} allowExpand Whether to allow auto-expansion
* @param {boolean} isNegative Whether the prompt is a negative one
* @returns {Promise<string>} Refined prompt
*/
async function refinePrompt(prompt, allowExpand, isNegative = false) {
if (allowExpand && extension_settings.sd.expand) {
prompt = await expandPrompt(prompt);
}
async function refinePrompt(prompt, isNegative) {
if (extension_settings.sd.refine_mode) {
const text = isNegative ? '<h3>Review and edit the <i>negative</i> prompt:</h3>' : '<h3>Review and edit the prompt:</h3>';
const refinedPrompt = await callGenericPopup(text + 'Press "Cancel" to abort the image generation.', POPUP_TYPE.INPUT, prompt.trim(), { rows: 5, okButton: 'Continue' });
@ -800,11 +788,6 @@ function combinePrefixes(str1, str2, macro = '') {
return process(result);
}
function onExpandInput() {
extension_settings.sd.expand = !!$(this).prop('checked');
saveSettingsDebounced();
}
function onRefineModeInput() {
extension_settings.sd.refine_mode = !!$('#sd_refine_mode').prop('checked');
saveSettingsDebounced();
@ -945,6 +928,12 @@ async function onSourceChange() {
await loadSettingOptions();
}
function onFunctionToolInput() {
extension_settings.sd.function_tool = !!$(this).prop('checked');
saveSettingsDebounced();
registerFunctionTool();
}
async function onOpenAiStyleSelect() {
extension_settings.sd.openai_style = String($('#sd_openai_style').find(':selected').val());
saveSettingsDebounced();
@ -969,12 +958,6 @@ async function onViewAnlasClick() {
toastr.info(`Free image generation: ${unlimitedGeneration ? 'Yes' : 'No'}`, `Anlas: ${anlas}`);
}
function onNovelUpscaleRatioInput() {
extension_settings.sd.novel_upscale_ratio = Number($('#sd_novel_upscale_ratio').val());
$('#sd_novel_upscale_ratio_value').val(extension_settings.sd.novel_upscale_ratio.toFixed(1));
saveSettingsDebounced();
}
function onNovelAnlasGuardInput() {
extension_settings.sd.novel_anlas_guard = !!$('#sd_novel_anlas_guard').prop('checked');
saveSettingsDebounced();
@ -2272,6 +2255,25 @@ function getRawLastMessage() {
return `((${processReply(lastMessage.mes)})), (${processReply(character.scenario)}:0.7), (${processReply(character.description)}:0.5)`;
}
/**
* Ensure that the selected option exists in the dropdown.
* @param {string} setting Setting key
* @param {string} selector Dropdown selector
* @returns {void}
*/
function ensureSelectionExists(setting, selector) {
/** @type {HTMLSelectElement} */
const selectElement = document.querySelector(selector);
if (!selectElement) {
return;
}
const options = Array.from(selectElement.options);
const value = extension_settings.sd[setting];
if (selectElement.selectedOptions.length && !options.some(option => option.value === value)) {
extension_settings.sd[setting] = selectElement.selectedOptions[0].value;
}
}
/**
* Generates an image based on the given trigger word.
* @param {string} initiator The initiator of the image generation
@ -2292,8 +2294,8 @@ async function generatePicture(initiator, args, trigger, message, callback) {
return;
}
extension_settings.sd.sampler = $('#sd_sampler').find(':selected').val();
extension_settings.sd.model = $('#sd_model').find(':selected').val();
ensureSelectionExists('sampler', '#sd_sampler');
ensureSelectionExists('model', '#sd_model');
trigger = trigger.trim();
const generationType = getGenerationType(trigger);
@ -2307,14 +2309,14 @@ async function generatePicture(initiator, args, trigger, message, callback) {
if (generationType == generationMode.BACKGROUND) {
const callbackOriginal = callback;
callback = async function (prompt, imagePath, generationType) {
callback = async function (prompt, imagePath, generationType, _negativePromptPrefix, _initiator, prefixedPrompt) {
const imgUrl = `url("${encodeURI(imagePath)}")`;
eventSource.emit(event_types.FORCE_SET_BACKGROUND, { url: imgUrl, path: imagePath });
if (typeof callbackOriginal === 'function') {
callbackOriginal(prompt, imagePath, generationType, negativePromptPrefix, initiator);
await callbackOriginal(prompt, imagePath, generationType, negativePromptPrefix, initiator, prefixedPrompt);
} else {
sendMessage(prompt, imagePath, generationType, negativePromptPrefix, initiator);
await sendMessage(prompt, imagePath, generationType, negativePromptPrefix, initiator, prefixedPrompt);
}
};
}
@ -2441,7 +2443,7 @@ async function getPrompt(generationType, message, trigger, quietPrompt, combineN
}
if (generationType !== generationMode.FREE) {
prompt = await refinePrompt(prompt, true);
prompt = await refinePrompt(prompt, false);
}
return prompt;
@ -2469,7 +2471,7 @@ function generateFreeModePrompt(trigger, combineNegatives) {
return message.original_avatar.replace(/\.[^/.]+$/, '');
}
}
throw new Error('No usable messages found.');
return '';
};
const key = getLastCharacterKey();
@ -2643,7 +2645,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
const filename = `${characterName}_${humanizedDateTime()}`;
const base64Image = await saveBase64AsFile(result.data, characterName, filename, result.format);
callback ? callback(prompt, base64Image, generationType, additionalNegativePrefix, initiator) : sendMessage(prompt, base64Image, generationType, additionalNegativePrefix, initiator);
callback
? await callback(prompt, base64Image, generationType, additionalNegativePrefix, initiator, prefixedPrompt)
: await sendMessage(prompt, base64Image, generationType, additionalNegativePrefix, initiator, prefixedPrompt);
return base64Image;
}
@ -3031,7 +3035,7 @@ async function generateNovelImage(prompt, negativePrompt, signal) {
width: width,
height: height,
negative_prompt: negativePrompt,
upscale_ratio: extension_settings.sd.novel_upscale_ratio,
upscale_ratio: extension_settings.sd.hr_scale,
decrisper: extension_settings.sd.novel_decrisper,
sm: sm,
sm_dyn: sm_dyn,
@ -3431,12 +3435,13 @@ async function onComfyDeleteWorkflowClick() {
* @param {number} generationType Generation type of the image
* @param {string} additionalNegativePrefix Additional negative prompt used for the image generation
* @param {string} initiator The initiator of the image generation
* @param {string} prefixedPrompt Prompt with an attached specific prefix
*/
async function sendMessage(prompt, image, generationType, additionalNegativePrefix, initiator) {
async function sendMessage(prompt, image, generationType, additionalNegativePrefix, initiator, prefixedPrompt) {
const context = getContext();
const name = context.groupId ? systemUserName : context.name2;
const template = extension_settings.sd.prompts[generationMode.MESSAGE] || '{{prompt}}';
const messageText = substituteParamsExtended(template, { char: name, prompt: prompt });
const messageText = substituteParamsExtended(template, { char: name, prompt: prompt, prefixedPrompt: prefixedPrompt });
const message = {
name: name,
is_user: false,
@ -3613,8 +3618,8 @@ async function sdMessageButton(e) {
try {
setBusyIcon(true);
if (hasSavedImage) {
const prompt = await refinePrompt(message.extra.title, false, false);
const negative = hasSavedNegative ? await refinePrompt(message.extra.negative, false, true) : '';
const prompt = await refinePrompt(message.extra.title, false);
const negative = hasSavedNegative ? await refinePrompt(message.extra.negative, true) : '';
message.extra.title = prompt;
const generationType = message?.extra?.generationType ?? generationMode.FREE;
@ -3756,8 +3761,8 @@ async function onImageSwiped({ message, element, direction }) {
eventSource.once(CUSTOM_STOP_EVENT, stopListener);
const callback = () => { };
const hasNegative = message.extra.negative;
const prompt = await refinePrompt(message.extra.title, false, false);
const negativePromptPrefix = hasNegative ? await refinePrompt(message.extra.negative, false, true) : '';
const prompt = await refinePrompt(message.extra.title, false);
const negativePromptPrefix = hasNegative ? await refinePrompt(message.extra.negative, true) : '';
const characterName = context.groupId
? context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString()
: context.characters[context.characterId]?.name;
@ -3788,12 +3793,121 @@ async function onImageSwiped({ message, element, direction }) {
await context.saveChat();
}
/**
* Applies the command arguments to the extension settings.
* @typedef {import('../../slash-commands/SlashCommand.js').NamedArguments} NamedArguments
* @typedef {import('../../slash-commands/SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture
* @param {NamedArguments | NamedArgumentsCapture} args - Command arguments
* @returns {Record<string, any>} - Current settings before applying the command arguments
*/
function applyCommandArguments(args) {
const overrideSettings = {};
const currentSettings = {};
const settingMap = {
'edit': 'refine_mode',
'extend': 'free_extend',
'multimodal': 'multimodal_captioning',
'seed': 'seed',
'width': 'width',
'height': 'height',
'steps': 'steps',
'cfg': 'scale',
'skip': 'clip_skip',
'model': 'model',
'sampler': 'sampler',
'scheduler': 'scheduler',
'vae': 'vae',
'upscaler': 'hr_upscaler',
'scale': 'hr_scale',
'hires': 'enable_hr',
'denoise': 'denoising_strength',
'2ndpass': 'hr_second_pass_steps',
'faces': 'restore_faces',
};
for (const [param, setting] of Object.entries(settingMap)) {
if (args[param] === undefined || defaultSettings[setting] === undefined) {
continue;
}
currentSettings[setting] = extension_settings.sd[setting];
const value = String(args[param]);
const type = typeof defaultSettings[setting];
switch (type) {
case 'boolean':
overrideSettings[setting] = isTrueBoolean(value) || !isFalseBoolean(value);
break;
case 'number':
overrideSettings[setting] = Number(value);
break;
default:
overrideSettings[setting] = value;
break;
}
}
Object.assign(extension_settings.sd, overrideSettings);
return currentSettings;
}
function registerFunctionTool() {
if (!extension_settings.sd.function_tool) {
return ToolManager.unregisterFunctionTool('GenerateImage');
}
ToolManager.registerFunctionTool({
name: 'GenerateImage',
displayName: 'Generate Image',
description: [
'Generate an image from a given text prompt.',
'Use when a user asks to generate an image, imagine a concept or an item, send a picture of a scene, a selfie, etc.',
].join(' '),
parameters: Object.freeze({
$schema: 'http://json-schema.org/draft-04/schema#',
type: 'object',
properties: {
prompt: {
type: 'string',
description: extension_settings.sd.prompts[generationMode.TOOL] || promptTemplates[generationMode.TOOL],
},
},
required: [
'prompt',
],
}),
action: async (args) => {
if (!isValidState()) throw new Error('Image generation is not configured.');
if (!args) throw new Error('Missing arguments');
if (!args.prompt) throw new Error('Missing prompt');
const url = await generatePicture(initiators.tool, {}, args.prompt);
return encodeURI(url);
},
formatMessage: () => 'Generating an image...',
});
}
jQuery(async () => {
await addSDGenButtons();
const getSelectEnumProvider = (id, text) => () => Array.from(document.querySelectorAll(`#${id} > [value]`)).map(x => new SlashCommandEnumValue(x.getAttribute('value'), text ? x.textContent : null));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'imagine',
callback: (args, trigger) => generatePicture(initiators.command, args, String(trigger)),
returns: 'URL of the generated image, or an empty string if the generation failed',
callback: async (args, trigger) => {
const currentSettings = applyCommandArguments(args);
try {
return await generatePicture(initiators.command, args, String(trigger));
} catch (error) {
console.error('Failed to generate image:', error);
return '';
} finally {
if (Object.keys(currentSettings).length) {
Object.assign(extension_settings.sd, currentSettings);
saveSettingsDebounced();
}
}
},
aliases: ['sd', 'img', 'image'],
namedArgumentList: [
new SlashCommandNamedArgument(
@ -3803,6 +3917,164 @@ jQuery(async () => {
name: 'negative',
description: 'negative prompt prefix',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: false,
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'extend',
description: 'auto-extend free mode prompts with the LLM',
typeList: [ARGUMENT_TYPE.BOOLEAN],
enumProvider: commonEnumProviders.boolean('trueFalse'),
isRequired: false,
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'edit',
description: 'edit the prompt before generation',
typeList: [ARGUMENT_TYPE.BOOLEAN],
enumProvider: commonEnumProviders.boolean('trueFalse'),
isRequired: false,
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'multimodal',
description: 'use multimodal captioning (for portraits only)',
typeList: [ARGUMENT_TYPE.BOOLEAN],
enumProvider: commonEnumProviders.boolean('trueFalse'),
isRequired: false,
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'snap',
description: 'snap auto-adjusted dimensions to the nearest known resolution (portraits and backgrounds only)',
typeList: [ARGUMENT_TYPE.BOOLEAN],
enumProvider: commonEnumProviders.boolean('trueFalse'),
isRequired: false,
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'seed',
description: 'random seed',
isRequired: false,
typeList: [ARGUMENT_TYPE.NUMBER],
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'width',
description: 'image width',
isRequired: false,
typeList: [ARGUMENT_TYPE.NUMBER],
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'height',
description: 'image height',
isRequired: false,
typeList: [ARGUMENT_TYPE.NUMBER],
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'steps',
description: 'number of steps',
isRequired: false,
typeList: [ARGUMENT_TYPE.NUMBER],
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'cfg',
description: 'CFG scale',
isRequired: false,
typeList: [ARGUMENT_TYPE.NUMBER],
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'skip',
description: 'CLIP skip layers',
isRequired: false,
typeList: [ARGUMENT_TYPE.NUMBER],
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'model',
description: 'model override',
isRequired: false,
typeList: [ARGUMENT_TYPE.STRING],
acceptsMultiple: false,
forceEnum: true,
enumProvider: getSelectEnumProvider('sd_model', true),
}),
SlashCommandNamedArgument.fromProps({
name: 'sampler',
description: 'sampler override',
isRequired: false,
typeList: [ARGUMENT_TYPE.STRING],
acceptsMultiple: false,
forceEnum: true,
enumProvider: getSelectEnumProvider('sd_sampler', false),
}),
SlashCommandNamedArgument.fromProps({
name: 'scheduler',
description: 'scheduler override',
isRequired: false,
typeList: [ARGUMENT_TYPE.STRING],
acceptsMultiple: false,
forceEnum: true,
enumProvider: getSelectEnumProvider('sd_scheduler', false),
}),
SlashCommandNamedArgument.fromProps({
name: 'vae',
description: 'VAE name override',
isRequired: false,
typeList: [ARGUMENT_TYPE.STRING],
acceptsMultiple: false,
forceEnum: true,
enumProvider: getSelectEnumProvider('sd_vae', false),
}),
SlashCommandNamedArgument.fromProps({
name: 'upscaler',
description: 'upscaler override',
isRequired: false,
typeList: [ARGUMENT_TYPE.STRING],
acceptsMultiple: false,
forceEnum: true,
enumProvider: getSelectEnumProvider('sd_hr_upscaler', false),
}),
SlashCommandNamedArgument.fromProps({
name: 'hires',
description: 'enable high-res fix',
isRequired: false,
typeList: [ARGUMENT_TYPE.BOOLEAN],
acceptsMultiple: false,
enumProvider: commonEnumProviders.boolean('trueFalse'),
}),
SlashCommandNamedArgument.fromProps({
name: 'scale',
description: 'upscale amount',
isRequired: false,
typeList: [ARGUMENT_TYPE.NUMBER],
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'denoise',
description: 'denoising strength',
isRequired: false,
typeList: [ARGUMENT_TYPE.NUMBER],
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: '2ndpass',
description: 'second pass steps',
isRequired: false,
typeList: [ARGUMENT_TYPE.NUMBER],
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'faces',
description: 'restore faces',
isRequired: false,
typeList: [ARGUMENT_TYPE.BOOLEAN],
acceptsMultiple: false,
enumProvider: commonEnumProviders.boolean('trueFalse'),
}),
],
unnamedArgumentList: [
@ -3823,6 +4095,66 @@ jQuery(async () => {
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'imagine-source',
aliases: ['sd-source', 'img-source'],
returns: 'a name of the current generation source',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'source name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: false,
forceEnum: true,
enumProvider: getSelectEnumProvider('sd_source', true),
}),
],
helpString: 'If an argument is provided, change the source of the image generation, e.g. <code>/imagine-source comfy</code>. Returns the current source.',
callback: async (_args, name) => {
if (!name) {
return extension_settings.sd.source;
}
const isKnownSource = Object.keys(sources).includes(String(name));
if (!isKnownSource) {
throw new Error('The value provided is not a valid image generation source.');
}
const option = document.querySelector(`#sd_source [value="${name}"]`);
if (!(option instanceof HTMLOptionElement)) {
throw new Error('Could not find the source option in the dropdown.');
}
option.selected = true;
await onSourceChange();
return extension_settings.sd.source;
},
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'imagine-style',
aliases: ['sd-style', 'img-style'],
returns: 'a name of the current style',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'style name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: false,
forceEnum: true,
enumProvider: getSelectEnumProvider('sd_style', false),
}),
],
helpString: 'If an argument is provided, change the style of the image generation, e.g. <code>/imagine-style MyStyle</code>. Returns the current style.',
callback: async (_args, name) => {
if (!name) {
return extension_settings.sd.style;
}
const option = document.querySelector(`#sd_style [value="${name}"]`);
if (!(option instanceof HTMLOptionElement)) {
throw new Error('Could not find the style option in the dropdown.');
}
option.selected = true;
onStyleSelect();
return extension_settings.sd.style;
},
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'imagine-comfy-workflow',
callback: changeComfyWorkflow,
@ -3832,7 +4164,7 @@ jQuery(async () => {
description: 'workflow name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: () => Array.from(document.querySelectorAll('#sd_comfy_workflow > [value]')).map(x => x.getAttribute('value')).map(workflow => new SlashCommandEnumValue(workflow)),
enumProvider: getSelectEnumProvider('sd_comfy_workflow', false),
}),
],
helpString: '(workflowName) - change the workflow to be used for image generation with ComfyUI, e.g. <pre><code>/imagine-comfy-workflow MyWorkflow</code></pre>',
@ -3874,7 +4206,6 @@ jQuery(async () => {
$('#sd_hr_scale').on('input', onHrScaleInput);
$('#sd_denoising_strength').on('input', onDenoisingStrengthInput);
$('#sd_hr_second_pass_steps').on('input', onHrSecondPassStepsInput);
$('#sd_novel_upscale_ratio').on('input', onNovelUpscaleRatioInput);
$('#sd_novel_anlas_guard').on('input', onNovelAnlasGuardInput);
$('#sd_novel_view_anlas').on('click', onViewAnlasClick);
$('#sd_novel_sm').on('input', onNovelSmInput);
@ -3887,7 +4218,6 @@ jQuery(async () => {
$('#sd_comfy_open_workflow_editor').on('click', onComfyOpenWorkflowEditorClick);
$('#sd_comfy_new_workflow').on('click', onComfyNewWorkflowClick);
$('#sd_comfy_delete_workflow').on('click', onComfyDeleteWorkflowClick);
$('#sd_expand').on('input', onExpandInput);
$('#sd_style').on('change', onStyleSelect);
$('#sd_save_style').on('click', onSaveStyleClick);
$('#sd_delete_style').on('click', onDeleteStyleClick);
@ -3908,6 +4238,7 @@ jQuery(async () => {
$('#sd_stability_key').on('click', onStabilityKeyClick);
$('#sd_stability_style_preset').on('change', onStabilityStylePresetChange);
$('#sd_huggingface_model_id').on('input', onHFModelInput);
$('#sd_function_tool').on('input', onFunctionToolInput);
if (!CSS.supports('field-sizing', 'content')) {
$('.sd_settings .inline-drawer-toggle').on('click', function () {

View File

@ -14,9 +14,13 @@
<input id="sd_refine_mode" type="checkbox" />
<span data-i18n="sd_refine_mode_txt">Edit prompts before generation</span>
</label>
<label for="sd_function_tool" class="checkbox_label" data-i18n="[title]sd_function_tool" title="Use the function tool to automatically detect intents to generate images.">
<input id="sd_function_tool" type="checkbox" />
<span data-i18n="sd_function_tool_txt">Use function tool</span>
</label>
<label for="sd_interactive_mode" class="checkbox_label" data-i18n="[title]sd_interactive_mode" title="Automatically generate images when sending messages like 'send me a picture of cat'.">
<input id="sd_interactive_mode" type="checkbox" />
<span data-i18n="sd_interactive_mode_txt">Interactive mode</span>
<span data-i18n="sd_interactive_mode_txt">Use interactive mode</span>
</label>
<label for="sd_multimodal_captioning" class="checkbox_label" data-i18n="[title]sd_multimodal_captioning" title="Use multimodal captioning to generate prompts for user and character portraits based on their avatars.">
<input id="sd_multimodal_captioning" type="checkbox" />
@ -27,11 +31,6 @@
<span data-i18n="sd_free_extend_txt">Extend free mode prompts</span>
<small data-i18n="sd_free_extend_small">(interactive/commands)</small>
</label>
<label for="sd_expand" class="checkbox_label" data-i18n="[title]sd_expand" title="Automatically extend prompts using text generation model">
<input id="sd_expand" type="checkbox" />
<span data-i18n="sd_expand_txt">Auto-extend prompts</span>
<span class="right_menu_button fa-solid fa-triangle-exclamation" data-i18n="[title]sd_expand_warning" title="May produce unexpected results. Manual prompt editing is recommended."></span>
</label>
<label for="sd_snap" class="checkbox_label" data-i18n="[title]sd_snap" title="Snap generation requests with a forced aspect ratio (portraits, backgrounds) to the nearest known resolution, while trying to preserve the absolute pixel counts (recommended for SDXL).">
<input id="sd_snap" type="checkbox" />
<span data-i18n="sd_snap_txt">Snap auto-adjusted resolutions</span>
@ -308,7 +307,7 @@
</div>
<div class="flex-container">
<div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad,drawthings">
<div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad,drawthings,novel">
<small>
<span data-i18n="Upscale by">Upscale by</span>
</small>
@ -332,14 +331,6 @@
<input class="neo-range-input" type="number" id="sd_hr_second_pass_steps_value" data-for="sd_hr_second_pass_steps" max="{{hr_second_pass_steps_max}}" step="{{hr_second_pass_steps_step}}" value="{{hr_second_pass_steps}}" >
</div>
<div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="novel">
<small>
<span data-i18n="Upscale by">Upscale by</span>
</small>
<input class="neo-range-slider" type="range" id="sd_novel_upscale_ratio" name="sd_novel_upscale_ratio" min="{{novel_upscale_ratio_min}}" max="{{novel_upscale_ratio_max}}" step="{{novel_upscale_ratio_step}}" value="{{novel_upscale_ratio}}" >
<input class="neo-range-input" type="number" id="sd_novel_upscale_ratio_value" data-for="sd_novel_upscale_ratio" min="{{novel_upscale_ratio_min}}" max="{{novel_upscale_ratio_max}}" step="{{novel_upscale_ratio_step}}" value="{{novel_upscale_ratio}}" >
</div>
<div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad,comfy,horde,drawthings,extras">
<small>
<span data-i18n="CLIP Skip">CLIP Skip</span>

View File

@ -0,0 +1,206 @@
import { saveTtsProviderSettings } from './index.js';
export { CosyVoiceProvider };
class CosyVoiceProvider {
//########//
// Config //
//########//
settings;
ready = false;
voices = [];
separator = '. ';
audioElement = document.createElement('audio');
/**
* Perform any text processing before passing to TTS engine.
* @param {string} text Input text
* @returns {string} Processed text
*/
processText(text) {
return text;
}
audioFormats = ['wav', 'ogg', 'silk', 'mp3', 'flac'];
languageLabels = {
'Auto': 'auto',
};
langKey2LangCode = {
'zh': 'zh-CN',
'en': 'en-US',
'ja': 'ja-JP',
'ko': 'ko-KR',
};
modelTypes = {
CosyVoice: 'CosyVoice',
};
defaultSettings = {
provider_endpoint: 'http://localhost:9880',
format: 'wav',
lang: 'auto',
streaming: false,
};
get settingsHtml() {
let html = `
<label for="tts_endpoint">Provider Endpoint:</label>
<input id="tts_endpoint" type="text" class="text_pole" maxlength="250" height="300" value="${this.defaultSettings.provider_endpoint}"/>
<span>Windows users Use <a target="_blank" href="https://github.com/v3ucn/CosyVoice_For_Windows">CosyVoice_For_Windows</a>(Unofficial).</span><br/>
<span>Macos Users Use <a target="_blank" href="https://github.com/v3ucn/CosyVoice_for_MacOs">CosyVoice_for_MacOs</a>(Unofficial).</span><br/>
<br/>
`;
return html;
}
onSettingsChange() {
// Used when provider settings are updated from UI
this.settings.provider_endpoint = $('#tts_endpoint').val();
saveTtsProviderSettings();
this.changeTTSSettings();
}
async loadSettings(settings) {
// Pupulate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.info('Using default TTS Provider settings');
}
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings;
for (const key in settings) {
if (key in this.settings) {
this.settings[key] = settings[key];
} else {
console.debug(`Ignoring non-user-configurable setting: ${key}`);
}
}
// Set initial values from the settings
$('#tts_endpoint').val(this.settings.provider_endpoint);
await this.checkReady();
console.info('ITS: Settings loaded');
}
// Perform a simple readiness check by trying to fetch voiceIds
async checkReady() {
await Promise.allSettled([this.fetchTtsVoiceObjects(), this.changeTTSSettings()]);
}
async onRefreshClick() {
return;
}
//#################//
// TTS Interfaces //
//#################//
async getVoice(voiceName) {
if (this.voices.length == 0) {
this.voices = await this.fetchTtsVoiceObjects();
}
const match = this.voices.filter(
v => v.name == voiceName,
)[0];
console.log(match);
if (!match) {
throw `TTS Voice name ${voiceName} not found`;
}
return match;
}
async generateTts(text, voiceId) {
const response = await this.fetchTtsGeneration(text, voiceId);
return response;
}
//###########//
// API CALLS //
//###########//
async fetchTtsVoiceObjects() {
const response = await fetch(`${this.settings.provider_endpoint}/speakers`);
console.info(response);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`);
}
const responseJson = await response.json();
this.voices = responseJson;
return responseJson;
}
// Each time a parameter is changed, we change the configuration
async changeTTSSettings() {
}
/**
* Fetch TTS generation from the API.
* @param {string} inputText Text to generate TTS for
* @param {string} voiceId Voice ID to use (model_type&speaker_id))
* @returns {Promise<Response|string>} Fetch response
*/
async fetchTtsGeneration(inputText, voiceId, lang = null, forceNoStreaming = false) {
console.info(`Generating new TTS for voice_id ${voiceId}`);
const streaming = this.settings.streaming;
const params = {
text: inputText,
speaker: voiceId,
};
if (streaming) {
params['streaming'] = 1;
}
const url = `${this.settings.provider_endpoint}/`;
const response = await fetch(
url,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(params), // Convert parameter objects to JSON strings
},
);
if (!response.ok) {
toastr.error(response.statusText, 'TTS Generation Failed');
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return response;
}
// Interface not used
async fetchTtsFromHistory(history_item_id) {
return Promise.resolve(history_item_id);
}
}

View File

@ -0,0 +1,226 @@
import { saveTtsProviderSettings } from './index.js';
export { GptSovitsV2Provider };
class GptSovitsV2Provider {
//########//
// Config //
//########//
settings;
ready = false;
voices = [];
separator = '. ';
audioElement = document.createElement('audio');
/**
* Perform any text processing before passing to TTS engine.
* @param {string} text Input text
* @returns {string} Processed text
*/
processText(text) {
return text;
}
audioFormats = ['wav', 'ogg', 'silk', 'mp3', 'flac'];
languageLabels = {
'Auto': 'auto',
};
langKey2LangCode = {
'zh': 'zh-CN',
'en': 'en-US',
'ja': 'ja-JP',
'ko': 'ko-KR',
};
defaultSettings = {
provider_endpoint: 'http://localhost:9880',
format: 'wav',
lang: 'auto',
streaming: false,
text_lang: 'zh',
prompt_lang: 'zh',
};
get settingsHtml() {
let html = `
<label for="tts_endpoint">Provider Endpoint:</label>
<input id="tts_endpoint" type="text" class="text_pole" maxlength="250" height="300" value="${this.defaultSettings.provider_endpoint}"/>
<span>Use <a target="_blank" href="https://github.com/v3ucn/GPT-SoVITS-V2">GPT-SoVITS-V2</a>(Unofficial).</span><br/>
<label for="text_lang">Text Lang(Inference text language):</label>
<input id="text_lang" type="text" class="text_pole" maxlength="250" height="300" value="${this.defaultSettings.text_lang}"/>
<label for="text_lang">Prompt Lang(Reference audio text language):</label>
<input id="prompt_lang" type="text" class="text_pole" maxlength="250" height="300" value="${this.defaultSettings.prompt_lang}"/>
<br/>
`;
return html;
}
onSettingsChange() {
// Used when provider settings are updated from UI
this.settings.provider_endpoint = $('#tts_endpoint').val();
this.settings.text_lang = $('#text_lang').val();
this.settings.prompt_lang = $('#prompt_lang').val();
saveTtsProviderSettings();
this.changeTTSSettings();
}
async loadSettings(settings) {
// Pupulate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.info('Using default TTS Provider settings');
}
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings;
for (const key in settings) {
if (key in this.settings) {
this.settings[key] = settings[key];
} else {
console.debug(`Ignoring non-user-configurable setting: ${key}`);
}
}
// Set initial values from the settings
$('#tts_endpoint').val(this.settings.provider_endpoint);
$('#text_lang').val(this.settings.text_lang);
$('#prompt_lang').val(this.settings.prompt_lang);
await this.checkReady();
console.info('ITS: Settings loaded');
}
// Perform a simple readiness check by trying to fetch voiceIds
async checkReady() {
await Promise.allSettled([this.fetchTtsVoiceObjects(), this.changeTTSSettings()]);
}
async onRefreshClick() {
return;
}
//#################//
// TTS Interfaces //
//#################//
async getVoice(voiceName) {
if (this.voices.length == 0) {
this.voices = await this.fetchTtsVoiceObjects();
}
const match = this.voices.filter(
v => v.name == voiceName,
)[0];
console.log(match);
if (!match) {
throw `TTS Voice name ${voiceName} not found`;
}
return match;
}
async generateTts(text, voiceId) {
const response = await this.fetchTtsGeneration(text, voiceId);
return response;
}
//###########//
// API CALLS //
//###########//
async fetchTtsVoiceObjects() {
const response = await fetch(`${this.settings.provider_endpoint}/speakers`);
console.info(response);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`);
}
const responseJson = await response.json();
this.voices = responseJson;
return responseJson;
}
// Each time a parameter is changed, we change the configuration
async changeTTSSettings() {
}
/**
* Fetch TTS generation from the API.
* @param {string} inputText Text to generate TTS for
* @param {string} voiceId Voice ID to use (model_type&speaker_id))
* @returns {Promise<Response|string>} Fetch response
*/
async fetchTtsGeneration(inputText, voiceId, lang = null, forceNoStreaming = false) {
console.info(`Generating new TTS for voice_id ${voiceId}`);
function replaceSpeaker(text) {
return text.replace(/\[.*?\]/gu, '');
}
let prompt_text = replaceSpeaker(voiceId);
const streaming = this.settings.streaming;
const params = {
text: inputText,
prompt_text: prompt_text,
ref_audio_path: './参考音频/' + voiceId + '.wav',
text_lang: this.settings.text_lang,
prompt_lang: this.settings.prompt_lang,
text_split_method: 'cut5',
batch_size: 1,
media_type: 'ogg',
streaming_mode: 'true',
};
const url = `${this.settings.provider_endpoint}/`;
const response = await fetch(
url,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(params), // Convert parameter objects to JSON strings
},
);
if (!response.ok) {
toastr.error(response.statusText, 'TTS Generation Failed');
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return response;
}
// Interface not used
async fetchTtsFromHistory(history_item_id) {
return Promise.resolve(history_item_id);
}
}

View File

@ -4,6 +4,7 @@ import { delay, escapeRegex, getBase64Async, getStringHash, onlyUnique } from '.
import { EdgeTtsProvider } from './edge.js';
import { ElevenLabsTtsProvider } from './elevenlabs.js';
import { SileroTtsProvider } from './silerotts.js';
import { GptSovitsV2Provider } from './gpt-sovits-v2.js';
import { CoquiTtsProvider } from './coqui.js';
import { SystemTtsProvider } from './system.js';
import { NovelTtsProvider } from './novel.js';
@ -15,6 +16,7 @@ import { VITSTtsProvider } from './vits.js';
import { GSVITtsProvider } from './gsvi.js';
import { SBVits2TtsProvider } from './sbvits2.js';
import { AllTalkTtsProvider } from './alltalk.js';
import { CosyVoiceProvider } from './cosyvoice.js';
import { SpeechT5TtsProvider } from './speecht5.js';
import { AzureTtsProvider } from './azure.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
@ -86,9 +88,11 @@ const ttsProviders = {
AllTalk: AllTalkTtsProvider,
Azure: AzureTtsProvider,
Coqui: CoquiTtsProvider,
'CosyVoice (Unofficial)': CosyVoiceProvider,
Edge: EdgeTtsProvider,
ElevenLabs: ElevenLabsTtsProvider,
GSVI: GSVITtsProvider,
'GPT-SoVITS-V2 (Unofficial)': GptSovitsV2Provider,
Novel: NovelTtsProvider,
OpenAI: OpenAITtsProvider,
'OpenAI Compatible': OpenAICompatibleTtsProvider,
@ -470,7 +474,7 @@ async function processTtsQueue() {
}
if (extension_settings.tts.narrate_quoted_only) {
const special_quotes = /[“”«»]/g; // Extend this regex to include other special quotes
const special_quotes = /[“”«»「」『』""]/g; // Extend this regex to include other special quotes
text = text.replace(special_quotes, '"');
const matches = text.match(/".*?"/g); // Matches text inside double quotes, non-greedily
const partJoiner = (ttsProvider?.separator || ' ... ');

View File

@ -466,13 +466,13 @@ async function ingestDataBankAttachments(source) {
}
// Download and process the file
file.text = await getFileAttachment(file.url);
const fileText = await getFileAttachment(file.url);
console.log(`Vectors: Retrieved file ${file.name} from Data Bank`);
// Convert kilobytes to string length
const thresholdLength = settings.size_threshold_db * 1024;
// Use chunk size from settings if file is larger than threshold
const chunkSize = file.size > thresholdLength ? settings.chunk_size_db : -1;
await vectorizeFile(file.text, file.name, collectionId, chunkSize, settings.overlap_percent_db);
await vectorizeFile(fileText, file.name, collectionId, chunkSize, settings.overlap_percent_db);
}
return dataBankCollectionIds;
@ -999,25 +999,6 @@ async function purgeAllVectorIndexes() {
}
}
async function isModelScopesEnabled() {
try {
const response = await fetch('/api/vector/scopes-enabled', {
method: 'GET',
headers: getVectorHeaders(),
});
if (!response.ok) {
return false;
}
const data = await response.json();
return data?.enabled ?? false;
} catch (error) {
console.error('Vectors: Failed to check model scopes', error);
return false;
}
}
function toggleSettings() {
$('#vectors_files_settings').toggle(!!settings.enabled_files);
$('#vectors_chats_settings').toggle(!!settings.enabled_chats);
@ -1282,7 +1263,6 @@ jQuery(async () => {
}
Object.assign(settings, extension_settings.vectors);
const scopesEnabled = await isModelScopesEnabled();
// Migrate from TensorFlow to Transformers
settings.source = settings.source !== 'local' ? settings.source : 'transformers';
@ -1294,7 +1274,6 @@ jQuery(async () => {
saveSettingsDebounced();
toggleSettings();
});
$('#vectors_modelWarning').hide();
$('#vectors_enabled_files').prop('checked', settings.enabled_files).on('input', () => {
settings.enabled_files = $('#vectors_enabled_files').prop('checked');
Object.assign(extension_settings.vectors, settings);
@ -1334,31 +1313,26 @@ jQuery(async () => {
saveSettingsDebounced();
});
$('#vectors_togetherai_model').val(settings.togetherai_model).on('change', () => {
!scopesEnabled && $('#vectors_modelWarning').show();
settings.togetherai_model = String($('#vectors_togetherai_model').val());
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
$('#vectors_openai_model').val(settings.openai_model).on('change', () => {
!scopesEnabled && $('#vectors_modelWarning').show();
settings.openai_model = String($('#vectors_openai_model').val());
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
$('#vectors_cohere_model').val(settings.cohere_model).on('change', () => {
!scopesEnabled && $('#vectors_modelWarning').show();
settings.cohere_model = String($('#vectors_cohere_model').val());
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
$('#vectors_ollama_model').val(settings.ollama_model).on('input', () => {
!scopesEnabled && $('#vectors_modelWarning').show();
settings.ollama_model = String($('#vectors_ollama_model').val());
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
$('#vectors_vllm_model').val(settings.vllm_model).on('input', () => {
!scopesEnabled && $('#vectors_modelWarning').show();
settings.vllm_model = String($('#vectors_vllm_model').val());
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();

View File

@ -96,14 +96,6 @@
</i>
</div>
<small id="vectors_modelWarning">
<i class="fa-solid fa-exclamation-triangle"></i>
<span>
Set <code>vectors.enableModelScopes</code> to true in config.yaml to switch between vectorization models without needing to purge existing vectors.
This option will soon be enabled by default.
</span>
</small>
<div class="flex-container alignItemsCenter" id="nomicai_apiKey">
<label for="api_key_nomicai" class="flex1">
<span data-i18n="NomicAI API Key">NomicAI API Key</span>

View File

@ -70,12 +70,12 @@ import {
animation_duration,
depth_prompt_role_default,
shouldAutoContinue,
this_chid,
} from '../script.js';
import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js';
import { FILTER_TYPES, FilterHelper } from './filters.js';
import { isExternalMediaAllowed } from './chats.js';
import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { t } from './i18n.js';
export {
selected_group,
@ -189,8 +189,8 @@ async function validateGroup(group) {
group.members = group.members.filter(member => {
const character = characters.find(x => x.avatar === member || x.name === member);
if (!character) {
const msg = `Warning: Listed member ${member} does not exist as a character. It will be removed from the group.`;
toastr.warning(msg, 'Group Validation');
const msg = t`Warning: Listed member ${member} does not exist as a character. It will be removed from the group.`;
toastr.warning(msg, t`Group Validation`);
console.warn(msg);
dirty = true;
}
@ -522,7 +522,7 @@ async function saveGroupChat(groupId, shouldSaveGroup) {
});
if (!response.ok) {
toastr.error('Check the server connection and reload the page to prevent data loss.', 'Group Chat could not be saved');
toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Group Chat could not be saved`);
console.error('Group chat could not be saved', response);
return;
}
@ -837,7 +837,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
activatedMembers = activateSwipe(group.members);
if (activatedMembers.length === 0) {
toastr.warning('Deleted group member swiped. To get a reply, add them back to the group.');
toastr.warning(t`Deleted group member swiped. To get a reply, add them back to the group.`);
throw new Error('Deleted group member swiped');
}
}
@ -1368,15 +1368,15 @@ function isGroupMemberDisabled(avatarId) {
async function onDeleteGroupClick() {
if (!openGroupId) {
toastr.warning('Currently no group selected.');
toastr.warning(t`Currently no group selected.`);
return;
}
if (is_group_generating) {
toastr.warning('Not so fast! Wait for the characters to stop typing before deleting the group.');
toastr.warning(t`Not so fast! Wait for the characters to stop typing before deleting the group.`);
return;
}
const confirm = await Popup.show.confirm('Delete the group?', '<p>This will also delete all your chats with that group. If you want to delete a single conversation, select a "View past chats" option in the lower left menu.</p>');
const confirm = await Popup.show.confirm(t`Delete the group?`, '<p>' + t`This will also delete all your chats with that group. If you want to delete a single conversation, select a "View past chats" option in the lower left menu.` + '</p>');
if (confirm) {
deleteGroup(openGroupId);
}
@ -1630,7 +1630,7 @@ function updateFavButtonState(state) {
export async function openGroupById(groupId) {
if (isChatSaving) {
toastr.info('Please wait until the chat is saved before switching characters.', 'Your chat is still saving...');
toastr.info(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`);
return;
}
@ -1659,7 +1659,7 @@ export async function openGroupById(groupId) {
function openCharacterDefinition(characterSelect) {
if (is_group_generating) {
toastr.warning('Can\'t peek a character while group reply is being generated');
toastr.warning(t`Can't peek a character while group reply is being generated`);
console.warn('Can\'t peek a character def while group reply is being generated');
return;
}
@ -1908,7 +1908,7 @@ export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) {
});
if (!response.ok) {
toastr.error('Check the server connection and reload the page to prevent data loss.', 'Group chat could not be saved');
toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Group chat could not be saved`);
console.error('Group chat could not be saved', response);
}
}

View File

@ -215,6 +215,7 @@ function addLanguagesToDropdown() {
}
export async function initLocales() {
moment.locale(localeFile);
langs = await fetch('/locales/lang.json').then(response => response.json());
localeData = await getLocaleData(localeFile);
applyLocale();

View File

@ -384,6 +384,10 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata
* @returns {string} Formatted instruct mode system prompt.
*/
export function formatInstructModeSystemPrompt(systemPrompt) {
if (!systemPrompt) {
return '';
}
const separator = power_user.instruct.wrap ? '\n' : '';
if (power_user.instruct.system_sequence_prefix) {
@ -550,11 +554,9 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1,
* @param {string} name Preset name.
*/
function selectMatchingContextTemplate(name) {
let foundMatch = false;
for (const context_preset of context_presets) {
// If context template matches the instruct preset
if (context_preset.name === name) {
foundMatch = true;
selectContextPreset(context_preset.name, { isAuto: true });
break;
}

View File

@ -188,7 +188,7 @@ export async function generateKoboldWithStreaming(generate_data, signal) {
if (data?.token) {
text += data.token;
}
yield { text, swipes: [] };
yield { text, swipes: [], toolCalls: [] };
}
};
}

View File

@ -180,7 +180,13 @@ function displayError(message) {
* Preserves the query string.
*/
function redirectToHome() {
window.location.href = '/' + window.location.search;
// After a login theres no need to preserve the
// noauto (if present)
const urlParams = new URLSearchParams(window.location.search);
urlParams.delete('noauto');
window.location.href = '/' + urlParams.toString();
}
/**

View File

@ -492,11 +492,37 @@ function getBadWordPermutations(text) {
export function getNovelGenerationData(finalPrompt, settings, maxLength, isImpersonate, isContinue, _cfgValues, type) {
console.debug('NovelAI generation data for', type);
const isKayra = nai_settings.model_novel.includes('kayra');
const isErato = nai_settings.model_novel.includes('erato');
const tokenizerType = getTokenizerTypeForModel(nai_settings.model_novel);
const stoppingStrings = getStoppingStrings(isImpersonate, isContinue);
// Llama 3 tokenizer, huh?
if (isErato) {
const additionalStopStrings = [];
for (const stoppingString of stoppingStrings) {
if (stoppingString.startsWith('\n')) {
additionalStopStrings.push('.' + stoppingString);
additionalStopStrings.push('!' + stoppingString);
additionalStopStrings.push('?' + stoppingString);
additionalStopStrings.push('*' + stoppingString);
additionalStopStrings.push('"' + stoppingString);
additionalStopStrings.push('_' + stoppingString);
additionalStopStrings.push('...' + stoppingString);
additionalStopStrings.push('."' + stoppingString);
additionalStopStrings.push('?"' + stoppingString);
additionalStopStrings.push('!"' + stoppingString);
additionalStopStrings.push('.*' + stoppingString);
additionalStopStrings.push(')' + stoppingString);
}
}
stoppingStrings.push(...additionalStopStrings);
}
const MAX_STOP_SEQUENCES = 1024;
const stopSequences = (tokenizerType !== tokenizers.NONE)
? getStoppingStrings(isImpersonate, isContinue)
.map(t => getTextTokens(tokenizerType, t))
? stoppingStrings.slice(0, MAX_STOP_SEQUENCES).map(t => getTextTokens(tokenizerType, t))
: undefined;
const badWordIds = (tokenizerType !== tokenizers.NONE)
@ -515,11 +541,9 @@ export function getNovelGenerationData(finalPrompt, settings, maxLength, isImper
console.log(finalPrompt);
}
const isKayra = nai_settings.model_novel.includes('kayra');
const isErato = nai_settings.model_novel.includes('erato');
if (isErato) {
finalPrompt = '<|startoftext|>' + finalPrompt;
finalPrompt = '<|startoftext|><|reserved_special_token81|>' + finalPrompt;
}
const adjustedMaxLength = (isKayra || isErato) ? getKayraMaxResponseTokens() : maximum_output_length;
@ -722,7 +746,7 @@ export async function generateNovelWithStreaming(generate_data, signal) {
text += data.token;
}
yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs) };
yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs), toolCalls: [] };
}
};
}

View File

@ -61,12 +61,6 @@ import {
stringFormat,
} from './utils.js';
import { countTokensOpenAI, getTokenizerModel } from './tokenizers.js';
import {
formatInstructModeChat,
formatInstructModeExamples,
formatInstructModePrompt,
formatInstructModeSystemPrompt,
} from './instruct-mode.js';
import { isMobile } from './RossAscends-mods.js';
import { saveLogprobsForActiveMessage } from './logprobs.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
@ -76,6 +70,7 @@ import { renderTemplateAsync } from './templates.js';
import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
import { Popup, POPUP_RESULT } from './popup.js';
import { t } from './i18n.js';
import { ToolManager } from './tool-calling.js';
export {
openai_messages_count,
@ -204,7 +199,10 @@ const continue_postfix_types = {
const custom_prompt_post_processing_types = {
NONE: '',
/** @deprecated Use MERGE instead. */
CLAUDE: 'claude',
MERGE: 'merge',
STRICT: 'strict',
};
const sensitiveFields = [
@ -227,7 +225,6 @@ const default_settings = {
top_a_openai: 0,
repetition_penalty_openai: 1,
stream_openai: false,
websearch_cohere: false,
openai_max_context: max_4k,
openai_max_tokens: 300,
wrap_in_quotes: false,
@ -263,7 +260,6 @@ const default_settings = {
windowai_model: '',
openrouter_model: openrouter_website_model,
openrouter_use_fallback: false,
openrouter_force_instruct: false,
openrouter_group_models: false,
openrouter_sort_models: 'alphabetically',
openrouter_providers: [],
@ -305,7 +301,6 @@ const oai_settings = {
top_a_openai: 0,
repetition_penalty_openai: 1,
stream_openai: false,
websearch_cohere: false,
openai_max_context: max_4k,
openai_max_tokens: 300,
wrap_in_quotes: false,
@ -341,7 +336,6 @@ const oai_settings = {
windowai_model: '',
openrouter_model: openrouter_website_model,
openrouter_use_fallback: false,
openrouter_force_instruct: false,
openrouter_group_models: false,
openrouter_sort_models: 'alphabetically',
openrouter_providers: [],
@ -396,7 +390,7 @@ async function validateReverseProxy() {
new URL(oai_settings.reverse_proxy);
}
catch (err) {
toastr.error('Entered reverse proxy address is not a valid URL');
toastr.error(t`Entered reverse proxy address is not a valid URL`);
setOnlineStatus('no_connection');
resultCheckStatus();
throw err;
@ -407,7 +401,7 @@ async function validateReverseProxy() {
const confirmation = skipConfirm || await Popup.show.confirm(t`Connecting To Proxy`, await renderTemplateAsync('proxyConnectionWarning', { proxyURL: DOMPurify.sanitize(oai_settings.reverse_proxy) }));
if (!confirmation) {
toastr.error('Update or remove your reverse proxy settings.');
toastr.error(t`Update or remove your reverse proxy settings.`);
setOnlineStatus('no_connection');
resultCheckStatus();
throw new Error('Proxy connection denied.');
@ -416,108 +410,6 @@ async function validateReverseProxy() {
localStorage.setItem(rememberKey, String(true));
}
/**
* Converts the Chat Completion object to an Instruct Mode prompt string.
* @param {object[]} messages Array of messages
* @param {string} type Generation type
* @returns {string} Text completion prompt
*/
function convertChatCompletionToInstruct(messages, type) {
const newChatPrompts = [
substituteParams(oai_settings.new_chat_prompt),
substituteParams(oai_settings.new_example_chat_prompt),
substituteParams(oai_settings.new_group_chat_prompt),
];
messages = messages.filter(x => !newChatPrompts.includes(x.content));
let chatMessagesText = '';
let systemPromptText = '';
let examplesText = '';
function getPrefix(message) {
let prefix;
if (message.role === 'user' || message.name === 'example_user') {
if (selected_group) {
prefix = '';
} else if (message.name === 'example_user') {
prefix = name1;
} else {
prefix = message.name ?? name1;
}
}
if (message.role === 'assistant' || message.name === 'example_assistant') {
if (selected_group) {
prefix = '';
}
else if (message.name === 'example_assistant') {
prefix = name2;
} else {
prefix = message.name ?? name2;
}
}
return prefix;
}
function toString(message) {
if (message.role === 'system' && !message.name) {
return message.content;
}
const prefix = getPrefix(message);
return prefix ? `${prefix}: ${message.content}` : message.content;
}
const firstChatMessage = messages.findIndex(message => message.role === 'assistant' || message.role === 'user');
const systemPromptMessages = messages.slice(0, firstChatMessage).filter(message => message.role === 'system' && !message.name);
if (systemPromptMessages.length) {
systemPromptText = systemPromptMessages.map(message => message.content).join('\n');
systemPromptText = formatInstructModeSystemPrompt(systemPromptText);
}
const exampleMessages = messages.filter(x => x.role === 'system' && (x.name === 'example_user' || x.name === 'example_assistant'));
if (exampleMessages.length) {
const blockHeading = power_user.context.example_separator ? (substituteParams(power_user.context.example_separator) + '\n') : '';
const examplesArray = exampleMessages.map(m => '<START>\n' + toString(m));
examplesText = blockHeading + formatInstructModeExamples(examplesArray, name1, name2).join('');
}
const chatMessages = messages.slice(firstChatMessage);
if (chatMessages.length) {
chatMessagesText = substituteParams(power_user.context.chat_start) + '\n';
for (const message of chatMessages) {
const name = getPrefix(message);
const isUser = message.role === 'user';
const isNarrator = message.role === 'system';
chatMessagesText += formatInstructModeChat(name, message.content, isUser, isNarrator, '', name1, name2, false);
}
}
const isImpersonate = type === 'impersonate';
const isContinue = type === 'continue';
const isQuiet = type === 'quiet';
const isQuietToLoud = false; // Quiet to loud not implemented for Chat Completion
const promptName = isImpersonate ? name1 : name2;
const promptLine = isContinue ? '' : formatInstructModePrompt(promptName, isImpersonate, '', name1, name2, isQuiet, isQuietToLoud).trimStart();
let prompt = [systemPromptText, examplesText, chatMessagesText, promptLine]
.filter(x => x)
.map(x => x.endsWith('\n') ? x : `${x}\n`)
.join('');
if (isContinue) {
prompt = prompt.replace(/\n$/, '');
}
return prompt;
}
/**
* Formats chat messages into chat completion messages.
* @param {object[]} chat - Array containing all messages.
@ -563,7 +455,8 @@ function setOpenAIMessages(chat) {
if (role == 'user' && oai_settings.wrap_in_quotes) content = `"${content}"`;
const name = chat[j]['name'];
const image = chat[j]?.extra?.image;
messages[i] = { 'role': role, 'content': content, name: name, 'image': image };
const invocations = chat[j]?.extra?.tool_invocations;
messages[i] = { 'role': role, 'content': content, name: name, 'image': image, 'invocations': invocations };
j++;
}
@ -761,10 +654,6 @@ function populationInjectionPrompts(prompts, messages) {
return messages;
}
export function isOpenRouterWithInstruct() {
return oai_settings.chat_completion_source === chat_completion_sources.OPENROUTER && oai_settings.openrouter_force_instruct && power_user.instruct.enabled;
}
/**
* Populates the chat history of the conversation.
* @param {object[]} messages - Array containing all messages.
@ -795,8 +684,7 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
// Reserve budget for continue nudge
let continueMessage = null;
const instruct = isOpenRouterWithInstruct();
if (type === 'continue' && cyclePrompt && !instruct && !oai_settings.continue_prefill) {
if (type === 'continue' && cyclePrompt && !oai_settings.continue_prefill) {
const promptObject = {
identifier: 'continueNudge',
role: 'system',
@ -816,6 +704,7 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
}
const imageInlining = isImageInliningSupported();
const canUseTools = ToolManager.isToolCallingSupported();
// Insert chat messages as long as there is budget available
const chatPool = [...messages].reverse();
@ -837,8 +726,32 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
await chatMessage.addImage(chatPrompt.image);
}
if (canUseTools && Array.isArray(chatPrompt.invocations)) {
/** @type {import('./tool-calling.js').ToolInvocation[]} */
const invocations = chatPrompt.invocations;
const toolCallMessage = new Message(chatMessage.role, undefined, 'toolCall-' + chatMessage.identifier);
const toolResultMessages = invocations.slice().reverse().map((invocation) => new Message('tool', invocation.result || '[No content]', invocation.id));
toolCallMessage.setToolCalls(invocations);
if (chatCompletion.canAffordAll([toolCallMessage, ...toolResultMessages])) {
for (const resultMessage of toolResultMessages) {
chatCompletion.insertAtStart(resultMessage, 'chatHistory');
}
chatCompletion.insertAtStart(toolCallMessage, 'chatHistory');
} else {
break;
}
continue;
}
if (chatCompletion.canAfford(chatMessage)) {
if (type === 'continue' && oai_settings.continue_prefill && chatPrompt === firstNonInjected) {
// 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
if (chatPrompt.role === 'assistant') {
const collection = new MessageCollection('continuePrefill', new Message(chatMessage.role, substituteParams(oai_settings.assistant_prefill + '\n\n') + chatMessage.content, chatMessage.identifier));
chatCompletion.add(collection, -1);
continue;
}
const collection = new MessageCollection('continuePrefill', chatMessage);
chatCompletion.add(collection, -1);
continue;
@ -1340,15 +1253,15 @@ export async function prepareOpenAIMessages({
await populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, quietImage, type, cyclePrompt, messages, messageExamples });
} catch (error) {
if (error instanceof TokenBudgetExceededError) {
toastr.error('An error occurred while counting tokens: Token budget exceeded.');
toastr.error(t`An error occurred while counting tokens: Token budget exceeded.`);
chatCompletion.log('Token budget exceeded.');
promptManager.error = 'Not enough free tokens for mandatory prompts. Raise your token Limit or disable custom prompts.';
promptManager.error = t`Not enough free tokens for mandatory prompts. Raise your token Limit or disable custom prompts.`;
} else if (error instanceof InvalidCharacterNameError) {
toastr.warning('An error occurred while counting tokens: Invalid character name');
toastr.warning(t`An error occurred while counting tokens: Invalid character name`);
chatCompletion.log('Invalid character name');
promptManager.error = 'The name of at least one character contained whitespaces or special characters. Please check your user and character name.';
promptManager.error = t`The name of at least one character contained whitespaces or special characters. Please check your user and character name.`;
} else {
toastr.error('An unknown error occurred while counting tokens. Further information may be available in console.');
toastr.error(t`An unknown error occurred while counting tokens. Further information may be available in console.`);
chatCompletion.log('----- Unexpected error while preparing prompts -----');
chatCompletion.log(error);
chatCompletion.log(error.stack);
@ -1371,7 +1284,7 @@ export async function prepareOpenAIMessages({
const eventData = { chat, dryRun };
await eventSource.emit(event_types.CHAT_COMPLETION_PROMPT_READY, eventData);
openai_messages_count = chat.filter(x => x?.role === 'user' || x?.role === 'assistant')?.length || 0;
openai_messages_count = chat.filter(x => !x?.tool_calls && (x?.role === 'user' || x?.role === 'assistant'))?.length || 0;
return [chat, promptManager.tokenHandler.counts];
}
@ -1402,11 +1315,8 @@ function tryParseStreamingError(response, decoded) {
}
}
function checkQuotaError(data) {
const errorText = `<h3>Encountered an error while processing your request.<br>
Check you have credits available on your
<a href="https://platform.openai.com/account/usage" target="_blank">OpenAI account</a>.<br>
If you have sufficient credits, please try again later.</h3>`;
async function checkQuotaError(data) {
const errorText = await renderTemplateAsync('quotaError');
if (!data) {
return;
@ -1799,7 +1709,6 @@ async function sendOpenAIRequest(type, messages, signal) {
messages = messages.filter(msg => msg && typeof msg === 'object');
let logit_bias = {};
const messageId = getNextMessageId(type);
const isClaude = oai_settings.chat_completion_source == chat_completion_sources.CLAUDE;
const isOpenRouter = oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER;
const isScale = oai_settings.chat_completion_source == chat_completion_sources.SCALE;
@ -1811,7 +1720,7 @@ async function sendOpenAIRequest(type, messages, signal) {
const isPerplexity = oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY;
const isGroq = oai_settings.chat_completion_source == chat_completion_sources.GROQ;
const is01AI = oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI;
const isTextCompletion = (isOAI && textCompletionModels.includes(oai_settings.openai_model)) || (isOpenRouter && oai_settings.openrouter_force_instruct && power_user.instruct.enabled);
const isTextCompletion = isOAI && textCompletionModels.includes(oai_settings.openai_model);
const isQuiet = type === 'quiet';
const isImpersonate = type === 'impersonate';
const isContinue = type === 'continue';
@ -1819,11 +1728,6 @@ async function sendOpenAIRequest(type, messages, signal) {
const useLogprobs = !!power_user.request_token_probabilities;
const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom);
if (isTextCompletion && isOpenRouter) {
messages = convertChatCompletionToInstruct(messages, type);
replaceItemizedPromptText(messageId, messages);
}
// If we're using the window.ai extension, use that instead
// Doesn't support logit bias yet
if (oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI) {
@ -1890,8 +1794,8 @@ async function sendOpenAIRequest(type, messages, signal) {
generate_data['claude_use_sysprompt'] = oai_settings.claude_use_sysprompt;
generate_data['stop'] = getCustomStoppingStrings(); // Claude shouldn't have limits on stop strings.
generate_data['human_sysprompt_message'] = substituteParams(oai_settings.human_sysprompt_message);
// Don't add a prefill on quiet gens (summarization)
if (!isQuiet) {
// Don't add a prefill on quiet gens (summarization) and when using continue prefill.
if (!isQuiet && !(isContinue && oai_settings.continue_prefill)) {
generate_data['assistant_prefill'] = isImpersonate ? substituteParams(oai_settings.assistant_impersonation) : substituteParams(oai_settings.assistant_prefill);
}
}
@ -1915,10 +1819,9 @@ async function sendOpenAIRequest(type, messages, signal) {
}
if (isGoogle) {
const nameStopString = isImpersonate ? `\n${name2}:` : `\n${name1}:`;
const stopStringsLimit = 3; // 5 - 2 (nameStopString and new_chat_prompt)
const stopStringsLimit = 5;
generate_data['top_k'] = Number(oai_settings.top_k_openai);
generate_data['stop'] = [nameStopString, substituteParams(oai_settings.new_chat_prompt), ...getCustomStoppingStrings(stopStringsLimit)];
generate_data['stop'] = getCustomStoppingStrings(stopStringsLimit).slice(0, stopStringsLimit).filter(x => x.length >= 1 && x.length <= 16);
generate_data['use_makersuite_sysprompt'] = oai_settings.use_makersuite_sysprompt;
}
@ -1942,7 +1845,6 @@ async function sendOpenAIRequest(type, messages, signal) {
generate_data['frequency_penalty'] = Math.min(Math.max(Number(oai_settings.freq_pen_openai), 0), 1);
generate_data['presence_penalty'] = Math.min(Math.max(Number(oai_settings.pres_pen_openai), 0), 1);
generate_data['stop'] = getCustomStoppingStrings(5);
generate_data['websearch'] = oai_settings.websearch_cohere;
}
if (isPerplexity) {
@ -1978,8 +1880,8 @@ async function sendOpenAIRequest(type, messages, signal) {
generate_data['seed'] = oai_settings.seed;
}
if (isFunctionCallingSupported() && !stream) {
await registerFunctionTools(type, generate_data);
if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) {
await ToolManager.registerFunctionToolsOpenAI(generate_data);
}
if (isOAI && oai_settings.openai_model.startsWith('o1-')) {
@ -2026,6 +1928,7 @@ async function sendOpenAIRequest(type, messages, signal) {
return async function* streamData() {
let text = '';
const swipes = [];
const toolCalls = [];
while (true) {
const { done, value } = await reader.read();
if (done) return;
@ -2041,18 +1944,20 @@ async function sendOpenAIRequest(type, messages, signal) {
text += getStreamingReply(parsed);
}
yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed) };
ToolManager.parseToolCalls(toolCalls, parsed);
yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls };
}
};
}
else {
const data = await response.json();
checkQuotaError(data);
await checkQuotaError(data);
checkModerationError(data);
if (data.error) {
toastr.error(data.error.message || response.statusText, 'API returned an error');
toastr.error(data.error.message || response.statusText, t`API returned an error`);
throw new Error(data);
}
@ -2063,154 +1968,19 @@ async function sendOpenAIRequest(type, messages, signal) {
delay(1).then(() => saveLogprobsForActiveMessage(logprobs, null));
}
if (isFunctionCallingSupported()) {
await checkFunctionToolCalls(data);
}
return data;
}
}
/**
* Register function tools for the next chat completion request.
* @param {string} type Generation type
* @param {object} data Generation data
*/
async function registerFunctionTools(type, data) {
let toolChoice = 'auto';
const tools = [];
/**
* @type {registerFunctionTool}
*/
const registerFunctionTool = (name, description, parameters, required) => {
tools.push({
type: 'function',
function: {
name,
description,
parameters,
},
});
if (required) {
toolChoice = 'required';
}
};
/**
* @type {FunctionToolRegister}
*/
const args = {
type,
data,
registerFunctionTool,
};
await eventSource.emit(event_types.LLM_FUNCTION_TOOL_REGISTER, args);
if (tools.length) {
console.log('Registered function tools:', tools);
data['tools'] = tools;
data['tool_choice'] = toolChoice;
}
}
async function checkFunctionToolCalls(data) {
const oaiCompat = [
chat_completion_sources.OPENAI,
chat_completion_sources.CUSTOM,
chat_completion_sources.MISTRALAI,
chat_completion_sources.OPENROUTER,
chat_completion_sources.GROQ,
];
if (oaiCompat.includes(oai_settings.chat_completion_source)) {
if (!Array.isArray(data?.choices)) {
return;
}
// Find a choice with 0-index
const choice = data.choices.find(choice => choice.index === 0);
if (!choice) {
return;
}
const toolCalls = choice.message.tool_calls;
if (!Array.isArray(toolCalls)) {
return;
}
for (const toolCall of toolCalls) {
if (typeof toolCall.function !== 'object') {
continue;
}
/** @type {FunctionToolCall} */
const args = toolCall.function;
console.log('Function tool call:', toolCall);
await eventSource.emit(event_types.LLM_FUNCTION_TOOL_CALL, args);
}
}
if ([chat_completion_sources.CLAUDE].includes(oai_settings.chat_completion_source)) {
if (!Array.isArray(data?.content)) {
return;
}
for (const content of data.content) {
if (content.type === 'tool_use') {
/** @type {FunctionToolCall} */
const args = { name: content.name, arguments: JSON.stringify(content.input) };
await eventSource.emit(event_types.LLM_FUNCTION_TOOL_CALL, args);
}
}
}
if ([chat_completion_sources.COHERE].includes(oai_settings.chat_completion_source)) {
if (!Array.isArray(data?.tool_calls)) {
return;
}
for (const toolCall of data.tool_calls) {
/** @type {FunctionToolCall} */
const args = { name: toolCall.name, arguments: JSON.stringify(toolCall.parameters) };
console.log('Function tool call:', toolCall);
await eventSource.emit(event_types.LLM_FUNCTION_TOOL_CALL, args);
}
}
}
export function isFunctionCallingSupported() {
if (main_api !== 'openai') {
return false;
}
if (!oai_settings.function_calling) {
return false;
}
const supportedSources = [
chat_completion_sources.OPENAI,
chat_completion_sources.COHERE,
chat_completion_sources.CUSTOM,
chat_completion_sources.MISTRALAI,
chat_completion_sources.CLAUDE,
chat_completion_sources.OPENROUTER,
chat_completion_sources.GROQ,
];
return supportedSources.includes(oai_settings.chat_completion_source);
}
function getStreamingReply(data) {
if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) {
return data?.delta?.text || '';
} else if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
} else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {
return data?.candidates?.[0]?.content?.parts?.[0]?.text || '';
} else if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || '';
} else {
return data.choices[0]?.delta?.content ?? data.choices[0]?.message?.content ?? data.choices[0]?.text ?? '';
return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
}
}
@ -2299,7 +2069,7 @@ function parseOpenAITextLogprobs(logprobs) {
function handleWindowError(err) {
const text = parseWindowError(err);
toastr.error(text, 'Window.ai returned an error');
toastr.error(text, t`Window.ai returned an error`);
throw err;
}
@ -2441,6 +2211,8 @@ class Message {
content;
/** @type {string} */
name;
/** @type {object} */
tool_call = null;
/**
* @constructor
@ -2465,6 +2237,22 @@ class Message {
}
}
/**
* Reconstruct the message from a tool invocation.
* @param {import('./tool-calling.js').ToolInvocation[]} invocations
*/
setToolCalls(invocations) {
this.tool_calls = invocations.map(i => ({
id: i.id,
type: 'function',
function: {
arguments: i.parameters,
name: i.name,
},
}));
this.tokens = tokenHandler.count({ role: this.role, tool_calls: JSON.stringify(this.tool_calls) });
}
setName(name) {
this.name = name;
this.tokens = tokenHandler.count({ role: this.role, content: this.content, name: this.name });
@ -2601,13 +2389,20 @@ class MessageCollection {
}
/**
* Get chat in the format of {role, name, content}.
* Get chat in the format of {role, name, content, tool_calls}.
* @returns {Array} Array of objects with role, name, and content properties.
*/
getChat() {
return this.collection.reduce((acc, message) => {
const name = message.name;
if (message.content) acc.push({ role: message.role, ...(name && { name }), content: message.content });
if (message.content || message.tool_calls) {
acc.push({
role: message.role,
content: message.content,
...(message.name && { name: message.name }),
...(message.tool_calls && { tool_calls: message.tool_calls }),
...(message.role === 'tool' && { tool_call_id: message.identifier }),
});
}
return acc;
}, []);
}
@ -2812,7 +2607,7 @@ export class ChatCompletion {
this.checkTokenBudget(message, message.identifier);
const index = this.findMessageIndex(identifier);
if (message.content) {
if (message.content || message.tool_calls) {
if ('start' === position) this.messages.collection[index].collection.unshift(message);
else if ('end' === position) this.messages.collection[index].collection.push(message);
else if (typeof position === 'number') this.messages.collection[index].collection.splice(position, 0, message);
@ -2852,6 +2647,15 @@ export class ChatCompletion {
return 0 <= this.tokenBudget - message.getTokens();
}
/**
* Checks if the token budget can afford the tokens of all the specified messages.
* @param {Message[]} messages - The messages to check for affordability.
* @returns {boolean} True if the budget can afford all the messages, false otherwise.
*/
canAffordAll(messages) {
return 0 <= this.tokenBudget - messages.reduce((total, message) => total + message.getTokens(), 0);
}
/**
* Checks if a message with the specified identifier exists in the collection.
*
@ -2881,8 +2685,14 @@ export class ChatCompletion {
for (let item of this.messages.collection) {
if (item instanceof MessageCollection) {
chat.push(...item.getChat());
} else if (item instanceof Message && item.content) {
const message = { role: item.role, content: item.content, ...(item.name ? { name: item.name } : {}) };
} else if (item instanceof Message && (item.content || item.tool_calls)) {
const message = {
role: item.role,
content: item.content,
...(item.name ? { name: item.name } : {}),
...(item.tool_calls ? { tool_calls: item.tool_calls } : {}),
...(item.role === 'tool' ? { tool_call_id: item.identifier } : {}),
};
chat.push(message);
} else {
this.log(`Skipping invalid or empty message in collection: ${JSON.stringify(item)}`);
@ -3046,7 +2856,6 @@ function loadOpenAISettings(data, settings) {
oai_settings.min_p_openai = settings.min_p_openai ?? default_settings.min_p_openai;
oai_settings.repetition_penalty_openai = settings.repetition_penalty_openai ?? default_settings.repetition_penalty_openai;
oai_settings.stream_openai = settings.stream_openai ?? default_settings.stream_openai;
oai_settings.websearch_cohere = settings.websearch_cohere ?? default_settings.websearch_cohere;
oai_settings.openai_max_context = settings.openai_max_context ?? default_settings.openai_max_context;
oai_settings.openai_max_tokens = settings.openai_max_tokens ?? default_settings.openai_max_tokens;
oai_settings.bias_preset_selected = settings.bias_preset_selected ?? default_settings.bias_preset_selected;
@ -3063,7 +2872,6 @@ function loadOpenAISettings(data, settings) {
oai_settings.openrouter_group_models = settings.openrouter_group_models ?? default_settings.openrouter_group_models;
oai_settings.openrouter_sort_models = settings.openrouter_sort_models ?? default_settings.openrouter_sort_models;
oai_settings.openrouter_use_fallback = settings.openrouter_use_fallback ?? default_settings.openrouter_use_fallback;
oai_settings.openrouter_force_instruct = settings.openrouter_force_instruct ?? default_settings.openrouter_force_instruct;
oai_settings.openrouter_allow_fallbacks = settings.openrouter_allow_fallbacks ?? default_settings.openrouter_allow_fallbacks;
oai_settings.ai21_model = settings.ai21_model ?? default_settings.ai21_model;
oai_settings.mistralai_model = settings.mistralai_model ?? default_settings.mistralai_model;
@ -3121,7 +2929,6 @@ function loadOpenAISettings(data, settings) {
if (settings.use_makersuite_sysprompt !== undefined) oai_settings.use_makersuite_sysprompt = !!settings.use_makersuite_sysprompt;
if (settings.use_alt_scale !== undefined) { oai_settings.use_alt_scale = !!settings.use_alt_scale; updateScaleForm(); }
$('#stream_toggle').prop('checked', oai_settings.stream_openai);
$('#websearch_toggle').prop('checked', oai_settings.websearch_cohere);
$('#api_url_scale').val(oai_settings.api_url_scale);
$('#openai_proxy_password').val(oai_settings.proxy_password);
$('#claude_assistant_prefill').val(oai_settings.assistant_prefill);
@ -3170,7 +2977,6 @@ function loadOpenAISettings(data, settings) {
$('#use_makersuite_sysprompt').prop('checked', oai_settings.use_makersuite_sysprompt);
$('#scale-alt').prop('checked', oai_settings.use_alt_scale);
$('#openrouter_use_fallback').prop('checked', oai_settings.openrouter_use_fallback);
$('#openrouter_force_instruct').prop('checked', oai_settings.openrouter_force_instruct);
$('#openrouter_group_models').prop('checked', oai_settings.openrouter_group_models);
$('#openrouter_allow_fallbacks').prop('checked', oai_settings.openrouter_allow_fallbacks);
$('#openrouter_providers_chat').val(oai_settings.openrouter_providers).trigger('change');
@ -3238,6 +3044,10 @@ function loadOpenAISettings(data, settings) {
setNamesBehaviorControls();
setContinuePostfixControls();
if (oai_settings.custom_prompt_post_processing === custom_prompt_post_processing_types.CLAUDE) {
oai_settings.custom_prompt_post_processing = custom_prompt_post_processing_types.MERGE;
}
$('#chat_completion_source').val(oai_settings.chat_completion_source).trigger('change');
$('#oai_max_context_unlocked').prop('checked', oai_settings.max_context_unlocked);
$('#custom_prompt_post_processing').val(oai_settings.custom_prompt_post_processing);
@ -3374,7 +3184,7 @@ async function getStatusOpen() {
}
function showWindowExtensionError() {
toastr.error('Get it here: <a href="https://windowai.io/" target="_blank">windowai.io</a>', 'Extension is not installed', {
toastr.error(t`Get it here:` + ' <a href="https://windowai.io/" target="_blank">windowai.io</a>', t`Extension is not installed`, {
escapeHtml: false,
timeOut: 0,
extendedTimeOut: 0,
@ -3398,7 +3208,6 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
windowai_model: settings.windowai_model,
openrouter_model: settings.openrouter_model,
openrouter_use_fallback: settings.openrouter_use_fallback,
openrouter_force_instruct: settings.openrouter_force_instruct,
openrouter_group_models: settings.openrouter_group_models,
openrouter_sort_models: settings.openrouter_sort_models,
openrouter_providers: settings.openrouter_providers,
@ -3446,7 +3255,6 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
personality_format: settings.personality_format,
group_nudge_prompt: settings.group_nudge_prompt,
stream_openai: settings.stream_openai,
websearch_cohere: settings.websearch_cohere,
prompts: settings.prompts,
prompt_order: settings.prompt_order,
api_url_scale: settings.api_url_scale,
@ -3494,7 +3302,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
if (triggerUi) $('#settings_preset_openai').append(option).trigger('change');
}
} else {
toastr.error('Failed to save preset');
toastr.error(t`Failed to save preset`);
throw new Error('Failed to save preset');
}
}
@ -3573,7 +3381,7 @@ async function createNewLogitBiasPreset() {
}
if (name in oai_settings.bias_presets) {
toastr.error('Preset name should be unique.');
toastr.error(t`Preset name should be unique.`);
return;
}
@ -3617,7 +3425,7 @@ async function onPresetImportFileChange(e) {
try {
presetBody = JSON.parse(importedFile);
} catch (err) {
toastr.error('Invalid file');
toastr.error(t`Invalid file`);
return;
}
@ -3658,7 +3466,7 @@ async function onPresetImportFileChange(e) {
});
if (!savePresetSettings.ok) {
toastr.error('Failed to save preset');
toastr.error(t`Failed to save preset`);
return;
}
@ -3683,7 +3491,7 @@ async function onPresetImportFileChange(e) {
async function onExportPresetClick() {
if (!oai_settings.preset_settings_openai) {
toastr.error('No preset selected');
toastr.error(t`No preset selected`);
return;
}
@ -3724,12 +3532,12 @@ async function onLogitBiasPresetImportFileChange(e) {
e.target.value = '';
if (name in oai_settings.bias_presets) {
toastr.error('Preset name should be unique.');
toastr.error(t`Preset name should be unique.`);
return;
}
if (!Array.isArray(importedFile)) {
toastr.error('Invalid logit bias preset file.');
toastr.error(t`Invalid logit bias preset file.`);
return;
}
@ -3788,16 +3596,16 @@ async function onDeletePresetClick() {
});
if (!response.ok) {
toastr.warning('Preset was not deleted from server');
toastr.warning(t`Preset was not deleted from server`);
} else {
toastr.success('Preset deleted');
toastr.success(t`Preset deleted`);
}
saveSettingsDebounced();
}
async function onLogitBiasPresetDeleteClick() {
const value = await callPopup('Delete the preset?', 'confirm');
const value = await callPopup(t`Delete the preset?`, 'confirm');
if (!value) {
return;
@ -3835,7 +3643,6 @@ function onSettingsPresetChange() {
windowai_model: ['#model_windowai_select', 'windowai_model', false],
openrouter_model: ['#model_openrouter_select', 'openrouter_model', false],
openrouter_use_fallback: ['#openrouter_use_fallback', 'openrouter_use_fallback', true],
openrouter_force_instruct: ['#openrouter_force_instruct', 'openrouter_force_instruct', true],
openrouter_group_models: ['#openrouter_group_models', 'openrouter_group_models', false],
openrouter_sort_models: ['#openrouter_sort_models', 'openrouter_sort_models', false],
openrouter_providers: ['#openrouter_providers_chat', 'openrouter_providers', false],
@ -3871,7 +3678,6 @@ function onSettingsPresetChange() {
personality_format: ['#personality_format_textarea', 'personality_format', false],
group_nudge_prompt: ['#group_nudge_prompt_textarea', 'group_nudge_prompt', false],
stream_openai: ['#stream_toggle', 'stream_openai', true],
websearch_cohere: ['#websearch_toggle', 'websearch_cohere', true],
prompts: ['', 'prompts', false],
prompt_order: ['', 'prompt_order', false],
api_url_scale: ['#api_url_scale', 'api_url_scale', false],
@ -4136,7 +3942,7 @@ async function onModelChange() {
$('#openai_max_context').attr('max', max_32k);
} else if (value === 'text-bison-001') {
$('#openai_max_context').attr('max', max_8k);
// The ultra endpoints are possibly dead:
// The ultra endpoints are possibly dead:
} else if (value.includes('gemini-1.0-ultra') || value === 'gemini-ultra') {
$('#openai_max_context').attr('max', max_32k);
} else {
@ -4313,7 +4119,10 @@ async function onModelChange() {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
}
else if (oai_settings.groq_model.includes('llama-3.1')) {
else if (oai_settings.groq_model.includes('llama-3.2') && oai_settings.groq_model.includes('-preview')) {
$('#openai_max_context').attr('max', max_8k);
}
else if (oai_settings.groq_model.includes('llama-3.2') || oai_settings.groq_model.includes('llama-3.1')) {
$('#openai_max_context').attr('max', max_128k);
}
else if (oai_settings.groq_model.includes('llama3-groq')) {
@ -4774,12 +4583,16 @@ export function isImageInliningSupported() {
'gemini-1.5-flash',
'gemini-1.5-flash-latest',
'gemini-1.5-flash-001',
'gemini-1.5-flash-002',
'gemini-1.5-flash-exp-0827',
'gemini-1.5-flash-8b',
'gemini-1.5-flash-8b-exp-0827',
'gemini-1.5-flash-8b-exp-0924',
'gemini-1.0-pro-vision-latest',
'gemini-1.5-pro',
'gemini-1.5-pro-latest',
'gemini-1.5-pro-001',
'gemini-1.5-pro-002',
'gemini-1.5-pro-exp-0801',
'gemini-1.5-pro-exp-0827',
'gemini-pro-vision',
@ -4802,7 +4615,7 @@ export function isImageInliningSupported() {
case chat_completion_sources.CLAUDE:
return visionSupportedModels.some(model => oai_settings.claude_model.includes(model));
case chat_completion_sources.OPENROUTER:
return !oai_settings.openrouter_force_instruct;
return true;
case chat_completion_sources.CUSTOM:
return true;
case chat_completion_sources.ZEROONEAI:
@ -4928,7 +4741,7 @@ function runProxyCallback(_, value) {
const result = fuse.search(value);
if (result.length === 0) {
toastr.warning(`Proxy preset "${value}" not found`);
toastr.warning(t`Proxy preset '${value}' not found`);
return '';
}
@ -5028,11 +4841,6 @@ export function initOpenAI() {
saveSettingsDebounced();
});
$('#websearch_toggle').on('change', function () {
oai_settings.websearch_cohere = !!$('#websearch_toggle').prop('checked');
saveSettingsDebounced();
});
$('#wrap_in_quotes').on('change', function () {
oai_settings.wrap_in_quotes = !!$('#wrap_in_quotes').prop('checked');
saveSettingsDebounced();
@ -5102,7 +4910,7 @@ export function initOpenAI() {
$('#update_oai_preset').on('click', async function () {
const name = oai_settings.preset_settings_openai;
await saveOpenAIPreset(name, oai_settings);
toastr.success('Preset updated');
toastr.success(t`Preset updated`);
});
$('#impersonation_prompt_restore').on('click', function () {
@ -5224,11 +5032,6 @@ export function initOpenAI() {
saveSettingsDebounced();
});
$('#openrouter_force_instruct').on('input', function () {
oai_settings.openrouter_force_instruct = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#openrouter_group_models').on('input', function () {
oai_settings.openrouter_group_models = !!$(this).prop('checked');
saveSettingsDebounced();

View File

@ -1,3 +1,4 @@
import dialogPolyfill from '../lib/dialog-polyfill.esm.js';
import { shouldSendOnEnter } from './RossAscends-mods.js';
import { power_user } from './power-user.js';
import { removeFromArray, runAfterAnimation, uuidv4 } from './utils.js';
@ -178,6 +179,10 @@ export class Popup {
const template = document.querySelector('#popup_template');
// @ts-ignore
this.dlg = template.content.cloneNode(true).querySelector('.popup');
if (!this.dlg.showModal) {
this.dlg.classList.add('poly_dialog');
dialogPolyfill.registerDialog(this.dlg);
}
this.body = this.dlg.querySelector('.popup-body');
this.content = this.dlg.querySelector('.popup-content');
this.mainInput = this.dlg.querySelector('.popup-input');

View File

@ -290,6 +290,7 @@ let power_user = {
restore_user_input: true,
reduced_motion: false,
compact_input_area: true,
show_swipe_num_all_messages: false,
auto_connect: false,
auto_load_chat: false,
forbid_external_media: true,
@ -469,6 +470,11 @@ function switchCompactInputArea() {
$('#compact_input_area').prop('checked', power_user.compact_input_area);
}
export function switchSwipeNumAllMessages() {
$('#show_swipe_num_all_messages').prop('checked', power_user.show_swipe_num_all_messages);
$('.mes:not(.last_mes) .swipes-counter').css('opacity', '').toggle(power_user.show_swipe_num_all_messages);
}
var originalSliderValues = [];
async function switchLabMode() {
@ -1283,6 +1289,13 @@ function applyTheme(name) {
switchCompactInputArea();
},
},
{
key: 'show_swipe_num_all_messages',
action: () => {
$('#show_swipe_num_all_messages').prop('checked', power_user.show_swipe_num_all_messages);
switchSwipeNumAllMessages();
},
},
];
for (const { key, selector, type, action } of themeProperties) {
@ -1352,6 +1365,7 @@ function applyPowerUserSettings() {
switchHideChatAvatars();
switchTokenCount();
switchMessageActions();
switchSwipeNumAllMessages();
}
function getExampleMessagesBehavior() {
@ -2296,6 +2310,7 @@ function getThemeObject(name) {
zoomed_avatar_magnification: power_user.zoomed_avatar_magnification,
reduced_motion: power_user.reduced_motion,
compact_input_area: power_user.compact_input_area,
show_swipe_num_all_messages: power_user.show_swipe_num_all_messages,
};
}
@ -3755,6 +3770,12 @@ $(document).ready(() => {
saveSettingsDebounced();
});
$('#show_swipe_num_all_messages').on('input', function () {
power_user.show_swipe_num_all_messages = !!$(this).prop('checked');
switchSwipeNumAllMessages();
saveSettingsDebounced();
});
$('#auto-connect-checkbox').on('input', function () {
power_user.auto_connect = !!$(this).prop('checked');
saveSettingsDebounced();

View File

@ -34,6 +34,7 @@ import {
textgenerationwebui_settings as textgen_settings,
} from './textgen-settings.js';
import { download, parseJsonFile, waitUntilCondition } from './utils.js';
import { t } from './i18n.js';
const presetManagers = {};
@ -194,32 +195,32 @@ class PresetManager {
*/
static async performMasterImport(data, fileName) {
if (!data || typeof data !== 'object') {
toastr.error('Invalid data provided for master import');
toastr.error(t`Invalid data provided for master import`);
return;
}
// Check for legacy file imports
// 1. Instruct Template
if (this.isPossiblyInstructData(data)) {
toastr.info('Importing instruct template...', 'Instruct template detected');
toastr.info(t`Importing instruct template...`, t`Instruct template detected`);
return await getPresetManager('instruct').savePreset(data.name, data);
}
// 2. Context Template
if (this.isPossiblyContextData(data)) {
toastr.info('Importing as context template...', 'Context template detected');
toastr.info(t`Importing as context template...`, t`Context template detected`);
return await getPresetManager('context').savePreset(data.name, data);
}
// 3. System Prompt
if (this.isPossiblySystemPromptData(data)) {
toastr.info('Importing as system prompt...', 'System prompt detected');
toastr.info(t`Importing as system prompt...`, t`System prompt detected`);
return await getPresetManager('sysprompt').savePreset(data.name, data);
}
// 4. Text Completion settings
if (this.isPossiblyTextCompletionData(data)) {
toastr.info('Importing as settings preset...', 'Text Completion settings detected');
toastr.info(t`Importing as settings preset...`, t`Text Completion settings detected`);
return await getPresetManager('textgenerationwebui').savePreset(fileName, data);
}
@ -231,7 +232,7 @@ class PresetManager {
}
if (validSections.length === 0) {
toastr.error('No valid sections found in imported data');
toastr.error(t`No valid sections found in imported data`);
return;
}
@ -242,8 +243,8 @@ class PresetManager {
const html = $(await renderTemplateAsync('masterImport', { sections: sectionNames }));
const popup = new Popup(html, POPUP_TYPE.CONFIRM, '', {
okButton: 'Import',
cancelButton: 'Cancel',
okButton: t`Import`,
cancelButton: t`Cancel`,
});
const result = await popup.show();
@ -257,7 +258,7 @@ class PresetManager {
const confirmedSections = html.find('input:checked').map((_, el) => el instanceof HTMLInputElement && el.value).get();
if (confirmedSections.length === 0) {
toastr.info('No sections selected for import');
toastr.info(t`No sections selected for import`);
return;
}
@ -270,7 +271,7 @@ class PresetManager {
}
}
toastr.success(`Imported ${importedSections.length} settings: ${importedSections.join(', ')}`);
toastr.success(t`Imported ${importedSections.length} settings: ${importedSections.join(', ')}`);
}
/**
@ -285,8 +286,8 @@ class PresetManager {
const html = $(await renderTemplateAsync('masterExport', { sections: sectionNames }));
const popup = new Popup(html, POPUP_TYPE.CONFIRM, '', {
okButton: 'Export',
cancelButton: 'Cancel',
okButton: t`Export`,
cancelButton: t`Cancel`,
});
const result = await popup.show();
@ -300,7 +301,7 @@ class PresetManager {
const data = {};
if (confirmedSections.length === 0) {
toastr.info('No sections selected for export');
toastr.info(t`No sections selected for export`);
return;
}
@ -328,7 +329,7 @@ class PresetManager {
* @returns {any} Preset value
*/
findPreset(name) {
return $(this.select).find('option').filter(function() {
return $(this.select).find('option').filter(function () {
return $(this).text() === name;
}).val();
}
@ -354,7 +355,7 @@ class PresetManager {
* @param {string} value Preset option value
*/
selectPreset(value) {
const option = $(this.select).filter(function() {
const option = $(this.select).filter(function () {
return $(this).val() === value;
});
option.prop('selected', true);
@ -366,21 +367,21 @@ class PresetManager {
console.log(selected);
if (selected.val() == 'gui') {
toastr.info('Cannot update GUI preset');
toastr.info(t`Cannot update GUI preset`);
return;
}
const name = selected.text();
await this.savePreset(name);
const successToast = !this.isAdvancedFormatting() ? 'Preset updated' : 'Template updated';
const successToast = !this.isAdvancedFormatting() ? t`Preset updated` : t`Template updated`;
toastr.success(successToast);
}
async savePresetAs() {
const inputValue = this.getSelectedPresetName();
const popupText = !this.isAdvancedFormatting() ? '<h4>Hint: Use a character/group name to bind preset to a specific chat.</h4>' : '';
const headerText = !this.isAdvancedFormatting() ? 'Preset name:' : 'Template name:';
const popupText = !this.isAdvancedFormatting() ? '<h4>' + t`Hint: Use a character/group name to bind preset to a specific chat.` + '</h4>' : '';
const headerText = !this.isAdvancedFormatting() ? t`Preset name:` : t`Template name:`;
const name = await Popup.show.input(headerText, popupText, inputValue);
if (!name) {
console.log('Preset name not provided');
@ -389,7 +390,7 @@ class PresetManager {
await this.savePreset(name);
const successToast = !this.isAdvancedFormatting() ? 'Preset saved' : 'Template saved';
const successToast = !this.isAdvancedFormatting() ? t`Preset saved` : t`Template saved`;
toastr.success(successToast);
}
@ -411,7 +412,7 @@ class PresetManager {
});
if (!response.ok) {
toastr.error('Check the server connection and reload the page to prevent data loss.', 'Preset could not be saved');
toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Preset could not be saved`);
console.error('Preset could not be saved', response);
throw new Error('Preset could not be saved');
}
@ -422,6 +423,19 @@ class PresetManager {
this.updateList(name, preset);
}
async renamePreset(newName) {
const oldName = this.getSelectedPresetName();
try {
await this.savePreset(newName);
await this.deletePreset(oldName);
} catch (error) {
toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Preset could not be renamed`);
console.error('Preset could not be renamed', error);
throw new Error('Preset could not be renamed');
}
}
getPresetList() {
let presets = [];
let preset_names = {};
@ -585,13 +599,14 @@ class PresetManager {
return settings;
}
async deleteCurrentPreset() {
// pass no arguments to delete current preset
async deletePreset(name) {
const { preset_names, presets } = this.getPresetList();
const value = this.getSelectedPreset();
const nameToDelete = this.getSelectedPresetName();
const value = name ? (this.isKeyedApi() ? this.findPreset(name) : name) : this.getSelectedPreset();
const nameToDelete = name || this.getSelectedPresetName();
if (value == 'gui') {
toastr.info('Cannot delete GUI preset');
toastr.info(t`Cannot delete GUI preset`);
return;
}
@ -605,7 +620,10 @@ class PresetManager {
delete preset_names[nameToDelete];
}
if (Object.keys(preset_names).length) {
// switch in UI only when deleting currently selected preset
const switchPresets = !name || this.getSelectedPresetName() == name;
if (Object.keys(preset_names).length && switchPresets) {
const nextPresetName = Object.keys(preset_names)[0];
const newValue = preset_names[nextPresetName];
$(this.select).find(`option[value="${newValue}"]`).attr('selected', 'true');
@ -629,7 +647,7 @@ class PresetManager {
});
if (!response.ok) {
const errorToast = !this.isAdvancedFormatting() ? 'Failed to restore default preset' : 'Failed to restore default template';
const errorToast = !this.isAdvancedFormatting() ? t`Failed to restore default preset` : t`Failed to restore default template`;
toastr.error(errorToast);
return;
}
@ -721,7 +739,8 @@ async function waitForConnection() {
export async function initPresetManager() {
eventSource.on(event_types.CHAT_CHANGED, autoSelectPreset);
registerPresetManagers();
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'preset',
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'preset',
callback: presetCommandCallback,
returns: 'current preset',
unnamedArgumentList: [
@ -774,6 +793,29 @@ export async function initPresetManager() {
await presetManager.savePresetAs();
});
$(document).on('click', '[data-preset-manager-rename]', async function () {
const apiId = $(this).data('preset-manager-rename');
const presetManager = getPresetManager(apiId);
if (!presetManager) {
console.warn(`Preset Manager not found for API: ${apiId}`);
return;
}
const popupHeader = !presetManager.isAdvancedFormatting() ? t`Rename preset` : t`Rename template`;
const oldName = presetManager.getSelectedPresetName();
const newName = await Popup.show.input(popupHeader, t`Enter a new name:`, oldName);
if (!newName || oldName === newName) {
console.debug(!presetManager.isAdvancedFormatting() ? 'Preset rename cancelled' : 'Template rename cancelled');
return;
}
await presetManager.renamePreset(newName);
const successToast = !presetManager.isAdvancedFormatting() ? t`Preset renamed` : t`Template renamed`;
toastr.success(successToast);
});
$(document).on('click', '[data-preset-manager-export]', async function () {
const apiId = $(this).data('preset-manager-export');
const presetManager = getPresetManager(apiId);
@ -816,7 +858,7 @@ export async function initPresetManager() {
data['name'] = name;
await presetManager.savePreset(name, data);
const successToast = !presetManager.isAdvancedFormatting() ? 'Preset imported' : 'Template imported';
const successToast = !presetManager.isAdvancedFormatting() ? t`Preset imported` : t`Template imported`;
toastr.success(successToast);
e.target.value = null;
});
@ -830,19 +872,19 @@ export async function initPresetManager() {
return;
}
const headerText = !presetManager.isAdvancedFormatting() ? 'Delete this preset?' : 'Delete this template?';
const confirm = await Popup.show.confirm(headerText, 'This action is irreversible and your current settings will be overwritten.');
const headerText = !presetManager.isAdvancedFormatting() ? t`Delete this preset?` : t`Delete this template?`;
const confirm = await Popup.show.confirm(headerText, t`This action is irreversible and your current settings will be overwritten.`);
if (!confirm) {
return;
}
const result = await presetManager.deleteCurrentPreset();
const result = await presetManager.deletePreset();
if (result) {
const successToast = !presetManager.isAdvancedFormatting() ? 'Preset deleted' : 'Template deleted';
const successToast = !presetManager.isAdvancedFormatting() ? t`Preset deleted` : t`Template deleted`;
toastr.success(successToast);
} else {
const warningToast = !presetManager.isAdvancedFormatting() ? 'Preset was not deleted from server' : 'Template was not deleted from server';
const warningToast = !presetManager.isAdvancedFormatting() ? t`Preset was not deleted from server` : t`Template was not deleted from server`;
toastr.warning(warningToast);
}
@ -862,7 +904,7 @@ export async function initPresetManager() {
const data = await presetManager.getDefaultPreset(name);
if (name == 'gui') {
toastr.info('Cannot restore GUI preset');
toastr.info(t`Cannot restore GUI preset`);
return;
}
@ -872,37 +914,37 @@ export async function initPresetManager() {
if (data.isDefault) {
if (Object.keys(data.preset).length === 0) {
const errorToast = !presetManager.isAdvancedFormatting() ? 'Default preset cannot be restored' : 'Default template cannot be restored';
const errorToast = !presetManager.isAdvancedFormatting() ? t`Default preset cannot be restored` : t`Default template cannot be restored`;
toastr.error(errorToast);
return;
}
const confirmText = !presetManager.isAdvancedFormatting()
? 'Resetting a <b>default preset</b> will restore the default settings.'
: 'Resetting a <b>default template</b> will restore the default settings.';
const confirm = await Popup.show.confirm('Are you sure?', confirmText);
? t`Resetting a <b>default preset</b> will restore the default settings.`
: t`Resetting a <b>default template</b> will restore the default settings.`;
const confirm = await Popup.show.confirm(t`Are you sure?`, confirmText);
if (!confirm) {
return;
}
await presetManager.deleteCurrentPreset();
await presetManager.deletePreset();
await presetManager.savePreset(name, data.preset);
const option = presetManager.findPreset(name);
presetManager.selectPreset(option);
const successToast = !presetManager.isAdvancedFormatting() ? 'Default preset restored' : 'Default template restored';
const successToast = !presetManager.isAdvancedFormatting() ? t`Default preset restored` : t`Default template restored`;
toastr.success(successToast);
} else {
const confirmText = !presetManager.isAdvancedFormatting()
? 'Resetting a <b>custom preset</b> will restore to the last saved state.'
: 'Resetting a <b>custom template</b> will restore to the last saved state.';
const confirm = await Popup.show.confirm('Are you sure?', confirmText);
? t`Resetting a <b>custom preset</b> will restore to the last saved state.`
: t`Resetting a <b>custom template</b> will restore to the last saved state.`;
const confirm = await Popup.show.confirm(t`Are you sure?`, confirmText);
if (!confirm) {
return;
}
const option = presetManager.findPreset(name);
presetManager.selectPreset(option);
const successToast = !presetManager.isAdvancedFormatting() ? 'Preset restored' : 'Template restored';
const successToast = !presetManager.isAdvancedFormatting() ? t`Preset restored` : t`Template restored`;
toastr.success(successToast);
}
});

View File

@ -13,6 +13,7 @@ import { isValidUrl } from './utils.js';
* @property {string} description
* @property {string} iconClass
* @property {boolean} iconAvailable
* @property {() => Promise<void>} [init=null]
* @property {() => Promise<boolean>} isAvailable
* @property {() => Promise<File[]>} scrape
*/
@ -36,12 +37,16 @@ export class ScraperManager {
* Register a scraper to be used by the Data Bank.
* @param {Scraper} scraper Instance of a scraper to register
*/
static registerDataBankScraper(scraper) {
static async registerDataBankScraper(scraper) {
if (ScraperManager.#scrapers.some(s => s.id === scraper.id)) {
console.warn(`Scraper with ID ${scraper.id} already registered`);
return;
}
if (scraper.init) {
await scraper.init();
}
ScraperManager.#scrapers.push(scraper);
}
@ -462,7 +467,9 @@ class YouTubeScraper {
this.description = 'Download a transcript from a YouTube video.';
this.iconClass = 'fa-brands fa-youtube';
this.iconAvailable = true;
}
async init() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'yt-script',
callback: async (args, url) => {
@ -564,9 +571,11 @@ class YouTubeScraper {
}
}
ScraperManager.registerDataBankScraper(new FileScraper());
ScraperManager.registerDataBankScraper(new Notepad());
ScraperManager.registerDataBankScraper(new WebScraper());
ScraperManager.registerDataBankScraper(new MediaWikiScraper());
ScraperManager.registerDataBankScraper(new FandomScraper());
ScraperManager.registerDataBankScraper(new YouTubeScraper());
export async function initScrapers() {
await ScraperManager.registerDataBankScraper(new FileScraper());
await ScraperManager.registerDataBankScraper(new Notepad());
await ScraperManager.registerDataBankScraper(new WebScraper());
await ScraperManager.registerDataBankScraper(new MediaWikiScraper());
await ScraperManager.registerDataBankScraper(new FandomScraper());
await ScraperManager.registerDataBankScraper(new YouTubeScraper());
}

View File

@ -55,7 +55,7 @@ import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockStat
import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
import { debounce, delay, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
import { registerVariableCommands, resolveVariable } from './variables.js';
import { background_settings } from './backgrounds.js';
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@ -68,9 +68,9 @@ import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashComma
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js';
import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
export {
executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
};
@ -173,27 +173,112 @@ export function initDefaultSlashCommands() {
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'char-find',
aliases: ['findchar'],
callback: (args, name) => {
if (typeof name !== 'string') throw new Error('name must be a string');
if (args.preferCurrent instanceof SlashCommandClosure || Array.isArray(args.preferCurrent)) throw new Error('preferCurrent cannot be a closure or array');
if (args.quiet instanceof SlashCommandClosure || Array.isArray(args.quiet)) throw new Error('quiet cannot be a closure or array');
const char = findChar({ name: name, filteredByTags: validateArrayArgString(args.tag, 'tag'), preferCurrentChar: !isFalseBoolean(args.preferCurrent), quiet: isTrueBoolean(args.quiet) });
return char?.avatar ?? '';
},
returns: 'the avatar key (unique identifier) of the character',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'tag',
description: 'Supply one or more tags to filter down to the correct character for the provided name, if multiple characters have the same name.',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: commonEnumProviders.tags('assigned'),
acceptsMultiple: true,
}),
SlashCommandNamedArgument.fromProps({
name: 'preferCurrent',
description: 'Prefer current character or characters in a group, if multiple characters match',
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'true',
}),
SlashCommandNamedArgument.fromProps({
name: 'quiet',
description: 'Do not show warning if multiple charactrers are found',
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'false',
enumProvider: commonEnumProviders.boolean('trueFalse'),
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: commonEnumProviders.characters('character'),
forceEnum: false,
}),
],
helpString: `
<div>
Searches for a character and returns its avatar key.
</div>
<div>
This can be used to choose the correct character for something like <code>/sendas</code> or other commands in need of a character name
if you have multiple characters with the same name.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/char-find name="Chloe"</code></pre>
Returns the avatar key for "Chloe".
</li>
<li>
<pre><code>/search name="Chloe" tag="friend"</code></pre>
Returns the avatar key for the character "Chloe" that is tagged with "friend".
This is useful if you for example have multiple characters named "Chloe", and the others are "foe", "goddess", or anything else,
so you can actually select the character you are looking for.
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'sendas',
callback: sendMessageAs,
returns: 'Optionally the text of the sent message, if specified in the "return" argument',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'name',
description: 'Character name',
description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: commonEnumProviders.characters('character'),
forceEnum: false,
}),
new SlashCommandNamedArgument(
'compact', 'Use compact layout', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
),
SlashCommandNamedArgument.fromProps({
name: 'avatar',
description: 'Character avatar override (Can be either avatar key or just the character name to pull the avatar from)',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: commonEnumProviders.characters('character'),
}),
SlashCommandNamedArgument.fromProps({
name: 'compact',
description: 'Use compact layout',
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'false',
}),
SlashCommandNamedArgument.fromProps({
name: 'at',
description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values are accepted and will work similarly to how \'depth\' usually works. For example, -1 will insert the message right before the last message in chat.',
typeList: [ARGUMENT_TYPE.NUMBER],
enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
}),
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'The way how you want the return value to be provided',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'none',
enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
forceEnum: true,
}),
],
unnamedArgumentList: [
new SlashCommandArgument(
@ -211,6 +296,10 @@ export function initDefaultSlashCommands() {
<pre><code>/sendas name="Chloe" Hello, guys!</code></pre>
will send "Hello, guys!" from "Chloe".
</li>
<li>
<pre><code>/sendas name="Chloe" avatar="BigBadBoss" Hehehe, I am the big bad evil, fear me.</code></pre>
will send a message as the character "Chloe", but utilizing the avatar from a character named "BigBadBoss".
</li>
</ul>
</div>
<div>
@ -222,6 +311,7 @@ export function initDefaultSlashCommands() {
name: 'sys',
callback: sendNarratorMessage,
aliases: ['nar'],
returns: 'Optionally the text of the sent message, if specified in the "return" argument',
namedArgumentList: [
new SlashCommandNamedArgument(
'compact',
@ -237,6 +327,14 @@ export function initDefaultSlashCommands() {
typeList: [ARGUMENT_TYPE.NUMBER],
enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
}),
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'The way how you want the return value to be provided',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'none',
enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
forceEnum: true,
}),
],
unnamedArgumentList: [
new SlashCommandArgument(
@ -276,6 +374,7 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'comment',
callback: sendCommentMessage,
returns: 'Optionally the text of the sent message, if specified in the "return" argument',
namedArgumentList: [
new SlashCommandNamedArgument(
'compact',
@ -291,6 +390,14 @@ export function initDefaultSlashCommands() {
typeList: [ARGUMENT_TYPE.NUMBER],
enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
}),
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'The way how you want the return value to be provided',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'none',
enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
forceEnum: true,
}),
],
unnamedArgumentList: [
new SlashCommandArgument(
@ -381,12 +488,14 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'go',
callback: goToCharacterCallback,
returns: 'The character/group name',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'name',
description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: commonEnumProviders.characters('all'),
forceEnum: true,
}),
],
helpString: 'Opens up a chat with the character or group by its name',
@ -428,15 +537,23 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'ask',
callback: askCharacter,
returns: 'the generated text',
returns: 'Optionally the text of the sent message, if specified in the "return" argument',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'name',
description: 'character name',
description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: commonEnumProviders.characters('character'),
}),
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'The way how you want the return value to be provided',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'pipe',
enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
forceEnum: true,
}),
],
unnamedArgumentList: [
new SlashCommandArgument(
@ -451,7 +568,7 @@ export function initDefaultSlashCommands() {
namedArgumentList: [],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'name',
description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: commonEnumProviders.characters('character'),
@ -475,6 +592,7 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'send',
callback: sendUserMessageCallback,
returns: 'Optionally the text of the sent message, if specified in the "return" argument',
namedArgumentList: [
new SlashCommandNamedArgument(
'compact',
@ -497,6 +615,14 @@ export function initDefaultSlashCommands() {
defaultValue: '{{user}}',
enumProvider: commonEnumProviders.personas,
}),
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'The way how you want the return value to be provided',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'none',
enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
forceEnum: true,
}),
],
unnamedArgumentList: [
new SlashCommandArgument(
@ -618,7 +744,7 @@ export function initDefaultSlashCommands() {
aliases: ['addmember', 'memberadd'],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'character name',
description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: () => selected_group ? commonEnumProviders.characters('character')() : [],
@ -856,7 +982,7 @@ export function initDefaultSlashCommands() {
),
SlashCommandNamedArgument.fromProps({
name: 'name',
description: 'in-prompt name for instruct mode',
description: 'in-prompt character name for instruct mode (or unique character identifier (avatar key), which will be used as name)',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'System',
enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)],
@ -1487,12 +1613,21 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'listinjects',
callback: listInjectsCallback,
helpString: 'Lists all script injections for the current chat. Displays injects in a popup by default. Use the <code>format</code> argument to change the output format.',
returns: 'JSON object of script injections',
helpString: 'Lists all script injections for the current chat. Displays injects in a popup by default. Use the <code>return</code> argument to change the return type.',
returns: 'Optionalls the JSON object of script injections',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'The way how you want the return value to be provided',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'popup-html',
enumList: slashCommandReturnHelper.enumList({ allowPipe: false, allowObject: true, allowChat: true, allowPopup: true, allowTextVersion: false }),
forceEnum: true,
}),
// TODO remove some day
SlashCommandNamedArgument.fromProps({
name: 'format',
description: 'output format',
description: '!!! DEPRECATED - use "return" instead !!! output format',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
forceEnum: true,
@ -1761,37 +1896,43 @@ function injectCallback(args, value) {
}
async function listInjectsCallback(args) {
const type = String(args?.format).toLowerCase().trim();
if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) {
type !== 'none' && toastr.info('No script injections for the current chat');
return JSON.stringify({});
/** @type {import('./slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
let returnType = args.return;
// Old legacy return type handling
if (args.format) {
toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning');
const type = String(args?.format).toLowerCase().trim();
if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) {
type !== 'none' && toastr.info('No script injections for the current chat');
}
switch (type) {
case 'none':
returnType = 'none';
break;
case 'chat':
returnType = 'chat-html';
break;
case 'popup':
default:
returnType = 'popup-html';
break;
}
}
const injects = Object.entries(chat_metadata.script_injects)
.map(([id, inject]) => {
const position = Object.entries(extension_prompt_types);
const positionName = position.find(([_, value]) => value === inject.position)?.[0] ?? 'unknown';
return `* **${id}**: <code>${inject.value}</code> (${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}, role: ${inject.role ?? extension_prompt_roles.SYSTEM})`;
})
.join('\n');
// Now the actual new return type handling
const buildTextValue = (injects) => {
const injectsStr = Object.entries(injects)
.map(([id, inject]) => {
const position = Object.entries(extension_prompt_types);
const positionName = position.find(([_, value]) => value === inject.position)?.[0] ?? 'unknown';
return `* **${id}**: <code>${inject.value}</code> (${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}, role: ${inject.role ?? extension_prompt_roles.SYSTEM})`;
})
.join('\n');
return `### Script injections:\n${injectsStr || 'No script injections for the current chat'}`;
};
const converter = new showdown.Converter();
const messageText = `### Script injections:\n${injects}`;
const htmlMessage = DOMPurify.sanitize(converter.makeHtml(messageText));
switch (type) {
case 'none':
break;
case 'chat':
sendSystemMessage(system_message_types.GENERIC, htmlMessage);
break;
case 'popup':
default:
await callGenericPopup(htmlMessage, POPUP_TYPE.TEXT);
break;
}
return JSON.stringify(chat_metadata.script_injects);
return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', chat_metadata.script_injects ?? {}, { objectToStringFunc: buildTextValue });
}
/**
@ -2293,7 +2434,8 @@ async function generateCallback(args, value) {
setEphemeralStopStrings(resolveVariable(args?.stop));
const name = args?.name;
const result = await generateQuietPrompt(value, quietToLoud, false, '', name, length);
const char = findChar({ name: name });
const result = await generateQuietPrompt(value, quietToLoud, false, '', char?.name ?? name, length);
return result;
} catch (err) {
console.error('Error on /gen generation', err);
@ -2477,30 +2619,26 @@ async function askCharacter(args, text) {
// Not supported in group chats
// TODO: Maybe support group chats?
if (selected_group) {
toastr.error('Cannot run /ask command in a group chat!');
toastr.warning('Cannot run /ask command in a group chat!');
return '';
}
let name = '';
if (args?.name) {
name = args.name.trim();
if (!name) {
toastr.warning('You must specify a name of the character to ask.');
return '';
}
if (!args.name) {
toastr.warning('You must specify a name of the character to ask.');
return '';
}
const prevChId = this_chid;
// Find the character
const chId = characters.findIndex((e) => e.name === name || e.avatar === name);
if (!characters[chId] || chId === -1) {
const character = findChar({ name: args?.name });
if (!character) {
toastr.error('Character not found.');
return '';
}
const chId = getCharIndex(character);
if (text) {
const mesText = getRegexedString(text.trim(), regex_placement.SLASH_COMMAND);
// Sending a message implicitly saves the chat, so this needs to be done before changing the character
@ -2511,32 +2649,27 @@ async function askCharacter(args, text) {
// Override character and send a user message
setCharacterId(String(chId));
const character = characters[chId];
let force_avatar, original_avatar;
const { name, force_avatar, original_avatar } = getNameAndAvatarForMessage(character, args?.name);
if (character && character.avatar !== 'none') {
force_avatar = getThumbnailUrl('avatar', character.avatar);
original_avatar = character.avatar;
}
else {
force_avatar = default_avatar;
original_avatar = default_avatar;
}
setCharacterName(character.name);
setCharacterName(name);
const restoreCharacter = () => {
if (String(this_chid) !== String(chId)) {
return;
}
setCharacterId(prevChId);
setCharacterName(characters[prevChId].name);
if (prevChId !== undefined) {
setCharacterId(prevChId);
setCharacterName(characters[prevChId].name);
} else {
setCharacterId(undefined);
setCharacterName(neutralCharacterName);
}
// Only force the new avatar if the character name is the same
// This skips if an error was fired
const lastMessage = chat[chat.length - 1];
if (lastMessage && lastMessage?.name === character.name) {
if (lastMessage && lastMessage?.name === name) {
lastMessage.force_avatar = force_avatar;
lastMessage.original_avatar = original_avatar;
}
@ -2547,7 +2680,7 @@ async function askCharacter(args, text) {
// Run generate and restore previous character
try {
eventSource.once(event_types.MESSAGE_RECEIVED, restoreCharacter);
toastr.info(`Asking ${character.name} something...`);
toastr.info(`Asking ${name} something...`);
askResult = await Generate('ask_command');
} catch (error) {
restoreCharacter();
@ -2560,7 +2693,9 @@ async function askCharacter(args, text) {
}
}
return askResult;
const message = askResult ? chat[chat.length - 1] : null;
return await slashCommandReturnHelper.doReturn(args.return ?? 'pipe', message, { objectToStringFunc: x => x.mes });
}
async function hideMessageCallback(_, arg) {
@ -2741,26 +2876,23 @@ async function removeGroupMemberCallback(_, arg) {
return '';
}
async function addGroupMemberCallback(_, arg) {
async function addGroupMemberCallback(_, name) {
if (!selected_group) {
toastr.warning('Cannot run /memberadd command outside of a group chat.');
return '';
}
if (!arg) {
if (!name) {
console.warn('WARN: No argument provided for /memberadd command');
return '';
}
arg = arg.trim();
const chid = findCharacterIndex(arg);
if (chid === -1) {
console.warn(`WARN: No character found for argument ${arg}`);
const character = findChar({ name: name, preferCurrentChar: false });
if (!character) {
console.warn(`WARN: No character found for argument ${name}`);
return '';
}
const character = characters[chid];
const group = groups.find(x => x.id === selected_group);
if (!group || !Array.isArray(group.members)) {
@ -2829,7 +2961,7 @@ function findPersonaByName(name) {
}
for (const persona of Object.entries(power_user.personas)) {
if (persona[1].toLowerCase() === name.toLowerCase()) {
if (equalsIgnoreCaseAndAccents(persona[1], name)) {
return persona[0];
}
}
@ -2838,7 +2970,7 @@ function findPersonaByName(name) {
async function sendUserMessageCallback(args, text) {
if (!text) {
console.warn('WARN: No text provided for /send command');
toastr.warning('You must specify text to send');
return;
}
@ -2854,16 +2986,17 @@ async function sendUserMessageCallback(args, text) {
insertAt = chat.length + insertAt;
}
let message;
if ('name' in args) {
const name = args.name || '';
const avatar = findPersonaByName(name) || user_avatar;
await sendMessageAsUser(text, bias, insertAt, compact, name, avatar);
message = await sendMessageAsUser(text, bias, insertAt, compact, name, avatar);
}
else {
await sendMessageAsUser(text, bias, insertAt, compact);
message = await sendMessageAsUser(text, bias, insertAt, compact);
}
return '';
return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
}
async function deleteMessagesByNameCallback(_, name) {
@ -2872,7 +3005,9 @@ async function deleteMessagesByNameCallback(_, name) {
return;
}
name = name.trim();
// Search for a matching character to get the real name, or take the name provided
const character = findChar({ name: name });
name = character?.name || name;
const messagesToDelete = [];
chat.forEach((value) => {
@ -2901,60 +3036,34 @@ async function deleteMessagesByNameCallback(_, name) {
return '';
}
function findCharacterIndex(name) {
const matchTypes = [
(a, b) => a === b,
(a, b) => a.startsWith(b),
(a, b) => a.includes(b),
];
const exactAvatarMatch = characters.findIndex(x => x.avatar === name);
if (exactAvatarMatch !== -1) {
return exactAvatarMatch;
}
for (const matchType of matchTypes) {
const index = characters.findIndex(x => matchType(x.name.toLowerCase(), name.toLowerCase()));
if (index !== -1) {
return index;
}
}
return -1;
}
async function goToCharacterCallback(_, name) {
if (!name) {
console.warn('WARN: No character name provided for /go command');
return;
}
name = name.trim();
const characterIndex = findCharacterIndex(name);
if (characterIndex !== -1) {
await openChat(new String(characterIndex));
setActiveCharacter(characters[characterIndex]?.avatar);
const character = findChar({ name: name });
if (character) {
const chid = getCharIndex(character);
await openChat(new String(chid));
setActiveCharacter(character.avatar);
setActiveGroup(null);
return characters[characterIndex]?.name;
} else {
const group = groups.find(it => it.name.toLowerCase() == name.toLowerCase());
if (group) {
await openGroupById(group.id);
setActiveCharacter(null);
setActiveGroup(group.id);
return group.name;
} else {
console.warn(`No matches found for name "${name}"`);
return '';
}
return character.name;
}
const group = groups.find(it => equalsIgnoreCaseAndAccents(it.name, name));
if (group) {
await openGroupById(group.id);
setActiveCharacter(null);
setActiveGroup(group.id);
return group.name;
}
console.warn(`No matches found for name "${name}"`);
return '';
}
async function openChat(id) {
async function openChat(chid) {
resetSelectedGroup();
setCharacterId(id);
setCharacterId(chid);
await delay(1);
await reloadCurrentChat();
}
@ -2981,7 +3090,7 @@ async function continueChatCallback(args, prompt) {
resolve();
} catch (error) {
console.error('Error running /continue command:', error);
reject();
reject(error);
}
});
@ -3100,32 +3209,95 @@ async function setNarratorName(_, text) {
return '';
}
/**
* Checks if an argument is a string array (or undefined), and if not, throws an error
* @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined} arg The named argument to check
* @param {string} name The name of the argument for the error message
* @param {object} [options={}] - The optional arguments
* @param {boolean} [options.allowUndefined=false] - Whether the argument can be undefined
* @throws {Error} If the argument is not an array
* @returns {string[]}
*/
export function validateArrayArgString(arg, name, { allowUndefined = true } = {}) {
if (arg === undefined) {
if (allowUndefined) return undefined;
throw new Error(`Argument "${name}" is undefined, but must be a string array`);
}
if (!Array.isArray(arg)) throw new Error(`Argument "${name}" must be an array`);
if (!arg.every(x => typeof x === 'string')) throw new Error(`Argument "${name}" must be an array of strings`);
return arg;
}
/**
* Checks if an argument is a string or closure array (or undefined), and if not, throws an error
* @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined} arg The named argument to check
* @param {string} name The name of the argument for the error message
* @param {object} [options={}] - The optional arguments
* @param {boolean} [options.allowUndefined=false] - Whether the argument can be undefined
* @throws {Error} If the argument is not an array of strings or closures
* @returns {(string|SlashCommandClosure)[]}
*/
export function validateArrayArg(arg, name, { allowUndefined = true } = {}) {
if (arg === undefined) {
if (allowUndefined) return [];
throw new Error(`Argument "${name}" is undefined, but must be an array of strings or closures`);
}
if (!Array.isArray(arg)) throw new Error(`Argument "${name}" must be an array`);
if (!arg.every(x => typeof x === 'string' || x instanceof SlashCommandClosure)) throw new Error(`Argument "${name}" must be an array of strings or closures`);
return arg;
}
/**
* Retrieves the name and avatar information for a message
*
* The name of the character will always have precendence over the one given as argument. If you want to specify a different name for the message,
* explicitly implement this in the code using this.
*
* @param {object?} character - The character object to get the avatar data for
* @param {string?} name - The name to get the avatar data for
* @returns {{name: string, force_avatar: string, original_avatar: string}} An object containing the name for the message, forced avatar URL, and original avatar
*/
export function getNameAndAvatarForMessage(character, name = null) {
const isNeutralCharacter = !character && name2 === neutralCharacterName && name === neutralCharacterName;
const currentChar = characters[this_chid];
let force_avatar, original_avatar;
if (character?.avatar === currentChar?.avatar || isNeutralCharacter) {
// If the targeted character is the currently selected one in a solo chat, we don't need to force any avatars
}
else if (character && character.avatar !== 'none') {
force_avatar = getThumbnailUrl('avatar', character.avatar);
original_avatar = character.avatar;
}
else {
force_avatar = default_avatar;
original_avatar = default_avatar;
}
return {
name: character?.name || name,
force_avatar: force_avatar,
original_avatar: original_avatar,
};
}
export async function sendMessageAs(args, text) {
if (!text) {
toastr.warning('You must specify text to send as');
return '';
}
let name;
let name = args.name?.trim();
let mesText;
if (args.name) {
name = args.name.trim();
if (!name && !text) {
toastr.warning('You must specify a name and text to send as');
return '';
}
} else {
if (!name) {
const namelessWarningKey = 'sendAsNamelessWarningShown';
if (localStorage.getItem(namelessWarningKey) !== 'true') {
toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 });
localStorage.setItem(namelessWarningKey, 'true');
}
name = name2;
if (!text) {
toastr.warning('You must specify text to send as');
return '';
}
}
mesText = text.trim();
@ -3138,26 +3310,18 @@ export async function sendMessageAs(args, text) {
const isSystem = bias && !removeMacros(mesText).length;
const compact = isTrueBoolean(args?.compact);
const character = characters.find(x => x.avatar === name) ?? characters.find(x => x.name === name);
let force_avatar, original_avatar;
const character = findChar({ name: name });
const chatCharacter = this_chid !== undefined ? characters[this_chid] : null;
const isNeutralCharacter = !chatCharacter && name2 === neutralCharacterName && name === neutralCharacterName;
const avatarCharacter = args.avatar ? findChar({ name: args.avatar }) : character;
if (args.avatar && !avatarCharacter) {
toastr.warning(`Character for avatar ${args.avatar} not found`);
return '';
}
if (chatCharacter === character || isNeutralCharacter) {
// If the targeted character is the currently selected one in a solo chat, we don't need to force any avatars
}
else if (character && character.avatar !== 'none') {
force_avatar = getThumbnailUrl('avatar', character.avatar);
original_avatar = character.avatar;
}
else {
force_avatar = default_avatar;
original_avatar = default_avatar;
}
const { name: avatarCharName, force_avatar, original_avatar } = getNameAndAvatarForMessage(avatarCharacter, name);
const message = {
name: name,
name: character?.name || name || avatarCharName,
is_user: false,
is_system: isSystem,
send_date: getMessageTimeStamp(),
@ -3210,11 +3374,12 @@ export async function sendMessageAs(args, text) {
await saveChatConditional();
}
return '';
return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
}
export async function sendNarratorMessage(args, text) {
if (!text) {
toastr.warning('You must specify text to send');
return '';
}
@ -3263,7 +3428,7 @@ export async function sendNarratorMessage(args, text) {
await saveChatConditional();
}
return '';
return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
}
export async function promptQuietForLoudResponse(who, text) {
@ -3309,6 +3474,7 @@ export async function promptQuietForLoudResponse(who, text) {
async function sendCommentMessage(args, text) {
if (!text) {
toastr.warning('You must specify text to send');
return '';
}
@ -3351,7 +3517,7 @@ async function sendCommentMessage(args, text) {
await saveChatConditional();
}
return '';
return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
}
/**
@ -3431,11 +3597,12 @@ function setBackgroundCallback(_, bg) {
* Retrieves the available model options based on the currently selected main API and its subtype
* @param {boolean} quiet - Whether to suppress toasts
*
* @returns {{control: HTMLSelectElement, options: HTMLOptionElement[]}?} An array of objects representing the available model options, or null if not supported
* @returns {{control: HTMLSelectElement|HTMLInputElement, options: HTMLOptionElement[]}?} An array of objects representing the available model options, or null if not supported
*/
function getModelOptions(quiet) {
const nullResult = { control: null, options: null };
const modelSelectMap = [
{ id: 'custom_model_textgenerationwebui', api: 'textgenerationwebui', type: textgen_types.OOBA },
{ id: 'model_togetherai_select', api: 'textgenerationwebui', type: textgen_types.TOGETHERAI },
{ id: 'openrouter_model', api: 'textgenerationwebui', type: textgen_types.OPENROUTER },
{ id: 'model_infermaticai_select', api: 'textgenerationwebui', type: textgen_types.INFERMATICAI },
@ -3445,6 +3612,7 @@ function getModelOptions(quiet) {
{ id: 'aphrodite_model', api: 'textgenerationwebui', type: textgen_types.APHRODITE },
{ id: 'ollama_model', api: 'textgenerationwebui', type: textgen_types.OLLAMA },
{ id: 'tabby_model', api: 'textgenerationwebui', type: textgen_types.TABBY },
{ id: 'featherless_model', api: 'textgenerationwebui', type: textgen_types.FEATHERLESS },
{ id: 'model_openai_select', api: 'openai', type: chat_completion_sources.OPENAI },
{ id: 'model_claude_select', api: 'openai', type: chat_completion_sources.CLAUDE },
{ id: 'model_windowai_select', api: 'openai', type: chat_completion_sources.WINDOWAI },
@ -3452,7 +3620,7 @@ function getModelOptions(quiet) {
{ id: 'model_ai21_select', api: 'openai', type: chat_completion_sources.AI21 },
{ id: 'model_google_select', api: 'openai', type: chat_completion_sources.MAKERSUITE },
{ id: 'model_mistralai_select', api: 'openai', type: chat_completion_sources.MISTRALAI },
{ id: 'model_custom_select', api: 'openai', type: chat_completion_sources.CUSTOM },
{ id: 'custom_model_id', api: 'openai', type: chat_completion_sources.CUSTOM },
{ id: 'model_cohere_select', api: 'openai', type: chat_completion_sources.COHERE },
{ id: 'model_perplexity_select', api: 'openai', type: chat_completion_sources.PERPLEXITY },
{ id: 'model_groq_select', api: 'openai', type: chat_completion_sources.GROQ },
@ -3469,7 +3637,7 @@ function getModelOptions(quiet) {
case 'openai':
return oai_settings.chat_completion_source;
default:
return nullResult;
return null;
}
}
@ -3483,12 +3651,31 @@ function getModelOptions(quiet) {
const modelSelectControl = document.getElementById(modelSelectItem);
if (!(modelSelectControl instanceof HTMLSelectElement)) {
if (!(modelSelectControl instanceof HTMLSelectElement) && !(modelSelectControl instanceof HTMLInputElement)) {
!quiet && toastr.error(`Model select control not found: ${main_api}[${apiSubType}]`);
return nullResult;
}
const options = Array.from(modelSelectControl.options).filter(x => x.value);
/**
* Get options from a HTMLSelectElement or HTMLInputElement with a list.
* @param {HTMLSelectElement | HTMLInputElement} control Control containing the options
* @returns {HTMLOptionElement[]} Array of options
*/
const getOptions = (control) => {
if (control instanceof HTMLSelectElement) {
return Array.from(control.options);
}
const valueOption = new Option(control.value, control.value);
if (control instanceof HTMLInputElement && control.list instanceof HTMLDataListElement) {
return [valueOption, ...Array.from(control.list.options)];
}
return [valueOption];
};
const options = getOptions(modelSelectControl).filter(x => x.value).filter(onlyUnique);
return { control: modelSelectControl, options };
}
@ -3507,11 +3694,6 @@ function modelCallback(args, model) {
return '';
}
if (!options.length) {
!quiet && toastr.warning('No model options found. Check your API settings.');
return '';
}
model = String(model || '').trim();
if (!model) {
@ -3520,6 +3702,18 @@ function modelCallback(args, model) {
console.log('Set model to ' + model);
if (modelSelectControl instanceof HTMLInputElement) {
modelSelectControl.value = model;
$(modelSelectControl).trigger('input');
!quiet && toastr.success(`Model set to "${model}"`);
return model;
}
if (!options.length) {
!quiet && toastr.warning('No model options found. Check your API settings.');
return '';
}
let newSelectedOption = null;
const fuse = new Fuse(options, { keys: ['text', 'value'] });
@ -3561,11 +3755,17 @@ function setPromptEntryCallback(args, targetState) {
const prompts = promptManager.serviceSettings.prompts;
function parseArgs(arg) {
// Arg is already an array
if (Array.isArray(arg)) {
return arg;
}
const list = [];
try {
// Arg is a JSON-stringified array
const parsedArg = JSON.parse(arg);
list.push(...Array.isArray(parsedArg) ? parsedArg : [arg]);
} catch {
// Arg is a string
list.push(arg);
}
return list;

View File

@ -15,13 +15,13 @@ import { SlashCommandScope } from './SlashCommandScope.js';
* _abortController:SlashCommandAbortController,
* _debugController:SlashCommandDebugController,
* _hasUnnamedArgument:boolean,
* [id:string]:string|SlashCommandClosure,
* [id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined,
* }} NamedArguments
*/
/**
* Alternative object for local JSDocs, where you don't need existing pipe, scope, etc. arguments
* @typedef {{[id:string]:string|SlashCommandClosure}} NamedArgumentsCapture
* @typedef {{[id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined}} NamedArgumentsCapture
*/
/**

View File

@ -2,6 +2,7 @@ import { substituteParams } from '../../script.js';
import { delay, escapeRegex, uuidv4 } from '../utils.js';
import { SlashCommand } from './SlashCommand.js';
import { SlashCommandAbortController } from './SlashCommandAbortController.js';
import { SlashCommandNamedArgument } from './SlashCommandArgument.js';
import { SlashCommandBreak } from './SlashCommandBreak.js';
import { SlashCommandBreakController } from './SlashCommandBreakController.js';
import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js';
@ -53,7 +54,7 @@ export class SlashCommandClosure {
*
* @param {string} text
* @param {SlashCommandScope} scope
* @returns
* @returns {string|SlashCommandClosure|(string|SlashCommandClosure)[]}
*/
substituteParams(text, scope = null) {
let isList = false;
@ -379,6 +380,52 @@ export class SlashCommandClosure {
* @param {import('./SlashCommand.js').NamedArguments} args
*/
async substituteNamedArguments(executor, args) {
/**
* Handles the assignment of named arguments, considering if they accept multiple values
* @param {string} name The name of the argument, as defined for the command execution
* @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]} value The value to be assigned
*/
const assign = (name, value) => {
// If an array is supposed to be assigned, assign it one by one
if (Array.isArray(value)) {
for (const val of value) {
assign(name, val);
}
return;
}
const definition = executor.command.namedArgumentList.find(x => x.name == name);
// Prefer definition name if a valid named args defintion is found
name = definition?.name ?? name;
// Unescape named argument
if (value && typeof value == 'string') {
value = value
.replace(/\\\{/g, '{')
.replace(/\\\}/g, '}');
}
// If the named argument accepts multiple values, we have to make sure to build an array correctly
if (definition?.acceptsMultiple) {
if (args[name] !== undefined) {
// If there already is something for that named arg, make the value is an array and add to it
let currentValue = args[name];
if (!Array.isArray(currentValue)) {
currentValue = [currentValue];
}
currentValue.push(value);
args[name] = currentValue;
} else {
// If there is nothing in there, we create an array with that singular value
args[name] = [value];
}
} else {
args[name] !== undefined && console.debug(`Named argument assigned multiple times: ${name}`);
args[name] = value;
}
};
// substitute named arguments
for (const arg of executor.namedArgumentList) {
if (arg.value instanceof SlashCommandClosure) {
@ -390,19 +437,12 @@ export class SlashCommandClosure {
closure.debugController = this.debugController;
}
if (closure.executeNow) {
args[arg.name] = (await closure.execute())?.pipe;
assign(arg.name, (await closure.execute())?.pipe);
} else {
args[arg.name] = closure;
assign(arg.name, closure);
}
} else {
args[arg.name] = this.substituteParams(arg.value);
}
// unescape named argument
if (typeof args[arg.name] == 'string') {
args[arg.name] = args[arg.name]
?.replace(/\\\{/g, '{')
?.replace(/\\\}/g, '}')
;
assign(arg.name, this.substituteParams(arg.value));
}
}
}
@ -424,6 +464,7 @@ export class SlashCommandClosure {
} else {
value = [];
for (let i = 0; i < executor.unnamedArgumentList.length; i++) {
/** @type {string|SlashCommandClosure|(string|SlashCommandClosure)[]} */
let v = executor.unnamedArgumentList[i].value;
if (v instanceof SlashCommandClosure) {
/**@type {SlashCommandClosure}*/
@ -467,6 +508,14 @@ export class SlashCommandClosure {
return v;
});
}
value ??= '';
// Make sure that if unnamed args are split, it should always return an array
if (executor.command.splitUnnamedArgument && !Array.isArray(value)) {
value = [value];
}
return value;
}

View File

@ -2,7 +2,7 @@ import { chat_metadata, characters, substituteParams, chat, extension_prompt_rol
import { extension_settings } from '../extensions.js';
import { getGroupMembers, groups } from '../group-chats.js';
import { power_user } from '../power-user.js';
import { searchCharByName, getTagsList, tags } from '../tags.js';
import { searchCharByName, getTagsList, tags, tag_map } from '../tags.js';
import { world_names } from '../world-info.js';
import { SlashCommandClosure } from './SlashCommandClosure.js';
import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js';
@ -36,6 +36,7 @@ export const enumIcons = {
message: '💬',
voice: '🎤',
server: '🖥️',
popup: '🗔',
true: '✔️',
false: '❌',
@ -152,6 +153,35 @@ export const commonEnumProviders = {
].filter((item, idx, list)=>idx == list.findIndex(it=>it.value == item.value));
},
/**
* Enum values for numbers and variable names
*
* Includes all variable names and the ability to specify any number
*
* @param {SlashCommandExecutor} executor - The executor of the slash command
* @param {SlashCommandScope} scope - The scope of the slash command
* @returns {SlashCommandEnumValue[]} The enum values
*/
numbersAndVariables: (executor, scope) => [
...commonEnumProviders.variables('all')(executor, scope),
new SlashCommandEnumValue(
'any variable name',
null,
enumTypes.variable,
enumIcons.variable,
(input) => /^\w*$/.test(input),
(input) => input,
),
new SlashCommandEnumValue(
'any number',
null,
enumTypes.number,
enumIcons.number,
(input) => input == '' || !Number.isNaN(Number(input)),
(input) => input,
),
],
/**
* All possible char entities, like characters and groups. Can be filtered down to just one type.
*
@ -181,6 +211,18 @@ export const commonEnumProviders = {
*/
personas: () => Object.values(power_user.personas).map(persona => new SlashCommandEnumValue(persona, null, enumTypes.name, enumIcons.persona)),
/**
* All possible tags, or only those that have been assigned
*
* @param {('all' | 'assigned')} [mode='all'] - Which types of tags to show
* @returns {() => SlashCommandEnumValue[]}
*/
tags: (mode = 'all') => () => {
let assignedTags = mode === 'assigned' ? new Set(Object.values(tag_map).flat()) : new Set();
return tags.filter(tag => mode === 'all' || (mode === 'assigned' && assignedTags.has(tag.id)))
.map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag));
},
/**
* All possible tags for a given char/group entity
*
@ -193,7 +235,7 @@ export const commonEnumProviders = {
if (charName instanceof SlashCommandClosure) throw new Error('Argument \'name\' does not support closures');
const key = searchCharByName(substituteParams(charName), { suppressLogging: true });
const assigned = key ? getTagsList(key) : [];
return tags.filter(it => !key || mode === 'all' || mode === 'existing' && assigned.includes(it) || mode === 'not-existing' && !assigned.includes(it))
return tags.filter(it => mode === 'all' || mode === 'existing' && assigned.includes(it) || mode === 'not-existing' && !assigned.includes(it))
.map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag));
},

View File

@ -0,0 +1,80 @@
import { sendSystemMessage, system_message_types } from '../../script.js';
import { callGenericPopup, POPUP_TYPE } from '../popup.js';
import { escapeHtml } from '../utils.js';
import { enumIcons } from './SlashCommandCommonEnumsProvider.js';
import { enumTypes, SlashCommandEnumValue } from './SlashCommandEnumValue.js';
/** @typedef {'pipe'|'object'|'chat-html'|'chat-text'|'popup-html'|'popup-text'|'toast-html'|'toast-text'|'console'|'none'} SlashCommandReturnType */
export const slashCommandReturnHelper = {
// Without this, VSCode formatter fucks up JS docs. Don't ask me why.
_: false,
/**
* Gets/creates the enum list of types of return relevant for a slash command
*
* @param {object} [options={}] Options
* @param {boolean} [options.allowPipe=true] Allow option to pipe the return value
* @param {boolean} [options.allowObject=false] Allow option to return the value as an object
* @param {boolean} [options.allowChat=false] Allow option to return the value as a chat message
* @param {boolean} [options.allowPopup=false] Allow option to return the value as a popup
* @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
* @returns {SlashCommandEnumValue[]} The enum list
*/
enumList: ({ allowPipe = true, allowObject = false, allowChat = false, allowPopup = false, allowTextVersion = true } = {}) => [
allowPipe && new SlashCommandEnumValue('pipe', 'Return to the pipe for the next command', enumTypes.name, '|'),
allowObject && new SlashCommandEnumValue('object', 'Return as an object (or array) to the pipe for the next command', enumTypes.variable, enumIcons.dictionary),
allowChat && new SlashCommandEnumValue('chat-html', 'Sending a chat message with the return value - Can display HTML', enumTypes.command, enumIcons.message),
allowChat && allowTextVersion && new SlashCommandEnumValue('chat-text', 'Sending a chat message with the return value - Will only display as text', enumTypes.qr, enumIcons.message),
allowPopup && new SlashCommandEnumValue('popup-html', 'Showing as a popup with the return value - Can display HTML', enumTypes.command, enumIcons.popup),
allowPopup && allowTextVersion && new SlashCommandEnumValue('popup-text', 'Showing as a popup with the return value - Will only display as text', enumTypes.qr, enumIcons.popup),
new SlashCommandEnumValue('toast-html', 'Show the return value as a toast notification - Can display HTML', enumTypes.command, ''),
allowTextVersion && new SlashCommandEnumValue('toast-text', 'Show the return value as a toast notification - Will only display as text', enumTypes.qr, ''),
new SlashCommandEnumValue('console', 'Log the return value (object, if it can be one) to the console', enumTypes.enum, '>'),
new SlashCommandEnumValue('none', 'No return value'),
].filter(x => !!x),
/**
* Handles the return value based on the specified type
*
* @param {SlashCommandReturnType} type The type of return
* @param {object|number|string} value The value to return
* @param {object} [options={}] Options
* @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
* @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
* @returns {Promise<*>} The processed return value
*/
async doReturn(type, value, { objectToStringFunc = o => o?.toString(), objectToHtmlFunc = null } = {}) {
const shouldHtml = type.endsWith('html');
const actualConverterFunc = shouldHtml && objectToHtmlFunc ? objectToHtmlFunc : objectToStringFunc;
const stringValue = typeof value !== 'string' ? actualConverterFunc(value) : value;
switch (type) {
case 'popup-html':
case 'popup-text':
case 'chat-text':
case 'chat-html':
case 'toast-text':
case 'toast-html': {
const htmlOrNotHtml = shouldHtml ? DOMPurify.sanitize((new showdown.Converter()).makeHtml(stringValue)) : escapeHtml(stringValue);
if (type.startsWith('popup')) await callGenericPopup(htmlOrNotHtml, POPUP_TYPE.TEXT, '', { allowVerticalScrolling: true, wide: true });
if (type.startsWith('chat')) sendSystemMessage(system_message_types.GENERIC, htmlOrNotHtml);
if (type.startsWith('toast')) toastr.info(htmlOrNotHtml, null, { escapeHtml: !shouldHtml });
return '';
}
case 'pipe':
return stringValue ?? '';
case 'object':
return JSON.stringify(value);
case 'console':
console.info(value);
return '';
case 'none':
return '';
default:
throw new Error(`Unknown return type: ${type}`);
}
},
};

View File

@ -108,9 +108,21 @@ function getDelay(s) {
* @returns {AsyncGenerator<{data: object, chunk: string}>} The parsed data and the chunk to be sent.
*/
async function* parseStreamData(json) {
// Cohere
if (typeof json.delta.message === 'object' && ['tool-plan-delta', 'content-delta'].includes(json.type)) {
const text = json?.delta?.message?.content?.text ?? '';
for (let i = 0; i < text.length; i++) {
const str = json.delta.message.content.text[i];
yield {
data: { ...json, delta: { message: { content: { text: str } } } },
chunk: str,
};
}
return;
}
// Claude
if (typeof json.delta === 'object') {
if (typeof json.delta.text === 'string' && json.delta.text.length > 0) {
if (typeof json.delta === 'object' && typeof json.delta.text === 'string') {
if (json.delta.text.length > 0) {
for (let i = 0; i < json.delta.text.length; i++) {
const str = json.delta.text[i];
yield {

View File

@ -15,7 +15,7 @@ import {
import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js';
import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight, equalsIgnoreCaseAndAccents, includesIgnoreCaseAndAccents, removeFromArray, getFreeName, debounce } from './utils.js';
import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight, equalsIgnoreCaseAndAccents, includesIgnoreCaseAndAccents, removeFromArray, getFreeName, debounce, findChar } from './utils.js';
import { power_user } from './power-user.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
@ -50,7 +50,6 @@ export {
removeTagFromMap,
};
/** @typedef {import('../scripts/popup.js').Popup} Popup */
/** @typedef {import('../script.js').Character} Character */
const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter';
@ -507,7 +506,7 @@ export function getTagKeyForEntityElement(element) {
*/
export function searchCharByName(charName, { suppressLogging = false } = {}) {
const entity = charName
? (characters.find(x => x.name === charName) || groups.find(x => x.name == charName))
? (findChar({ name: charName }) || groups.find(x => equalsIgnoreCaseAndAccents(x.name, charName)))
: (selected_group ? groups.find(x => x.id == selected_group) : characters[this_chid]);
const key = getTagKeyForEntity(entity);
if (!key) {
@ -1861,8 +1860,9 @@ function registerTagsSlashCommands() {
return String(result);
},
namedArgumentList: [
SlashCommandNamedArgument.fromProps({ name: 'name',
description: 'Character name',
SlashCommandNamedArgument.fromProps({
name: 'name',
description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: '{{char}}',
enumProvider: commonEnumProviders.characters(),
@ -1907,7 +1907,7 @@ function registerTagsSlashCommands() {
},
namedArgumentList: [
SlashCommandNamedArgument.fromProps({ name: 'name',
description: 'Character name',
description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: '{{char}}',
enumProvider: commonEnumProviders.characters(),
@ -1950,7 +1950,7 @@ function registerTagsSlashCommands() {
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'name',
description: 'Character name',
description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: '{{char}}',
enumProvider: commonEnumProviders.characters(),
@ -1993,7 +1993,7 @@ function registerTagsSlashCommands() {
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'name',
description: 'Character name',
description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: '{{char}}',
enumProvider: commonEnumProviders.characters(),

View File

@ -1,3 +1,3 @@
<div>
<b>Note:</b> this chat is temporary and will be deleted as soon as you leave it.
<b data-i18n="Note:">Note:</b> <span data-i18n="this chat is temporary and will be deleted as soon as you leave it.">this chat is temporary and will be deleted as soon as you leave it.</span>
</div>

View File

@ -0,0 +1,5 @@
<b><span data-i18n="THIS IS PERMANENT!">THIS IS PERMANENT!</span><br><br>
<label for="del_char_checkbox" class="checkbox_label justifyCenter">
<input type="checkbox" id="del_char_checkbox" />
<small data-i18n="Also delete the chat files">Also delete the chat files</small>
</label></b>

View File

@ -1,5 +1,5 @@
<div>
<h3>Are you sure you want to duplicate this character?</h3>
<span>If you just want to start a new chat with the same character, use "Start new chat" option in the bottom-left options menu.</span>
<h3 data-i18n="Are you sure you want to duplicate this character?">Are you sure you want to duplicate this character?</h3>
<span data-i18n="If you just want to start a new chat with the same character...">If you just want to start a new chat with the same character, use "Start new chat" option in the bottom-left options menu.</span>
<br>
</div>

View File

@ -14,18 +14,18 @@
<li><kbd data-i18n="help_hotkeys_19">Ctrl+Shift+Down</kbd> = <span data-i18n="help_hotkeys_20">Scroll chat to bottom</span></li>
</ul>
<div>
<strong>Markdown Hotkeys</strong>
<strong data-i18n="help_hotkeys_20">Markdown Hotkeys</strong>
</div>
<div>
<small>
<span>Works in the chatbar and textareas marked with this icon:</span>
<span data-i18n="help_hotkeys_21">Works in the chatbar and textareas marked with this icon:</span>
<code><i class="fa-brands fa-markdown"></i></code>
</small>
</div>
<ul>
<li><kbd>Ctrl+B</kbd> = <span>**bold**</span></li>
<li><kbd>Ctrl+I</kbd> = <span>*italic*</span></li>
<li><kbd>Ctrl+U</kbd> = <span>__underline__</span></li>
<li><kbd>Ctrl+K</kbd> = <span>`inline code`</span></li>
<li><kbd>Ctrl+Shift+~</kbd> = <span>~~strikethrough~~</span></li>
<li><kbd>Ctrl+B</kbd> = <span data-i18n="help_hotkeys_22">**bold**</span></li>
<li><kbd>Ctrl+I</kbd> = <span data-i18n="help_hotkeys_23">*italic*</span></li>
<li><kbd>Ctrl+U</kbd> = <span data-i18n="help_hotkeys_24">__underline__</span></li>
<li><kbd>Ctrl+K</kbd> = <span data-i18n="help_hotkeys_25">`inline code`</span></li>
<li><kbd>Ctrl+Shift+~</kbd> = <span data-i18n="help_hotkeys_26">~~strikethrough~~</span></li>
</ul>

View File

@ -0,0 +1,4 @@
<h3><span data-i18n="Encountered an error while processing your request.">Encountered an error while processing your request.</span><br>
<span data-i18n="Check you have credits available on your">Check you have credits available on your</span>
<a href="https://platform.openai.com/account/usage" target="_blank" data-i18n="OpenAI account quora_error">OpenAI account</a><span data-i18n="dot quota_error">.</span><br>
<span data-i18n="If you have sufficient credits, please try again later.">If you have sufficient credits, please try again later.</span></h3>

View File

@ -4,30 +4,56 @@
<a href="https://docs.sillytavern.app/usage/update/" target="_blank" data-i18n="Want to update?">
Want to update?
</a>
<hr>
<h3 data-i18n="How to start chatting?">How to start chatting?</h3>
<ol>
<li>
<span data-i18n="Click _space">Click </span><code><i class="fa-solid fa-plug"></i></code><span data-i18n="and select a"> and select a </span><a href="https://docs.sillytavern.app/usage/api-connections/" target="_blank" data-i18n="Chat API">Chat API</a>.</span>
<span data-i18n="Click _space">Click </span>
<button class="menu_button menu_button_icon drawer-opener inline-flex" data-target="sys-settings-button">
<i class="fa-solid fa-plug"></i>
<span data-i18n="API Connections">API Connections</span>
</button>
<span data-i18n="and connect to an">and connect to an</span>
<a href="https://docs.sillytavern.app/usage/api-connections/" target="_blank">
<span class="fa-solid fa-circle-question"></span>
<span data-i18n="API">API</span></a>.
</li>
<li>
<span data-i18n="Click _space">Click </span><code><i class="fa-solid fa-address-card"></i></code><span data-i18n="and pick a character."> and pick a character.</span>
<span data-i18n="Click _space">Click </span>
<button class="menu_button menu_button_icon drawer-opener inline-flex" data-target="rightNavHolder">
<i class="fa-solid fa-address-card"></i>
<span data-i18n="Character Management">Character Management</span>
</button>
<span data-i18n="and pick a character."> and pick a character.</span>
</li>
</ol>
<div>
<span data-i18n="You can browse a list of bundled characters in the">
You can browse a list of bundled characters in the
</span>
<i data-i18n="Download Extensions & Assets">
Download Extensions & Assets
</i>
<span data-i18n="menu within">
menu within
</span>
<code><i class="fa-solid fa-cubes"></i></code>
<span>.</span>
<span data-i18n="You can add more">You can add more</span>
<button class="open_characters_library menu_button menu_button_icon inline-flex">
<i class="fa-solid fa-image-portrait"></i>
<span data-i18n="Sample characters">Sample characters</span>
</button>
<span data-i18n="or_welcome">or</span>
<button class="external_import_button menu_button menu_button_icon inline-flex">
<i class="fa-solid fa-cloud-arrow-down"></i>
<span data-i18n="Import Characters">Import characters</span>
</button>
<span data-i18n="from other websites">from other websites.</span>
</div>
<hr>
<div>
<span data-i18n="Go to the">Go to the</span>
<i data-i18n="Download Extensions & Assets">Download Extensions & Assets</i>
<span data-i18n="menu within">menu within</span>
<button class="menu_button menu_button_icon drawer-opener inline-flex" data-target="extensions-settings-button">
<i class="fa-solid fa-cubes"></i>
<span data-i18n="Extensions">Extensions</span>
</button>
<span data-i18n="to install additional features.">to install additional features.</span>
</div>
<h3 data-i18n="Confused or lost?">Confused or lost?</h3>
<ul>
<li>
@ -43,7 +69,6 @@
</li>
</ul>
<hr>
<h3 data-i18n="Still have questions?">Still have questions?</h3>
<ul>
<li>
@ -62,4 +87,3 @@
</a>
</li>
</ul>

View File

@ -1,8 +1,8 @@
<div id="WIEntryHeaderTitlesPC" class="flex-container wide100p spaceBetween justifyCenter textAlignCenter" style="padding:0 4.5em;">
<small class="flex1" data-i18n="Title/Memo">Title/Memo</small>
<small style="width: calc(3.5em + 15px)" data-i18n="Strategy">Strategy</small>
<small style="width: calc(3.5em + 30px)" data-i18n="Position">Position</small>
<small style="width: calc(3.5em + 20px)" data-i18n="Depth">Depth</small>
<small style="width: calc(3.5em + 10px)" data-i18n="Strategy">Strategy</small>
<small style="width: calc(3.5em + 20px)" data-i18n="Position">Position</small>
<small style="width: calc(3.5em + 15px)" data-i18n="Depth">Depth</small>
<small style="width: calc(3.5em + 20px)" data-i18n="Order">Order</small>
<small style="width: calc(3.5em + 15px)" data-i18n="Trigger %">Trigger %</small>
</div>

Some files were not shown because too many files have changed in this diff Show More