From 45009cd0e4e5df98c522f6efc5dde76dc7e3def1 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Sun, 22 Mar 2026 02:30:23 +0100 Subject: [PATCH] Deprecate legacy loader and migrate all callsites to action-loader system with informative toasts (#5326) * Refactor loader.js to use action-loader system and move overlay management into action-loader module - Deprecate showLoader() and hideLoader() in favor of action-loader API - Implement legacy functions as thin wrappers around ActionLoaderHandle - Move overlay management (showOverlay, hideOverlay, isOverlayDisplayed) into action-loader.js - Move Popup-based loader implementation and preloader cleanup to action-loader - Add loader.isBlocking() method to check for active blocking overlays * Migrate from legacy loader functions to action-loader API throughout codebase - Replace showLoader()/hideLoader() imports with loader from action-loader.js - Update firstLoadInit() to use loader.show() with title, message, and ToastMode.STATIC - Pass initLoaderHandle to getSettings() for early hide during onboarding flow - Refactor renameGroupOrCharacterChat() to use loader.show() instead of boolean flag - Wrap handleDeleteChat() with loader.show() and proper error handling - Update BulkEditOver... * Update loader titles and remove redundant reload notification - Change bookmark loader title from "Bookmark" to "Chat History" for clarity - Remove loader notification before extensions reload (redundant with browser reload) * lint fix * Add splash screen support to action loader with custom overlay content - Add `overlayContent` option to ActionLoaderOptions for custom HTML in overlay - Implement splash screen styles with centered logo, spinner, and message - Update firstLoadInit() to use custom splash screen instead of static toast - Pass custom content through showOverlay() to replace default spinner - Adjust non-blocking loader warning to account for custom overlay content * Refactor loader overlay to use DOM elements instead of HTML strings - Add createDefaultLoaderOverlay() function to generate fresh loader overlay elements - Export createOverlay() method on loader utility API for external use - Change overlayContent parameter type from string-only to string|HTMLElement|null - Add getOverlayContent() helper to normalize custom content for Popup - Update firstLoadInit() to build splash screen using DOM manipulation instead of template literals - Add splash-logo class and * Use a true ellipsis * Adjust sizing for desktop * Even truer ellipsis * Add transition to splash screen and fix blur animation on hideOverlay (#5338) * Initial plan * Blur entire splash screen on hideOverlay, not just spinner Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/eee6c06d-7c9d-4363-bc8f-2647ed390368 * Add transition to splash-screen and fix transition detection Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/9368bc36-31a0-4a58-aebd-7b569696ff2e --------- Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com> Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * Add translations to supported locales * Localize logo alt on welcome screen --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com> Co-authored-by: Claude <242468646+Claude@users.noreply.github.com> --- public/css/loader.css | 33 ++++ public/locales/ar-sa.json | 12 +- public/locales/de-de.json | 12 +- public/locales/es-es.json | 12 +- public/locales/fr-fr.json | 12 +- public/locales/is-is.json | 12 +- public/locales/it-it.json | 12 +- public/locales/ja-jp.json | 12 +- public/locales/ko-kr.json | 12 +- public/locales/nl-nl.json | 12 +- public/locales/pt-pt.json | 12 +- public/locales/ru-ru.json | 12 +- public/locales/th-th.json | 12 +- public/locales/uk-ua.json | 12 +- public/locales/vi-vn.json | 12 +- public/locales/zh-cn.json | 10 +- public/locales/zh-tw.json | 12 +- public/script.js | 73 ++++++--- public/scripts/BulkEditOverlay.js | 15 +- public/scripts/action-loader.js | 176 ++++++++++++++++++++- public/scripts/bookmarks.js | 11 +- public/scripts/extensions.js | 2 - public/scripts/loader.js | 115 ++++++-------- public/scripts/st-context.js | 2 + public/scripts/templates/welcomePanel.html | 2 +- 25 files changed, 495 insertions(+), 124 deletions(-) diff --git a/public/css/loader.css b/public/css/loader.css index 169811708..93ee05d24 100644 --- a/public/css/loader.css +++ b/public/css/loader.css @@ -53,3 +53,36 @@ opacity: 1; color: color-mix(in srgb, currentColor 40%, #e74c3c 60%); } + +/* Splash screen styles for branded loading */ +#loader.splash-screen { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4rem; + width: 100%; + height: 100%; + transition: all var(--animation-duration-2x) ease-out; +} + +#loader.splash-screen #load-spinner { + position: static; + top: unset; + left: unset; +} + +.splash-logo { + /* max original px, or 50% on mobile */ + width: min(150px, 50%); + height: auto; + filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3)); +} + +.splash-message { + margin: 0; + font-size: 1.25rem; + font-weight: 500; + opacity: 0.9; + letter-spacing: 0.02em; +} diff --git a/public/locales/ar-sa.json b/public/locales/ar-sa.json index 0e4fb270b..31c28830b 100644 --- a/public/locales/ar-sa.json +++ b/public/locales/ar-sa.json @@ -1444,5 +1444,15 @@ "Interleaved Thinking": "التفكير المتداخل", "Since Last User Message": "منذ آخر رسالة من المستخدم", "Active Tool Chain": "سلسلة الأدوات النشطة", - "openrouter_interleaved_thinking_hint": "يرسل الاستدلال من رسائل المساعد السابقة مع طلبات استدعاء الأدوات للحفاظ على سياق التفكير المتداخل." + "openrouter_interleaved_thinking_hint": "يرسل الاستدلال من رسائل المساعد السابقة مع طلبات استدعاء الأدوات للحفاظ على سياق التفكير المتداخل.", + "SillyTavern Logo": "شعار SillyTavern", + "Initializing…": "جارٍ التهيئة…", + "Name not accepted, as it is the same as before (ignoring case and accents).": "الاسم غير مقبول، لأنه نفسه كما كان من قبل (تجاهل حالة الأحرف والنبرات).", + "Rename Chat": "إعادة تسمية المحادثة", + "Renaming chat…": "جارٍ إعادة تسمية المحادثة…", + "Delete Chat": "حذف المحادثة", + "Deleting chat…": "جارٍ حذف المحادثة…", + "Loading chat…": "جارٍ تحميل المحادثة…", + "Bulk Delete": "حذف جماعي", + "Deleting ${0} character(s)…": "جارٍ حذف ${0} شخصية (شخصيات)…" } diff --git a/public/locales/de-de.json b/public/locales/de-de.json index 6d05a899a..eede60a6f 100644 --- a/public/locales/de-de.json +++ b/public/locales/de-de.json @@ -1444,5 +1444,15 @@ "Interleaved Thinking": "Verschränktes Denken", "Since Last User Message": "Seit letzter Nutzernachricht", "Active Tool Chain": "Aktive Tool-Kette", - "openrouter_interleaved_thinking_hint": "Sendet Begründungen aus vorherigen Assistentennachrichten mit Tool-Aufrufen, um den Kontext für verschränktes Denken zu erhalten." + "openrouter_interleaved_thinking_hint": "Sendet Begründungen aus vorherigen Assistentennachrichten mit Tool-Aufrufen, um den Kontext für verschränktes Denken zu erhalten.", + "SillyTavern Logo": "SillyTavern-Logo", + "Initializing…": "Initialisierung…", + "Name not accepted, as it is the same as before (ignoring case and accents).": "Name nicht akzeptiert, da er derselbe wie zuvor ist (Groß-/Kleinschreibung und Akzente ignoriert).", + "Rename Chat": "Chat umbenennen", + "Renaming chat…": "Chat wird umbenannt…", + "Delete Chat": "Chat löschen", + "Deleting chat…": "Chat wird gelöscht…", + "Loading chat…": "Chat wird geladen…", + "Bulk Delete": "Massenlöschung", + "Deleting ${0} character(s)…": "${0} Charakter(e) werden gelöscht…" } diff --git a/public/locales/es-es.json b/public/locales/es-es.json index d8a791a3a..f3f099e34 100644 --- a/public/locales/es-es.json +++ b/public/locales/es-es.json @@ -1551,5 +1551,15 @@ "Interleaved Thinking": "Razonamiento intercalado", "Since Last User Message": "Desde el último mensaje del usuario", "Active Tool Chain": "Cadena de herramientas activa", - "openrouter_interleaved_thinking_hint": "Envía razonamiento de turnos anteriores del asistente junto con solicitudes de herramientas para mantener el contexto del razonamiento intercalado." + "openrouter_interleaved_thinking_hint": "Envía razonamiento de turnos anteriores del asistente junto con solicitudes de herramientas para mantener el contexto del razonamiento intercalado.", + "SillyTavern Logo": "Logo de SillyTavern", + "Initializing…": "Inicializando…", + "Name not accepted, as it is the same as before (ignoring case and accents).": "Nombre no aceptado, ya que es el mismo que antes (ignorando mayúsculas y acentos).", + "Rename Chat": "Renombrar chat", + "Renaming chat…": "Renombrando chat…", + "Delete Chat": "Eliminar chat", + "Deleting chat…": "Eliminando chat…", + "Loading chat…": "Cargando chat…", + "Bulk Delete": "Eliminación masiva", + "Deleting ${0} character(s)…": "Eliminando ${0} personaje(s)…" } diff --git a/public/locales/fr-fr.json b/public/locales/fr-fr.json index 2abacb386..fb4d709a1 100644 --- a/public/locales/fr-fr.json +++ b/public/locales/fr-fr.json @@ -2052,5 +2052,15 @@ "Interleaved Thinking": "Raisonnement intercalé", "Since Last User Message": "Depuis le dernier message utilisateur", "Active Tool Chain": "Chaîne d'outils active", - "openrouter_interleaved_thinking_hint": "Envoie le raisonnement des tours précédents de l'assistant avec les requêtes d'outils pour conserver le contexte du raisonnement intercalé." + "openrouter_interleaved_thinking_hint": "Envoie le raisonnement des tours précédents de l'assistant avec les requêtes d'outils pour conserver le contexte du raisonnement intercalé.", + "SillyTavern Logo": "Logo SillyTavern", + "Initializing…": "Initialisation…", + "Name not accepted, as it is the same as before (ignoring case and accents).": "Nom non accepté, car il est identique à l'ancien (en ignorant la casse et les accents).", + "Rename Chat": "Renommer la discussion", + "Renaming chat…": "Renommage de la discussion…", + "Delete Chat": "Supprimer la discussion", + "Deleting chat…": "Suppression de la discussion…", + "Loading chat…": "Chargement de la discussion…", + "Bulk Delete": "Suppression groupée", + "Deleting ${0} character(s)…": "Suppression de ${0} personnage(s)…" } diff --git a/public/locales/is-is.json b/public/locales/is-is.json index d1bda3a89..9c15493a6 100644 --- a/public/locales/is-is.json +++ b/public/locales/is-is.json @@ -1442,5 +1442,15 @@ "Interleaved Thinking": "Fléttuð hugsun", "Since Last User Message": "Frá síðustu notandaskilaboðum", "Active Tool Chain": "Virk verkfærakeðja", - "openrouter_interleaved_thinking_hint": "Sendir röksemdir úr fyrri aðstoðarskilaboðum með verkfæraköllum til að viðhalda samhengi fléttaðrar hugsunar." + "openrouter_interleaved_thinking_hint": "Sendir röksemdir úr fyrri aðstoðarskilaboðum með verkfæraköllum til að viðhalda samhengi fléttaðrar hugsunar.", + "SillyTavern Logo": "SillyTavern merki", + "Initializing…": "Ræsir…", + "Name not accepted, as it is the same as before (ignoring case and accents).": "Nafn ekki samþykkt, því það er það sama og áður (hunsar há-/lágstafi og brottmerki).", + "Rename Chat": "Endurnefna spjall", + "Renaming chat…": "Endurnefni spjall…", + "Delete Chat": "Eyða spjalli", + "Deleting chat…": "Eyði spjalli…", + "Loading chat…": "Hleð spjalli…", + "Bulk Delete": "Fjöldaeyðing", + "Deleting ${0} character(s)…": "Eyði ${0} persónu(m)…" } diff --git a/public/locales/it-it.json b/public/locales/it-it.json index 7bc293865..afcb15ea2 100644 --- a/public/locales/it-it.json +++ b/public/locales/it-it.json @@ -1444,5 +1444,15 @@ "Interleaved Thinking": "Ragionamento intercalato", "Since Last User Message": "Dall'ultimo messaggio utente", "Active Tool Chain": "Catena di strumenti attiva", - "openrouter_interleaved_thinking_hint": "Invia il ragionamento dai turni precedenti dell'assistente con le richieste di strumenti per mantenere il contesto del ragionamento intercalato." + "openrouter_interleaved_thinking_hint": "Invia il ragionamento dai turni precedenti dell'assistente con le richieste di strumenti per mantenere il contesto del ragionamento intercalato.", + "SillyTavern Logo": "Logo SillyTavern", + "Initializing…": "Inizializzazione…", + "Name not accepted, as it is the same as before (ignoring case and accents).": "Nome non accettato, poiché è lo stesso di prima (ignorando maiuscole e accenti).", + "Rename Chat": "Rinomina chat", + "Renaming chat…": "Rinominazione chat…", + "Delete Chat": "Elimina chat", + "Deleting chat…": "Eliminazione chat…", + "Loading chat…": "Caricamento chat…", + "Bulk Delete": "Eliminazione multipla", + "Deleting ${0} character(s)…": "Eliminazione di ${0} personaggio/i…" } diff --git a/public/locales/ja-jp.json b/public/locales/ja-jp.json index 394a26c24..b15d600b8 100644 --- a/public/locales/ja-jp.json +++ b/public/locales/ja-jp.json @@ -1449,5 +1449,15 @@ "Interleaved Thinking": "インターリーブ思考", "Since Last User Message": "直前のユーザーメッセージ以降", "Active Tool Chain": "アクティブなツールチェーン", - "openrouter_interleaved_thinking_hint": "インターリーブ思考の文脈を維持するため、直前のアシスタントターンの推論をツール呼び出しリクエストとともに送信します。" + "openrouter_interleaved_thinking_hint": "インターリーブ思考の文脈を維持するため、直前のアシスタントターンの推論をツール呼び出しリクエストとともに送信します。", + "SillyTavern Logo": "SillyTavernロゴ", + "Initializing…": "初期化中…", + "Name not accepted, as it is the same as before (ignoring case and accents).": "名前が受け入れられませんでした。以前と同じです(大文字小文字とアクセントを無視)。", + "Rename Chat": "チャット名を変更", + "Renaming chat…": "チャット名を変更中…", + "Delete Chat": "チャットを削除", + "Deleting chat…": "チャットを削除中…", + "Loading chat…": "チャットを読み込み中…", + "Bulk Delete": "一括削除", + "Deleting ${0} character(s)…": "${0}人のキャラクターを削除中…" } diff --git a/public/locales/ko-kr.json b/public/locales/ko-kr.json index 4354c9220..c54f27638 100644 --- a/public/locales/ko-kr.json +++ b/public/locales/ko-kr.json @@ -1623,5 +1623,15 @@ "Interleaved Thinking": "인터리브드 사고", "Since Last User Message": "마지막 사용자 메시지 이후", "Active Tool Chain": "활성 도구 체인", - "openrouter_interleaved_thinking_hint": "인터리브드 사고 컨텍스트를 유지하기 위해 이전 어시스턴트 턴의 추론을 도구 호출 요청과 함께 전송합니다." + "openrouter_interleaved_thinking_hint": "인터리브드 사고 컨텍스트를 유지하기 위해 이전 어시스턴트 턴의 추론을 도구 호출 요청과 함께 전송합니다.", + "SillyTavern Logo": "SillyTavern 로고", + "Initializing…": "초기화 중…", + "Name not accepted, as it is the same as before (ignoring case and accents).": "이름이 승인되지 않았습니다. 이전과 동일합니다(대소문자 및 악센트 무시).", + "Rename Chat": "채팅 이름 변경", + "Renaming chat…": "채팅 이름 변경 중…", + "Delete Chat": "채팅 삭제", + "Deleting chat…": "채팅 삭제 중…", + "Loading chat…": "채팅 불러오는 중…", + "Bulk Delete": "대량 삭제", + "Deleting ${0} character(s)…": "${0}개 캐릭터 삭제 중…" } diff --git a/public/locales/nl-nl.json b/public/locales/nl-nl.json index ba72a659e..8af57f559 100644 --- a/public/locales/nl-nl.json +++ b/public/locales/nl-nl.json @@ -1440,5 +1440,15 @@ "Interleaved Thinking": "Verweven redenering", "Since Last User Message": "Sinds laatste gebruikersbericht", "Active Tool Chain": "Actieve toolketen", - "openrouter_interleaved_thinking_hint": "Verstuurt redenering uit eerdere assistentbeurten met tool-aanvragen om de context van verweven redenering te behouden." + "openrouter_interleaved_thinking_hint": "Verstuurt redenering uit eerdere assistentbeurten met tool-aanvragen om de context van verweven redenering te behouden.", + "SillyTavern Logo": "SillyTavern-logo", + "Initializing…": "Initialiseren…", + "Name not accepted, as it is the same as before (ignoring case and accents).": "Naam niet geaccepteerd, omdat deze hetzelfde is als voorheen (hoofdletters en accenten genegeerd).", + "Rename Chat": "Chat hernoemen", + "Renaming chat…": "Chat hernoemen…", + "Delete Chat": "Chat verwijderen", + "Deleting chat…": "Chat verwijderen…", + "Loading chat…": "Chat laden…", + "Bulk Delete": "Bulkverwijdering", + "Deleting ${0} character(s)…": "${0} personage(s) verwijderen…" } diff --git a/public/locales/pt-pt.json b/public/locales/pt-pt.json index be2349150..fac3cd2ac 100644 --- a/public/locales/pt-pt.json +++ b/public/locales/pt-pt.json @@ -1442,5 +1442,15 @@ "Interleaved Thinking": "Raciocínio intercalado", "Since Last User Message": "Desde a última mensagem do utilizador", "Active Tool Chain": "Cadeia de ferramentas ativa", - "openrouter_interleaved_thinking_hint": "Envia o raciocínio de turnos anteriores do assistente com pedidos de ferramentas para manter o contexto de raciocínio intercalado." + "openrouter_interleaved_thinking_hint": "Envia o raciocínio de turnos anteriores do assistente com pedidos de ferramentas para manter o contexto de raciocínio intercalado.", + "SillyTavern Logo": "Logótipo do SillyTavern", + "Initializing…": "A inicializar…", + "Name not accepted, as it is the same as before (ignoring case and accents).": "Nome não aceite, pois é o mesmo que antes (ignorando maiúsculas e acentos).", + "Rename Chat": "Renomear chat", + "Renaming chat…": "A renomear chat…", + "Delete Chat": "Eliminar chat", + "Deleting chat…": "A eliminar chat…", + "Loading chat…": "A carregar chat…", + "Bulk Delete": "Eliminação em massa", + "Deleting ${0} character(s)…": "A eliminar ${0} personagem(ns)…" } diff --git a/public/locales/ru-ru.json b/public/locales/ru-ru.json index 448c0b359..6be63c092 100644 --- a/public/locales/ru-ru.json +++ b/public/locales/ru-ru.json @@ -2744,5 +2744,15 @@ "Interleaved Thinking": "Чередующееся рассуждение", "Since Last User Message": "С момента последнего сообщения пользователя", "Active Tool Chain": "Активная цепочка инструментов", - "openrouter_interleaved_thinking_hint": "Отправляет рассуждение из предыдущих ходов ассистента вместе с запросами вызова инструментов, чтобы сохранять контекст чередующегося рассуждения." + "openrouter_interleaved_thinking_hint": "Отправляет рассуждение из предыдущих ходов ассистента вместе с запросами вызова инструментов, чтобы сохранять контекст чередующегося рассуждения.", + "SillyTavern Logo": "Логотип SillyTavern", + "Initializing…": "Инициализация…", + "Name not accepted, as it is the same as before (ignoring case and accents).": "Имя не принято, так как оно совпадает с предыдущим (игнорируя регистр и акценты).", + "Rename Chat": "Переименовать чат", + "Renaming chat…": "Переименование чата…", + "Delete Chat": "Удалить чат", + "Deleting chat…": "Удаление чата…", + "Loading chat…": "Загрузка чата…", + "Bulk Delete": "Массовое удаление", + "Deleting ${0} character(s)…": "Удаление ${0} персонажа(ей)…" } diff --git a/public/locales/th-th.json b/public/locales/th-th.json index 942fb857c..21dee67f1 100644 --- a/public/locales/th-th.json +++ b/public/locales/th-th.json @@ -1461,5 +1461,15 @@ "Interleaved Thinking": "การคิดแบบสลับขั้น", "Since Last User Message": "ตั้งแต่ข้อความล่าสุดของผู้ใช้", "Active Tool Chain": "สายโซ่เครื่องมือที่ใช้งานอยู่", - "openrouter_interleaved_thinking_hint": "ส่งเหตุผลจากเทิร์นก่อนหน้าของผู้ช่วยไปพร้อมกับคำขอเรียกใช้เครื่องมือ เพื่อคงบริบทการคิดแบบสลับขั้นไว้" + "openrouter_interleaved_thinking_hint": "ส่งเหตุผลจากเทิร์นก่อนหน้าของผู้ช่วยไปพร้อมกับคำขอเรียกใช้เครื่องมือ เพื่อคงบริบทการคิดแบบสลับขั้นไว้", + "SillyTavern Logo": "โลโก้ SillyTavern", + "Initializing…": "กำลังเริ่มต้น…", + "Name not accepted, as it is the same as before (ignoring case and accents).": "ชื่อไม่ได้รับการยอมรับ เนื่องจากเหมือนกับก่อนหน้านี้ (ไม่สนใจตัวพิมพ์ใหญ่เล็กและเครื่องหมายเสียง)", + "Rename Chat": "เปลี่ยนชื่อแชท", + "Renaming chat…": "กำลังเปลี่ยนชื่อแชท…", + "Delete Chat": "ลบแชท", + "Deleting chat…": "กำลังลบแชท…", + "Loading chat…": "กำลังโหลดแชท…", + "Bulk Delete": "ลบจำนวนมาก", + "Deleting ${0} character(s)…": "กำลังลบ ${0} ตัวละคร…" } diff --git a/public/locales/uk-ua.json b/public/locales/uk-ua.json index 57e180949..5179547b4 100644 --- a/public/locales/uk-ua.json +++ b/public/locales/uk-ua.json @@ -1442,5 +1442,15 @@ "Interleaved Thinking": "Черговане мислення", "Since Last User Message": "Від останнього повідомлення користувача", "Active Tool Chain": "Активний ланцюжок інструментів", - "openrouter_interleaved_thinking_hint": "Надсилає міркування з попередніх ходів асистента разом із запитами виклику інструментів, щоб зберегти контекст чергованого мислення." + "openrouter_interleaved_thinking_hint": "Надсилає міркування з попередніх ходів асистента разом із запитами виклику інструментів, щоб зберегти контекст чергованого мислення.", + "SillyTavern Logo": "Логотип SillyTavern", + "Initializing…": "Ініціалізація…", + "Name not accepted, as it is the same as before (ignoring case and accents).": "Ім'я не прийнято, оскільки воно збігається з попереднім (ігноруючи регістр та акценти).", + "Rename Chat": "Перейменувати чат", + "Renaming chat…": "Перейменування чату…", + "Delete Chat": "Видалити чат", + "Deleting chat…": "Видалення чату…", + "Loading chat…": "Завантаження чату…", + "Bulk Delete": "Масове видалення", + "Deleting ${0} character(s)…": "Видалення ${0} персонажа(ів)…" } diff --git a/public/locales/vi-vn.json b/public/locales/vi-vn.json index ab5b53975..823c7fe79 100644 --- a/public/locales/vi-vn.json +++ b/public/locales/vi-vn.json @@ -1442,5 +1442,15 @@ "Interleaved Thinking": "Lập luận xen kẽ", "Since Last User Message": "Kể từ tin nhắn người dùng gần nhất", "Active Tool Chain": "Chuỗi công cụ đang hoạt động", - "openrouter_interleaved_thinking_hint": "Gửi lập luận từ các lượt trợ lý trước đó cùng với yêu cầu gọi công cụ để duy trì ngữ cảnh lập luận xen kẽ." + "openrouter_interleaved_thinking_hint": "Gửi lập luận từ các lượt trợ lý trước đó cùng với yêu cầu gọi công cụ để duy trì ngữ cảnh lập luận xen kẽ.", + "SillyTavern Logo": "Logo SillyTavern", + "Initializing…": "Đang khởi tạo…", + "Name not accepted, as it is the same as before (ignoring case and accents).": "Tên không được chấp nhận vì giống như trước đó (bỏ qua chữ hoa/thường và dấu).", + "Rename Chat": "Đổi tên cuộc trò chuyện", + "Renaming chat…": "Đang đổi tên cuộc trò chuyện…", + "Delete Chat": "Xóa cuộc trò chuyện", + "Deleting chat…": "Đang xóa cuộc trò chuyện…", + "Loading chat…": "Đang tải cuộc trò chuyện…", + "Bulk Delete": "Xóa hàng loạt", + "Deleting ${0} character(s)…": "Đang xóa ${0} nhân vật…" } diff --git a/public/locales/zh-cn.json b/public/locales/zh-cn.json index d04f09c07..511abf9e5 100644 --- a/public/locales/zh-cn.json +++ b/public/locales/zh-cn.json @@ -3635,5 +3635,13 @@ "sd_prompt_9": "你的人设 (多模态模式)", "Show only favorites": "仅显示收藏", "Show only groups": "仅显示群聊", - "Show Tag List": "显示标签列表" + "Show Tag List": "显示标签列表", + "SillyTavern Logo": "SillyTavern 徽标", + "Initializing…": "正在初始化…", + "Renaming chat…": "正在重命名聊天…", + "Delete Chat": "删除聊天", + "Deleting chat…": "正在删除聊天…", + "Loading chat…": "正在加载聊天…", + "Bulk Delete": "批量删除", + "Deleting ${0} character(s)…": "正在删除 ${0} 个角色…" } diff --git a/public/locales/zh-tw.json b/public/locales/zh-tw.json index b722abf6c..1ccf50e46 100644 --- a/public/locales/zh-tw.json +++ b/public/locales/zh-tw.json @@ -2764,5 +2764,15 @@ "Interleaved Thinking": "交錯思維", "Since Last User Message": "自上一則使用者訊息起", "Active Tool Chain": "使用中的工具鏈", - "openrouter_interleaved_thinking_hint": "會在工具呼叫請求中附帶先前助理回合的推理,以維持交錯思維的上下文。" + "openrouter_interleaved_thinking_hint": "會在工具呼叫請求中附帶先前助理回合的推理,以維持交錯思維的上下文。", + "SillyTavern Logo": "SillyTavern 標誌", + "Initializing…": "正在初始化…", + "Name not accepted, as it is the same as before (ignoring case and accents).": "名稱不被接受,因為它與之前相同(忽略大小寫和重音)。", + "Rename Chat": "重新命名聊天", + "Renaming chat…": "正在重新命名聊天…", + "Delete Chat": "刪除聊天", + "Deleting chat…": "正在刪除聊天…", + "Loading chat…": "正在載入聊天…", + "Bulk Delete": "批次刪除", + "Deleting ${0} character(s)…": "正在刪除 ${0} 個角色…" } diff --git a/public/script.js b/public/script.js index 1c4046ff1..2e18e8d70 100644 --- a/public/script.js +++ b/public/script.js @@ -244,7 +244,7 @@ import { isPersonaPanelOpen, } from './scripts/personas.js'; import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js'; -import { hideLoader, showLoader } from './scripts/loader.js'; +import { loader } from './scripts/action-loader.js'; import { BulkEditOverlay } from './scripts/BulkEditOverlay.js'; import { initTextGenModels } from './scripts/textgen-models.js'; import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, preserveNeutralChat, restoreNeutralChat, formatCreatorNotes, initChatUtilities, addDOMPurifyHooks } from './scripts/chats.js'; @@ -698,7 +698,28 @@ async function firstLoadInit() { throw new Error('Initialization failed'); } - showLoader(); + const initLoaderOverlay = loader.createOverlay(); + initLoaderOverlay.classList.add('splash-screen'); + + const splashLogo = document.createElement('img'); + splashLogo.src = '/img/logo.png'; + splashLogo.alt = 'SillyTavern'; + splashLogo.className = 'splash-logo'; + splashLogo.ariaLabel = t`SillyTavern Logo`; + + const splashMessage = document.createElement('h2'); + splashMessage.className = 'splash-message'; + splashMessage.textContent = t`Initializing…`; + splashMessage.dataset.i18n = 'Initializing…'; + + initLoaderOverlay.prepend(splashLogo); + initLoaderOverlay.appendChild(splashMessage); + + const initLoaderHandle = loader.show({ + toastMode: loader.ToastMode.NONE, + overlayContent: initLoaderOverlay, + }); + registerPromptManagerMigration(); initDomHandlers(); initStandaloneMode(); @@ -724,7 +745,7 @@ async function firstLoadInit() { ToolManager.initToolSlashCommands(); await initPresetManager(); await initSystemMessages(); - await getSettings(); + await getSettings(initLoaderHandle); initKeyboard(); initDynamicStyles(); initTags(); @@ -758,7 +779,7 @@ async function firstLoadInit() { addDebugFunctions(); doDailyExtensionUpdatesCheck(); await eventSource.emit(event_types.APP_INITIALIZED); - await hideLoader(); + await initLoaderHandle.hide(); await fixViewport(); await eventSource.emit(event_types.APP_READY); } @@ -7763,7 +7784,7 @@ function reloadLoop() { //MARK: getSettings() /////////////////////////////////////////// -export async function getSettings() { +export async function getSettings(initLoaderHandle = null) { const response = await fetch('/api/settings/get', { method: 'POST', headers: getRequestHeaders(), @@ -7881,7 +7902,7 @@ export async function getSettings() { firstRun = !!settings.firstRun; if (firstRun) { - hideLoader(); + await initLoaderHandle?.hide(); await doOnboarding(user_avatar); firstRun = false; } @@ -10457,7 +10478,7 @@ export async function doNewChat({ deleteCurrentChat = false } = {}) { * @param {string} param.newFileName New name for the chat (no JSONL extension) * @param {boolean} [param.loader=true] Whether to show loader during the operation */ -export async function renameGroupOrCharacterChat({ characterId, groupId, oldFileName, newFileName, loader }) { +export async function renameGroupOrCharacterChat({ characterId, groupId, oldFileName, newFileName, loader: showLoader }) { const currentChatId = getCurrentChatId(); const body = { is_group: !!groupId, @@ -10475,9 +10496,13 @@ export async function renameGroupOrCharacterChat({ characterId, groupId, oldFile return; } - try { - loader && showLoader(); + const loaderHandle = showLoader ? loader.show({ + title: t`Rename Chat`, + message: t`Renaming chat…`, + toastMode: loader.ToastMode.STATIC, + }) : null; + try { const response = await fetch('/api/chats/rename', { method: 'POST', body: JSON.stringify(body), @@ -10510,11 +10535,10 @@ export async function renameGroupOrCharacterChat({ characterId, groupId, oldFile await reloadCurrentChat(); } } catch { - loader && hideLoader(); await delay(500); await callGenericPopup('An error has occurred. Chat was not renamed.', POPUP_TYPE.TEXT); } finally { - loader && hideLoader(); + await loaderHandle?.hide(); } } @@ -11072,21 +11096,32 @@ jQuery(async function () { async function handleDeleteChat(chatFile, group, fromSlashCommand = false) { // Close past chat popup. $('#select_chat_cross').trigger('click'); - showLoader(); - if (group) { - await deleteGroupChat(group, chatFile); - } else { - await delChat(`${chatFile}.jsonl`); + + const loaderHandle = loader.show({ + title: t`Delete Chat`, + message: t`Deleting chat…`, + toastMode: loader.ToastMode.STATIC, + }); + + try { + if (group) { + await deleteGroupChat(group, chatFile); + } else { + await delChat(`${chatFile}.jsonl`); + } + } catch (error) { + loaderHandle.hide(); + throw error; } if (fromSlashCommand) { // When called from `/delchat` command, don't re-open the history view. $('#options').hide(); // Hide option popup menu. - hideLoader(); + await loaderHandle.hide(); } else { // Open the history view again after 2 seconds (delay to avoid edge cases for deleting last chat). - setTimeout(function () { + setTimeout(async function () { $('#option_select_chat').trigger('click'); $('#options').hide(); // Hide option popup menu. - hideLoader(); + await loaderHandle.hide(); }, 2000); } } diff --git a/public/scripts/BulkEditOverlay.js b/public/scripts/BulkEditOverlay.js index 135ed558f..0ae6cf8d5 100644 --- a/public/scripts/BulkEditOverlay.js +++ b/public/scripts/BulkEditOverlay.js @@ -14,10 +14,11 @@ import { } from '../script.js'; import { favsToHotswap } from './RossAscends-mods.js'; -import { hideLoader, showLoader } from './loader.js'; +import { loader } from './action-loader.js'; import { convertCharacterToPersona } from './personas.js'; import { callGenericPopup, POPUP_TYPE } from './popup.js'; import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap, importTags, tag_import_setting } from './tags.js'; +import { t } from './i18n.js'; /** * Static object representing the actions of the @@ -846,15 +847,15 @@ class BulkEditOverlay { const deleteChats = checkbox.prop('checked') ?? false; - showLoader(); - const toast = toastr.info('We\'re deleting your characters, please wait...', 'Working on it'); + const loaderHandle = loader.show({ + title: t`Bulk Delete`, + message: t`Deleting ${characterIds.length} character(s)…`, + toastMode: loader.ToastMode.STATIC, + }); const avatarList = characterIds.map(id => characters[id]?.avatar).filter(a => a); return CharacterContextMenu.delete(avatarList, deleteChats) .then(() => this.browseState()) - .finally(() => { - toastr.clear(toast); - hideLoader(); - }); + .finally(() => loaderHandle.hide()); }); // At this moment the popup is already changed in the dom, but not yet closed/resolved. We build the avatar list here diff --git a/public/scripts/action-loader.js b/public/scripts/action-loader.js index b3df4b9d8..b31f3969b 100644 --- a/public/scripts/action-loader.js +++ b/public/scripts/action-loader.js @@ -1,16 +1,19 @@ /** - * Action loader utility - shows loader overlay with stoppable toast notification. + * Unified action loader system - shows loader overlay with optional toast notifications. * Designed to be flexible and reusable for various long-running operations. - * Supports stacking multiple loaders - overlay stays single, but toasts can stack. * - * With default arguments, will function as a generation loader / wrapper. + * Features: + * - Stacking multiple loaders - overlay stays single, but toasts can stack + * - Blocking and non-blocking modes + * - Stoppable or static toasts + * - Class-based handle system for fine-grained control * * @module action-loader */ import { t } from './i18n.js'; import { stopGeneration } from '../script.js'; -import { showLoader, hideLoader, isLoaderDisplayed } from './loader.js'; +import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; /** * Enum representing the toast display mode for the action loader. @@ -33,6 +36,7 @@ export const ActionLoaderToastMode = { * @property {string} [message='Generating...'] - The message to display in the toast * @property {string} [title] - Optional title for the toast notification * @property {string} [stopTooltip='Stop'] - Tooltip text for the stop button + * @property {HTMLElement|string|null} [overlayContent=null] - Custom content for the overlay (replaces default spinner) * @property {(() => void)|null} [onStop=null] - Custom stop handler. If null, calls `stopGeneration()` * @property {(() => void)|null} [onHide=null] - Custom hide handler. Called when the loader is hidden (not stopped). */ @@ -95,6 +99,7 @@ export class ActionLoaderHandle { * @param {string} [options.message='Generating...'] - Message to display in the toast * @param {string} [options.title] - Title for the toast notification * @param {string} [options.stopTooltip='Stop'] - Tooltip for the stop button + * @param {HTMLElement|string|null} [options.overlayContent] - Custom content for the overlay (replaces default spinner) * @param {(() => void)|null} [options.onStop] - Custom stop handler * @param {(() => void)|null} [options.onHide] - Custom hide handler */ @@ -104,6 +109,7 @@ export class ActionLoaderHandle { message = t`Generating...`, title = '', stopTooltip = t`Stop`, + overlayContent = null, onStop = null, onHide = null, } = {}) { @@ -113,13 +119,13 @@ export class ActionLoaderHandle { this.#onHide = onHide; // Warn if non-blocking loader has no toast - it won't be visible to the user - if (!blocking && toastMode === ActionLoaderToastMode.NONE) { + if (!blocking && toastMode === ActionLoaderToastMode.NONE && !overlayContent) { console.warn('[ActionLoader] Non-blocking loader created without a toast. This loader will not be visible to the user.'); } // Show the blocking loader overlay if this is the first blocking handle - if (blocking && !hasBlockingLoaders() && !isLoaderDisplayed()) { - showLoader(); + if (blocking && !hasBlockingLoaders() && !isOverlayDisplayed()) { + showOverlay(overlayContent); } // Register this handle @@ -191,7 +197,7 @@ export class ActionLoaderHandle { // Hide the overlay if this was the last blocking handle if (this.#blocking && !hasBlockingLoaders()) { - await hideLoader(); + await hideOverlay(); } } @@ -300,6 +306,12 @@ export const loader = { */ get: getLoaderHandleById, + /** + * Checks if any blocking loader overlay is currently displayed. + * @returns {boolean} True if a blocking overlay is shown + */ + isBlocking: isOverlayDisplayed, + /** * Toast display mode constants. * @type {typeof ActionLoaderToastMode} @@ -311,6 +323,12 @@ export const loader = { * @type {typeof ActionLoaderHandle} */ Handle: ActionLoaderHandle, + + /** + * Creates a fresh default loader overlay element. + * @type {typeof createDefaultLoaderOverlay} + */ + createOverlay: createDefaultLoaderOverlay, }; /** @@ -404,3 +422,145 @@ export function getLoaderHandleById(id) { } return undefined; } + +// ============================================================================ +// Internal overlay management +// ============================================================================ + +/** @type {Popup|null} The current loader overlay popup */ +let loaderPopup = null; + +/** Whether the initial HTML preloader has been removed */ +let preloaderYoinked = false; + +/** + * Creates the default loader overlay element. + * Always returns a fresh element instance. + * + * @returns {HTMLDivElement} A new loader overlay element + */ +export function createDefaultLoaderOverlay() { + const loaderElement = document.createElement('div'); + loaderElement.id = 'loader'; + + const spinnerElement = document.createElement('div'); + spinnerElement.id = 'load-spinner'; + spinnerElement.className = 'fa-solid fa-gear fa-spin fa-3x'; + + loaderElement.appendChild(spinnerElement); + + return loaderElement; +} + +/** + * Normalizes custom overlay content into a value supported by Popup. + * @param {string|HTMLElement|null} customContent - Custom overlay content + * @returns {string|HTMLElement} Content for Popup + */ +function getOverlayContent(customContent) { + if (typeof customContent === 'string') { + return customContent; + } + + if (customContent instanceof HTMLElement) { + return customContent; + } + + return createDefaultLoaderOverlay(); +} + +/** + * Checks if the loader overlay is currently displayed. + * @returns {boolean} True if overlay is shown + */ +function isOverlayDisplayed() { + return !!loaderPopup; +} + +/** + * Shows the blocking loader overlay. + * Internal function - use showActionLoader() instead. + * @param {HTMLElement|string|null} [customContent] - Custom content for the overlay + */ +function showOverlay(customContent = null) { + // Two loaders don't make sense. Don't await, we can overlay the old loader while it closes + if (loaderPopup) loaderPopup.complete(POPUP_RESULT.CANCELLED); + + const content = getOverlayContent(customContent); + + loaderPopup = new Popup(content, POPUP_TYPE.DISPLAY, null, { transparent: true, animation: 'none', wide: true, large: true }); + + // No close button, loaders are not closable + loaderPopup.closeButton.style.display = 'none'; + + loaderPopup.show(); +} + +/** + * Hides the blocking loader overlay with animation. + * Internal function - use hideActionLoader() instead. + * @returns {Promise} + */ +async function hideOverlay() { + if (!loaderPopup) { + return Promise.resolve(); + } + + return new Promise((resolve) => { + const loaderElement = $('#loader'); + const spinner = $('#load-spinner'); + + if (!loaderElement.length) { + console.warn('Loader element not found, skipping animation'); + cleanup(); + return; + } + + // Check if transitions are enabled on spinner (which has the transition property) + const transitionDuration = spinner.length && spinner[0] ? getComputedStyle(spinner[0]).transitionDuration : '0s'; + const hasTransitions = parseFloat(transitionDuration) > 0; + + if (hasTransitions) { + Promise.race([ + new Promise((r) => setTimeout(r, 500)), // Fallback timeout + new Promise((r) => loaderElement.one('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', r)), + ]).finally(cleanup); + } else { + cleanup(); + } + + function cleanup() { + loaderElement.remove(); + // Yoink preloader entirely; it only exists to cover up unstyled content while loading JS + // If it's present, we remove it once and then it's gone. + yoinkPreloader(); + + loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE) + .catch((err) => console.error('Error completing loaderPopup:', err)) + .finally(() => { + loaderPopup = null; + resolve(); + }); + } + + // Apply the blur styles to the entire loader element + loaderElement.css({ + 'filter': 'blur(15px)', + 'opacity': '0', + }); + }); +} + +/** + * Removes the initial HTML preloader element. + * Called once after the first loader hide. + */ +function yoinkPreloader() { + if (preloaderYoinked) return; + document.getElementById('preloader')?.remove(); + preloaderYoinked = true; +} + +// ============================================================================ +// End internal overlay management +// ============================================================================ diff --git a/public/scripts/bookmarks.js b/public/scripts/bookmarks.js index 38bca295d..e82aac347 100644 --- a/public/scripts/bookmarks.js +++ b/public/scripts/bookmarks.js @@ -25,7 +25,7 @@ import { saveGroupBookmarkChat, selected_group, } from './group-chats.js'; -import { hideLoader, showLoader } from './loader.js'; +import { loader } from './action-loader.js'; import { getLastMessageId } from './macros.js'; import { Popup } from './popup.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; @@ -661,15 +661,20 @@ export function initBookmarks() { return; } + const loaderHandle = loader.show({ + title: t`Chat History`, + message: t`Loading chat…`, + toastMode: loader.ToastMode.STATIC, + }); + try { - showLoader(); if (selected_group) { await openGroupChat(selected_group, fileName); } else { await openCharacterChat(fileName); } } finally { - await hideLoader(); + await loaderHandle.hide(); } $('#shadow_select_chat_popup').css('display', 'none'); diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 8a4ed84cf..3608772ad 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -1,7 +1,6 @@ import { DOMPurify, Popper } from '../lib.js'; import { eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration, CLIENT_VERSION } from '../script.js'; -import { showLoader } from './loader.js'; import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; import { renderTemplate, renderTemplateAsync } from './templates.js'; import { delay, equalsIgnoreCaseAndAccents, isSubsetOf, sanitizeSelector, setValueByPath, versionCompare } from './utils.js'; @@ -1158,7 +1157,6 @@ async function showExtensionsDetails() { abortController.abort(); } if (requiresReload) { - showLoader(); location.reload(); } } diff --git a/public/scripts/loader.js b/public/scripts/loader.js index ab6954d4f..a5a3706ac 100644 --- a/public/scripts/loader.js +++ b/public/scripts/loader.js @@ -1,80 +1,59 @@ -import { POPUP_RESULT, POPUP_TYPE, Popup } from './popup.js'; +import { loader } from './action-loader.js'; -/** @type {Popup} */ -let loaderPopup; - -let preloaderYoinked = false; - -export function isLoaderDisplayed() { - return !!loaderPopup; -} +/** + * Handle for the legacy loader created by showLoader(). + * @type {import('./action-loader.js').ActionLoaderHandle|null} + */ +let legacyLoaderHandle = null; +/** + * Shows the loader overlay. + * + * @deprecated Use `showActionLoader()` from action-loader.js instead. + * This function now creates a blocking action loader with no toast. + * The new system supports stacking multiple loaders and provides better control. + * + * @example + * // New recommended approach: + * import { showActionLoader } from './action-loader.js'; + * const handle = showActionLoader({ message: 'Loading...' }); + * // ... do work ... + * handle.hide(); + */ export function showLoader() { - // Two loaders don't make sense. Don't await, we can overlay the old loader while it closes - if (loaderPopup) loaderPopup.complete(POPUP_RESULT.CANCELLED); + // Hide any existing legacy loader first to maintain old behavior + if (legacyLoaderHandle && legacyLoaderHandle.isActive) { + legacyLoaderHandle.hide(); + } - loaderPopup = new Popup(` -
-
-
`, POPUP_TYPE.DISPLAY, null, { transparent: true, animation: 'none', wide: true, large: true }); - - // No close button, loaders are not closable - loaderPopup.closeButton.style.display = 'none'; - - loaderPopup.show(); + // Create a blocking loader with no toast (matches old behavior) + legacyLoaderHandle = loader.show({ + blocking: true, + toastMode: loader.ToastMode.NONE, + }); } +/** + * Hides the loader overlay. + * + * @deprecated Use `hideActionLoader()` or `handle.hide()` from action-loader.js instead. + * This function now hides the legacy loader created by showLoader(). + * + * @example + * // New recommended approach: + * import { showActionLoader } from './action-loader.js'; + * const handle = showActionLoader({ message: 'Loading...' }); + * // ... do work ... + * await handle.hide(); + * + * @returns {Promise} + */ export async function hideLoader() { - if (!loaderPopup) { + if (!legacyLoaderHandle || !legacyLoaderHandle.isActive) { console.warn('There is no loader showing to hide'); return Promise.resolve(); } - return new Promise((resolve) => { - const spinner = $('#load-spinner'); - if (!spinner.length) { - console.warn('Spinner element not found, skipping animation'); - cleanup(); - return; - } - - // Check if transitions are enabled - const transitionDuration = spinner[0] ? getComputedStyle(spinner[0]).transitionDuration : '0s'; - const hasTransitions = parseFloat(transitionDuration) > 0; - - if (hasTransitions) { - Promise.race([ - new Promise((r) => setTimeout(r, 500)), // Fallback timeout - new Promise((r) => spinner.one('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', r)), - ]).finally(cleanup); - } else { - cleanup(); - } - - function cleanup() { - $('#loader').remove(); - // Yoink preloader entirely; it only exists to cover up unstyled content while loading JS - // If it's present, we remove it once and then it's gone. - yoinkPreloader(); - - loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE) - .catch((err) => console.error('Error completing loaderPopup:', err)) - .finally(() => { - loaderPopup = null; - resolve(); - }); - } - - // Apply the styles - spinner.css({ - 'filter': 'blur(15px)', - 'opacity': '0', - }); - }); -} - -function yoinkPreloader() { - if (preloaderYoinked) return; - document.getElementById('preloader').remove(); - preloaderYoinked = true; + await legacyLoaderHandle.hide(); + legacyLoaderHandle = null; } diff --git a/public/scripts/st-context.js b/public/scripts/st-context.js index 1e33ed972..21eba82ca 100644 --- a/public/scripts/st-context.js +++ b/public/scripts/st-context.js @@ -189,7 +189,9 @@ export function getContext() { /** @deprecated Use callGenericPopup or Popup instead. */ callPopup, callGenericPopup, + /** @deprecated Use loader.show instead. */ showLoader, + /** @deprecated Use loader.hide instead. */ hideLoader, mainApi: main_api, extensionSettings: extension_settings, diff --git a/public/scripts/templates/welcomePanel.html b/public/scripts/templates/welcomePanel.html index 7f9d9ab13..809eeefe8 100644 --- a/public/scripts/templates/welcomePanel.html +++ b/public/scripts/templates/welcomePanel.html @@ -1,6 +1,6 @@
- + {{version}}