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>

45009cd0e4e5df98c522f6efc5dde76dc7e3def1

Wolfsblvt <wolfsblvt@gmail.com>

Signed
25 files changed, +493 -122Showing whitespace changes
public/css/loader.css+33 -0
@@ -53,3 +53,36 @@
5353 opacity: 1;
5454 color: color-mix(in srgb, currentColor 40%, #e74c3c 60%);
5555}
56+
57+/* Splash screen styles for branded loading */
58+#loader.splash-screen {
59+ display: flex;
60+ flex-direction: column;
61+ align-items: center;
62+ justify-content: center;
63+ gap: 4rem;
64+ width: 100%;
65+ height: 100%;
66+ transition: all var(--animation-duration-2x) ease-out;
67+}
68+
69+#loader.splash-screen #load-spinner {
70+ position: static;
71+ top: unset;
72+ left: unset;
73+}
74+
75+.splash-logo {
76+ /* max original px, or 50% on mobile */
77+ width: min(150px, 50%);
78+ height: auto;
79+ filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
80+}
81+
82+.splash-message {
83+ margin: 0;
84+ font-size: 1.25rem;
85+ font-weight: 500;
86+ opacity: 0.9;
87+ letter-spacing: 0.02em;
88+}
public/locales/ar-sa.json+11 -1
@@ -1444,5 +1444,15 @@
14441444 "Interleaved Thinking": "التفكير المتداخل",
14451445 "Since Last User Message": "منذ آخر رسالة من المستخدم",
14461446 "Active Tool Chain": "سلسلة الأدوات النشطة",
14471447 "openrouter_interleaved_thinking_hint": "يرسل الاستدلال من رسائل المساعد السابقة مع طلبات استدعاء الأدوات للحفاظ على سياق التفكير المتداخل.",
1448+ "SillyTavern Logo": "شعار SillyTavern",
1449+ "Initializing…": "جارٍ التهيئة…",
1450+ "Name not accepted, as it is the same as before (ignoring case and accents).": "الاسم غير مقبول، لأنه نفسه كما كان من قبل (تجاهل حالة الأحرف والنبرات).",
1451+ "Rename Chat": "إعادة تسمية المحادثة",
1452+ "Renaming chat…": "جارٍ إعادة تسمية المحادثة…",
1453+ "Delete Chat": "حذف المحادثة",
1454+ "Deleting chat…": "جارٍ حذف المحادثة…",
1455+ "Loading chat…": "جارٍ تحميل المحادثة…",
1456+ "Bulk Delete": "حذف جماعي",
1457+ "Deleting ${0} character(s)…": "جارٍ حذف ${0} شخصية (شخصيات)…"
14481458}
public/locales/de-de.json+11 -1
@@ -1444,5 +1444,15 @@
14441444 "Interleaved Thinking": "Verschränktes Denken",
14451445 "Since Last User Message": "Seit letzter Nutzernachricht",
14461446 "Active Tool Chain": "Aktive Tool-Kette",
14471447 "openrouter_interleaved_thinking_hint": "Sendet Begründungen aus vorherigen Assistentennachrichten mit Tool-Aufrufen, um den Kontext für verschränktes Denken zu erhalten.",
1448+ "SillyTavern Logo": "SillyTavern-Logo",
1449+ "Initializing…": "Initialisierung…",
1450+ "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).",
1451+ "Rename Chat": "Chat umbenennen",
1452+ "Renaming chat…": "Chat wird umbenannt…",
1453+ "Delete Chat": "Chat löschen",
1454+ "Deleting chat…": "Chat wird gelöscht…",
1455+ "Loading chat…": "Chat wird geladen…",
1456+ "Bulk Delete": "Massenlöschung",
1457+ "Deleting ${0} character(s)…": "${0} Charakter(e) werden gelöscht…"
14481458}
public/locales/es-es.json+11 -1
@@ -1551,5 +1551,15 @@
15511551 "Interleaved Thinking": "Razonamiento intercalado",
15521552 "Since Last User Message": "Desde el último mensaje del usuario",
15531553 "Active Tool Chain": "Cadena de herramientas activa",
15541554 "openrouter_interleaved_thinking_hint": "Envía razonamiento de turnos anteriores del asistente junto con solicitudes de herramientas para mantener el contexto del razonamiento intercalado.",
1555+ "SillyTavern Logo": "Logo de SillyTavern",
1556+ "Initializing…": "Inicializando…",
1557+ "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).",
1558+ "Rename Chat": "Renombrar chat",
1559+ "Renaming chat…": "Renombrando chat…",
1560+ "Delete Chat": "Eliminar chat",
1561+ "Deleting chat…": "Eliminando chat…",
1562+ "Loading chat…": "Cargando chat…",
1563+ "Bulk Delete": "Eliminación masiva",
1564+ "Deleting ${0} character(s)…": "Eliminando ${0} personaje(s)…"
15551565}
public/locales/fr-fr.json+11 -1
@@ -2052,5 +2052,15 @@
20522052 "Interleaved Thinking": "Raisonnement intercalé",
20532053 "Since Last User Message": "Depuis le dernier message utilisateur",
20542054 "Active Tool Chain": "Chaîne d'outils active",
20552055 "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é.",
2056+ "SillyTavern Logo": "Logo SillyTavern",
2057+ "Initializing…": "Initialisation…",
2058+ "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).",
2059+ "Rename Chat": "Renommer la discussion",
2060+ "Renaming chat…": "Renommage de la discussion…",
2061+ "Delete Chat": "Supprimer la discussion",
2062+ "Deleting chat…": "Suppression de la discussion…",
2063+ "Loading chat…": "Chargement de la discussion…",
2064+ "Bulk Delete": "Suppression groupée",
2065+ "Deleting ${0} character(s)…": "Suppression de ${0} personnage(s)…"
20562066}
public/locales/is-is.json+11 -1
@@ -1442,5 +1442,15 @@
14421442 "Interleaved Thinking": "Fléttuð hugsun",
14431443 "Since Last User Message": "Frá síðustu notandaskilaboðum",
14441444 "Active Tool Chain": "Virk verkfærakeðja",
14451445 "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.",
1446+ "SillyTavern Logo": "SillyTavern merki",
1447+ "Initializing…": "Ræsir…",
1448+ "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).",
1449+ "Rename Chat": "Endurnefna spjall",
1450+ "Renaming chat…": "Endurnefni spjall…",
1451+ "Delete Chat": "Eyða spjalli",
1452+ "Deleting chat…": "Eyði spjalli…",
1453+ "Loading chat…": "Hleð spjalli…",
1454+ "Bulk Delete": "Fjöldaeyðing",
1455+ "Deleting ${0} character(s)…": "Eyði ${0} persónu(m)…"
14461456}
public/locales/it-it.json+11 -1
@@ -1444,5 +1444,15 @@
14441444 "Interleaved Thinking": "Ragionamento intercalato",
14451445 "Since Last User Message": "Dall'ultimo messaggio utente",
14461446 "Active Tool Chain": "Catena di strumenti attiva",
14471447 "openrouter_interleaved_thinking_hint": "Invia il ragionamento dai turni precedenti dell'assistente con le richieste di strumenti per mantenere il contesto del ragionamento intercalato.",
1448+ "SillyTavern Logo": "Logo SillyTavern",
1449+ "Initializing…": "Inizializzazione…",
1450+ "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).",
1451+ "Rename Chat": "Rinomina chat",
1452+ "Renaming chat…": "Rinominazione chat…",
1453+ "Delete Chat": "Elimina chat",
1454+ "Deleting chat…": "Eliminazione chat…",
1455+ "Loading chat…": "Caricamento chat…",
1456+ "Bulk Delete": "Eliminazione multipla",
1457+ "Deleting ${0} character(s)…": "Eliminazione di ${0} personaggio/i…"
14481458}
public/locales/ja-jp.json+11 -1
@@ -1449,5 +1449,15 @@
14491449 "Interleaved Thinking": "インターリーブ思考",
14501450 "Since Last User Message": "直前のユーザーメッセージ以降",
14511451 "Active Tool Chain": "アクティブなツールチェーン",
14521452 "openrouter_interleaved_thinking_hint": "インターリーブ思考の文脈を維持するため、直前のアシスタントターンの推論をツール呼び出しリクエストとともに送信します。",
1453+ "SillyTavern Logo": "SillyTavernロゴ",
1454+ "Initializing…": "初期化中…",
1455+ "Name not accepted, as it is the same as before (ignoring case and accents).": "名前が受け入れられませんでした。以前と同じです(大文字小文字とアクセントを無視)。",
1456+ "Rename Chat": "チャット名を変更",
1457+ "Renaming chat…": "チャット名を変更中…",
1458+ "Delete Chat": "チャットを削除",
1459+ "Deleting chat…": "チャットを削除中…",
1460+ "Loading chat…": "チャットを読み込み中…",
1461+ "Bulk Delete": "一括削除",
1462+ "Deleting ${0} character(s)…": "${0}人のキャラクターを削除中…"
14531463}
public/locales/ko-kr.json+11 -1
@@ -1623,5 +1623,15 @@
16231623 "Interleaved Thinking": "인터리브드 사고",
16241624 "Since Last User Message": "마지막 사용자 메시지 이후",
16251625 "Active Tool Chain": "활성 도구 체인",
16261626 "openrouter_interleaved_thinking_hint": "인터리브드 사고 컨텍스트를 유지하기 위해 이전 어시스턴트 턴의 추론을 도구 호출 요청과 함께 전송합니다.",
1627+ "SillyTavern Logo": "SillyTavern 로고",
1628+ "Initializing…": "초기화 중…",
1629+ "Name not accepted, as it is the same as before (ignoring case and accents).": "이름이 승인되지 않았습니다. 이전과 동일합니다(대소문자 및 악센트 무시).",
1630+ "Rename Chat": "채팅 이름 변경",
1631+ "Renaming chat…": "채팅 이름 변경 중…",
1632+ "Delete Chat": "채팅 삭제",
1633+ "Deleting chat…": "채팅 삭제 중…",
1634+ "Loading chat…": "채팅 불러오는 중…",
1635+ "Bulk Delete": "대량 삭제",
1636+ "Deleting ${0} character(s)…": "${0}개 캐릭터 삭제 중…"
16271637}
public/locales/nl-nl.json+11 -1
@@ -1440,5 +1440,15 @@
14401440 "Interleaved Thinking": "Verweven redenering",
14411441 "Since Last User Message": "Sinds laatste gebruikersbericht",
14421442 "Active Tool Chain": "Actieve toolketen",
14431443 "openrouter_interleaved_thinking_hint": "Verstuurt redenering uit eerdere assistentbeurten met tool-aanvragen om de context van verweven redenering te behouden.",
1444+ "SillyTavern Logo": "SillyTavern-logo",
1445+ "Initializing…": "Initialiseren…",
1446+ "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).",
1447+ "Rename Chat": "Chat hernoemen",
1448+ "Renaming chat…": "Chat hernoemen…",
1449+ "Delete Chat": "Chat verwijderen",
1450+ "Deleting chat…": "Chat verwijderen…",
1451+ "Loading chat…": "Chat laden…",
1452+ "Bulk Delete": "Bulkverwijdering",
1453+ "Deleting ${0} character(s)…": "${0} personage(s) verwijderen…"
14441454}
public/locales/pt-pt.json+11 -1
@@ -1442,5 +1442,15 @@
14421442 "Interleaved Thinking": "Raciocínio intercalado",
14431443 "Since Last User Message": "Desde a última mensagem do utilizador",
14441444 "Active Tool Chain": "Cadeia de ferramentas ativa",
14451445 "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.",
1446+ "SillyTavern Logo": "Logótipo do SillyTavern",
1447+ "Initializing…": "A inicializar…",
1448+ "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).",
1449+ "Rename Chat": "Renomear chat",
1450+ "Renaming chat…": "A renomear chat…",
1451+ "Delete Chat": "Eliminar chat",
1452+ "Deleting chat…": "A eliminar chat…",
1453+ "Loading chat…": "A carregar chat…",
1454+ "Bulk Delete": "Eliminação em massa",
1455+ "Deleting ${0} character(s)…": "A eliminar ${0} personagem(ns)…"
14461456}
public/locales/ru-ru.json+11 -1
@@ -2744,5 +2744,15 @@
27442744 "Interleaved Thinking": "Чередующееся рассуждение",
27452745 "Since Last User Message": "С момента последнего сообщения пользователя",
27462746 "Active Tool Chain": "Активная цепочка инструментов",
27472747 "openrouter_interleaved_thinking_hint": "Отправляет рассуждение из предыдущих ходов ассистента вместе с запросами вызова инструментов, чтобы сохранять контекст чередующегося рассуждения.",
2748+ "SillyTavern Logo": "Логотип SillyTavern",
2749+ "Initializing…": "Инициализация…",
2750+ "Name not accepted, as it is the same as before (ignoring case and accents).": "Имя не принято, так как оно совпадает с предыдущим (игнорируя регистр и акценты).",
2751+ "Rename Chat": "Переименовать чат",
2752+ "Renaming chat…": "Переименование чата…",
2753+ "Delete Chat": "Удалить чат",
2754+ "Deleting chat…": "Удаление чата…",
2755+ "Loading chat…": "Загрузка чата…",
2756+ "Bulk Delete": "Массовое удаление",
2757+ "Deleting ${0} character(s)…": "Удаление ${0} персонажа(ей)…"
27482758}
public/locales/th-th.json+11 -1
@@ -1461,5 +1461,15 @@
14611461 "Interleaved Thinking": "การคิดแบบสลับขั้น",
14621462 "Since Last User Message": "ตั้งแต่ข้อความล่าสุดของผู้ใช้",
14631463 "Active Tool Chain": "สายโซ่เครื่องมือที่ใช้งานอยู่",
14641464 "openrouter_interleaved_thinking_hint": "ส่งเหตุผลจากเทิร์นก่อนหน้าของผู้ช่วยไปพร้อมกับคำขอเรียกใช้เครื่องมือ เพื่อคงบริบทการคิดแบบสลับขั้นไว้",
1465+ "SillyTavern Logo": "โลโก้ SillyTavern",
1466+ "Initializing…": "กำลังเริ่มต้น…",
1467+ "Name not accepted, as it is the same as before (ignoring case and accents).": "ชื่อไม่ได้รับการยอมรับ เนื่องจากเหมือนกับก่อนหน้านี้ (ไม่สนใจตัวพิมพ์ใหญ่เล็กและเครื่องหมายเสียง)",
1468+ "Rename Chat": "เปลี่ยนชื่อแชท",
1469+ "Renaming chat…": "กำลังเปลี่ยนชื่อแชท…",
1470+ "Delete Chat": "ลบแชท",
1471+ "Deleting chat…": "กำลังลบแชท…",
1472+ "Loading chat…": "กำลังโหลดแชท…",
1473+ "Bulk Delete": "ลบจำนวนมาก",
1474+ "Deleting ${0} character(s)…": "กำลังลบ ${0} ตัวละคร…"
14651475}
public/locales/uk-ua.json+11 -1
@@ -1442,5 +1442,15 @@
14421442 "Interleaved Thinking": "Черговане мислення",
14431443 "Since Last User Message": "Від останнього повідомлення користувача",
14441444 "Active Tool Chain": "Активний ланцюжок інструментів",
14451445 "openrouter_interleaved_thinking_hint": "Надсилає міркування з попередніх ходів асистента разом із запитами виклику інструментів, щоб зберегти контекст чергованого мислення.",
1446+ "SillyTavern Logo": "Логотип SillyTavern",
1447+ "Initializing…": "Ініціалізація…",
1448+ "Name not accepted, as it is the same as before (ignoring case and accents).": "Ім'я не прийнято, оскільки воно збігається з попереднім (ігноруючи регістр та акценти).",
1449+ "Rename Chat": "Перейменувати чат",
1450+ "Renaming chat…": "Перейменування чату…",
1451+ "Delete Chat": "Видалити чат",
1452+ "Deleting chat…": "Видалення чату…",
1453+ "Loading chat…": "Завантаження чату…",
1454+ "Bulk Delete": "Масове видалення",
1455+ "Deleting ${0} character(s)…": "Видалення ${0} персонажа(ів)…"
14461456}
public/locales/vi-vn.json+11 -1
@@ -1442,5 +1442,15 @@
14421442 "Interleaved Thinking": "Lập luận xen kẽ",
14431443 "Since Last User Message": "Kể từ tin nhắn người dùng gần nhất",
14441444 "Active Tool Chain": "Chuỗi công cụ đang hoạt động",
14451445 "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ẽ.",
1446+ "SillyTavern Logo": "Logo SillyTavern",
1447+ "Initializing…": "Đang khởi tạo…",
1448+ "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).",
1449+ "Rename Chat": "Đổi tên cuộc trò chuyện",
1450+ "Renaming chat…": "Đang đổi tên cuộc trò chuyện…",
1451+ "Delete Chat": "Xóa cuộc trò chuyện",
1452+ "Deleting chat…": "Đang xóa cuộc trò chuyện…",
1453+ "Loading chat…": "Đang tải cuộc trò chuyện…",
1454+ "Bulk Delete": "Xóa hàng loạt",
1455+ "Deleting ${0} character(s)…": "Đang xóa ${0} nhân vật…"
14461456}
public/locales/zh-cn.json+9 -1
@@ -3635,5 +3635,13 @@
36353635 "sd_prompt_9": "你的人设 (多模态模式)",
36363636 "Show only favorites": "仅显示收藏",
36373637 "Show only groups": "仅显示群聊",
36383638 "Show Tag List": "显示标签列表",
3639+ "SillyTavern Logo": "SillyTavern 徽标",
3640+ "Initializing…": "正在初始化…",
3641+ "Renaming chat…": "正在重命名聊天…",
3642+ "Delete Chat": "删除聊天",
3643+ "Deleting chat…": "正在删除聊天…",
3644+ "Loading chat…": "正在加载聊天…",
3645+ "Bulk Delete": "批量删除",
3646+ "Deleting ${0} character(s)…": "正在删除 ${0} 个角色…"
36393647}
public/locales/zh-tw.json+11 -1
@@ -2764,5 +2764,15 @@
27642764 "Interleaved Thinking": "交錯思維",
27652765 "Since Last User Message": "自上一則使用者訊息起",
27662766 "Active Tool Chain": "使用中的工具鏈",
27672767 "openrouter_interleaved_thinking_hint": "會在工具呼叫請求中附帶先前助理回合的推理,以維持交錯思維的上下文。",
2768+ "SillyTavern Logo": "SillyTavern 標誌",
2769+ "Initializing…": "正在初始化…",
2770+ "Name not accepted, as it is the same as before (ignoring case and accents).": "名稱不被接受,因為它與之前相同(忽略大小寫和重音)。",
2771+ "Rename Chat": "重新命名聊天",
2772+ "Renaming chat…": "正在重新命名聊天…",
2773+ "Delete Chat": "刪除聊天",
2774+ "Deleting chat…": "正在刪除聊天…",
2775+ "Loading chat…": "正在載入聊天…",
2776+ "Bulk Delete": "批次刪除",
2777+ "Deleting ${0} character(s)…": "正在刪除 ${0} 個角色…"
27682778}
public/script.js+50 -15
@@ -244,7 +244,7 @@ import {
244244 isPersonaPanelOpen,
245245} from './scripts/personas.js';
246246import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';
247247import { hideLoader, showLoaderloader } from './scripts/action-loader.js';
248248import { BulkEditOverlay } from './scripts/BulkEditOverlay.js';
249249import { initTextGenModels } from './scripts/textgen-models.js';
250250import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, preserveNeutralChat, restoreNeutralChat, formatCreatorNotes, initChatUtilities, addDOMPurifyHooks } from './scripts/chats.js';
@@ -698,7 +698,28 @@ async function firstLoadInit() {
698698 throw new Error('Initialization failed');
699699 }
700700
701- showLoader();
701+ const initLoaderOverlay = loader.createOverlay();
702+ initLoaderOverlay.classList.add('splash-screen');
703+
704+ const splashLogo = document.createElement('img');
705+ splashLogo.src = '/img/logo.png';
706+ splashLogo.alt = 'SillyTavern';
707+ splashLogo.className = 'splash-logo';
708+ splashLogo.ariaLabel = t`SillyTavern Logo`;
709+
710+ const splashMessage = document.createElement('h2');
711+ splashMessage.className = 'splash-message';
712+ splashMessage.textContent = t`Initializing…`;
713+ splashMessage.dataset.i18n = 'Initializing…';
714+
715+ initLoaderOverlay.prepend(splashLogo);
716+ initLoaderOverlay.appendChild(splashMessage);
717+
718+ const initLoaderHandle = loader.show({
719+ toastMode: loader.ToastMode.NONE,
720+ overlayContent: initLoaderOverlay,
721+ });
722+
702723 registerPromptManagerMigration();
703724 initDomHandlers();
704725 initStandaloneMode();
@@ -724,7 +745,7 @@ async function firstLoadInit() {
724745 ToolManager.initToolSlashCommands();
725746 await initPresetManager();
726747 await initSystemMessages();
727748 await getSettings(initLoaderHandle);
728749 initKeyboard();
729750 initDynamicStyles();
730751 initTags();
@@ -758,7 +779,7 @@ async function firstLoadInit() {
758779 addDebugFunctions();
759780 doDailyExtensionUpdatesCheck();
760781 await eventSource.emit(event_types.APP_INITIALIZED);
761782 await hideLoaderinitLoaderHandle.hide();
762783 await fixViewport();
763784 await eventSource.emit(event_types.APP_READY);
764785}
@@ -7763,7 +7784,7 @@ function reloadLoop() {
77637784
77647785//MARK: getSettings()
77657786///////////////////////////////////////////
77667787export async function getSettings(initLoaderHandle = null) {
77677788 const response = await fetch('/api/settings/get', {
77687789 method: 'POST',
77697790 headers: getRequestHeaders(),
@@ -7881,7 +7902,7 @@ export async function getSettings() {
78817902 firstRun = !!settings.firstRun;
78827903
78837904 if (firstRun) {
78847905 hideLoaderawait initLoaderHandle?.hide();
78857906 await doOnboarding(user_avatar);
78867907 firstRun = false;
78877908 }
@@ -10457,7 +10478,7 @@ export async function doNewChat({ deleteCurrentChat = false } = {}) {
1045710478 * @param {string} param.newFileName New name for the chat (no JSONL extension)
1045810479 * @param {boolean} [param.loader=true] Whether to show loader during the operation
1045910480 */
1046010481export async function renameGroupOrCharacterChat({ characterId, groupId, oldFileName, newFileName, loader: showLoader }) {
1046110482 const currentChatId = getCurrentChatId();
1046210483 const body = {
1046310484 is_group: !!groupId,
@@ -10475,9 +10496,13 @@ export async function renameGroupOrCharacterChat({ characterId, groupId, oldFile
1047510496 return;
1047610497 }
1047710498
10478- try {
10499+ const loaderHandle = showLoader ? loader.show({
10479- loader && showLoader();
10500+ title: t`Rename Chat`,
10501+ message: t`Renaming chat…`,
10502+ toastMode: loader.ToastMode.STATIC,
10503+ }) : null;
1048010504
10505+ try {
1048110506 const response = await fetch('/api/chats/rename', {
1048210507 method: 'POST',
1048310508 body: JSON.stringify(body),
@@ -10510,11 +10535,10 @@ export async function renameGroupOrCharacterChat({ characterId, groupId, oldFile
1051010535 await reloadCurrentChat();
1051110536 }
1051210537 } catch {
10513- loader && hideLoader();
1051410538 await delay(500);
1051510539 await callGenericPopup('An error has occurred. Chat was not renamed.', POPUP_TYPE.TEXT);
1051610540 } finally {
1051710541 loader &&await hideLoaderloaderHandle?.hide();
1051810542 }
1051910543}
1052010544
@@ -11072,21 +11096,32 @@ jQuery(async function () {
1107211096 async function handleDeleteChat(chatFile, group, fromSlashCommand = false) {
1107311097 // Close past chat popup.
1107411098 $('#select_chat_cross').trigger('click');
11075- showLoader();
11099+
11100+ const loaderHandle = loader.show({
11101+ title: t`Delete Chat`,
11102+ message: t`Deleting chat…`,
11103+ toastMode: loader.ToastMode.STATIC,
11104+ });
11105+
11106+ try {
1107611107 if (group) {
1107711108 await deleteGroupChat(group, chatFile);
1107811109 } else {
1107911110 await delChat(`${chatFile}.jsonl`);
1108011111 }
11112+ } catch (error) {
11113+ loaderHandle.hide();
11114+ throw error;
11115+ }
1108111116
1108211117 if (fromSlashCommand) { // When called from `/delchat` command, don't re-open the history view.
1108311118 $('#options').hide(); // Hide option popup menu.
1108411119 hideLoaderawait loaderHandle.hide();
1108511120 } else { // Open the history view again after 2 seconds (delay to avoid edge cases for deleting last chat).
1108611121 setTimeout(async function () {
1108711122 $('#option_select_chat').trigger('click');
1108811123 $('#options').hide(); // Hide option popup menu.
1108911124 hideLoaderawait loaderHandle.hide();
1109011125 }, 2000);
1109111126 }
1109211127 }
public/scripts/BulkEditOverlay.js+8 -7
@@ -14,10 +14,11 @@ import {
1414} from '../script.js';
1515
1616import { favsToHotswap } from './RossAscends-mods.js';
1717import { hideLoader, showLoaderloader } from './action-loader.js';
1818import { convertCharacterToPersona } from './personas.js';
1919import { callGenericPopup, POPUP_TYPE } from './popup.js';
2020import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap, importTags, tag_import_setting } from './tags.js';
21+import { t } from './i18n.js';
2122
2223/**
2324 * Static object representing the actions of the
@@ -846,15 +847,15 @@ class BulkEditOverlay {
846847
847848 const deleteChats = checkbox.prop('checked') ?? false;
848849
849- showLoader();
850+ const loaderHandle = loader.show({
850- const toast = toastr.info('We\'re deleting your characters, please wait...', 'Working on it');
851+ title: t`Bulk Delete`,
852+ message: t`Deleting ${characterIds.length} character(s)…`,
853+ toastMode: loader.ToastMode.STATIC,
854+ });
851855 const avatarList = characterIds.map(id => characters[id]?.avatar).filter(a => a);
852856 return CharacterContextMenu.delete(avatarList, deleteChats)
853857 .then(() => this.browseState())
854858 .finally(() => {loaderHandle.hide());
855- toastr.clear(toast);
856- hideLoader();
857- });
858859 });
859860
860861 // At this moment the popup is already changed in the dom, but not yet closed/resolved. We build the avatar list here
public/scripts/action-loader.js+168 -8
@@ -1,16 +1,19 @@
11/**
22 * ActionUnified action loader utilitysystem - shows loader overlay with stoppableoptional toast notificationnotifications.
33 * Designed to be flexible and reusable for various long-running operations.
4- * Supports stacking multiple loaders - overlay stays single, but toasts can stack.
54 *
6- * With default arguments, will function as a generation loader / wrapper.
5+ * Features:
6+ * - Stacking multiple loaders - overlay stays single, but toasts can stack
7+ * - Blocking and non-blocking modes
8+ * - Stoppable or static toasts
9+ * - Class-based handle system for fine-grained control
710 *
811 * @module action-loader
912 */
1013
1114import { t } from './i18n.js';
1215import { stopGeneration } from '../script.js';
1316import { showLoaderPopup, hideLoaderPOPUP_RESULT, isLoaderDisplayedPOPUP_TYPE } from './loaderpopup.js';
1417
1518/**
1619 * Enum representing the toast display mode for the action loader.
@@ -33,6 +36,7 @@ export const ActionLoaderToastMode = {
3336 * @property {string} [message='Generating...'] - The message to display in the toast
3437 * @property {string} [title] - Optional title for the toast notification
3538 * @property {string} [stopTooltip='Stop'] - Tooltip text for the stop button
39+ * @property {HTMLElement|string|null} [overlayContent=null] - Custom content for the overlay (replaces default spinner)
3640 * @property {(() => void)|null} [onStop=null] - Custom stop handler. If null, calls `stopGeneration()`
3741 * @property {(() => void)|null} [onHide=null] - Custom hide handler. Called when the loader is hidden (not stopped).
3842 */
@@ -95,6 +99,7 @@ export class ActionLoaderHandle {
9599 * @param {string} [options.message='Generating...'] - Message to display in the toast
96100 * @param {string} [options.title] - Title for the toast notification
97101 * @param {string} [options.stopTooltip='Stop'] - Tooltip for the stop button
102+ * @param {HTMLElement|string|null} [options.overlayContent] - Custom content for the overlay (replaces default spinner)
98103 * @param {(() => void)|null} [options.onStop] - Custom stop handler
99104 * @param {(() => void)|null} [options.onHide] - Custom hide handler
100105 */
@@ -104,6 +109,7 @@ export class ActionLoaderHandle {
104109 message = t`Generating...`,
105110 title = '',
106111 stopTooltip = t`Stop`,
112+ overlayContent = null,
107113 onStop = null,
108114 onHide = null,
109115 } = {}) {
@@ -113,13 +119,13 @@ export class ActionLoaderHandle {
113119 this.#onHide = onHide;
114120
115121 // Warn if non-blocking loader has no toast - it won't be visible to the user
116122 if (!blocking && toastMode === ActionLoaderToastMode.NONE && !overlayContent) {
117123 console.warn('[ActionLoader] Non-blocking loader created without a toast. This loader will not be visible to the user.');
118124 }
119125
120126 // Show the blocking loader overlay if this is the first blocking handle
121127 if (blocking && !hasBlockingLoaders() && !isLoaderDisplayedisOverlayDisplayed()) {
122128 showLoadershowOverlay(overlayContent);
123129 }
124130
125131 // Register this handle
@@ -191,7 +197,7 @@ export class ActionLoaderHandle {
191197
192198 // Hide the overlay if this was the last blocking handle
193199 if (this.#blocking && !hasBlockingLoaders()) {
194200 await hideLoaderhideOverlay();
195201 }
196202 }
197203
@@ -301,6 +307,12 @@ export const loader = {
301307 get: getLoaderHandleById,
302308
303309 /**
310+ * Checks if any blocking loader overlay is currently displayed.
311+ * @returns {boolean} True if a blocking overlay is shown
312+ */
313+ isBlocking: isOverlayDisplayed,
314+
315+ /**
304316 * Toast display mode constants.
305317 * @type {typeof ActionLoaderToastMode}
306318 */
@@ -311,6 +323,12 @@ export const loader = {
311323 * @type {typeof ActionLoaderHandle}
312324 */
313325 Handle: ActionLoaderHandle,
326+
327+ /**
328+ * Creates a fresh default loader overlay element.
329+ * @type {typeof createDefaultLoaderOverlay}
330+ */
331+ createOverlay: createDefaultLoaderOverlay,
314332};
315333
316334/**
@@ -404,3 +422,145 @@ export function getLoaderHandleById(id) {
404422 }
405423 return undefined;
406424}
425+
426+// ============================================================================
427+// Internal overlay management
428+// ============================================================================
429+
430+/** @type {Popup|null} The current loader overlay popup */
431+let loaderPopup = null;
432+
433+/** Whether the initial HTML preloader has been removed */
434+let preloaderYoinked = false;
435+
436+/**
437+ * Creates the default loader overlay element.
438+ * Always returns a fresh element instance.
439+ *
440+ * @returns {HTMLDivElement} A new loader overlay element
441+ */
442+export function createDefaultLoaderOverlay() {
443+ const loaderElement = document.createElement('div');
444+ loaderElement.id = 'loader';
445+
446+ const spinnerElement = document.createElement('div');
447+ spinnerElement.id = 'load-spinner';
448+ spinnerElement.className = 'fa-solid fa-gear fa-spin fa-3x';
449+
450+ loaderElement.appendChild(spinnerElement);
451+
452+ return loaderElement;
453+}
454+
455+/**
456+ * Normalizes custom overlay content into a value supported by Popup.
457+ * @param {string|HTMLElement|null} customContent - Custom overlay content
458+ * @returns {string|HTMLElement} Content for Popup
459+ */
460+function getOverlayContent(customContent) {
461+ if (typeof customContent === 'string') {
462+ return customContent;
463+ }
464+
465+ if (customContent instanceof HTMLElement) {
466+ return customContent;
467+ }
468+
469+ return createDefaultLoaderOverlay();
470+}
471+
472+/**
473+ * Checks if the loader overlay is currently displayed.
474+ * @returns {boolean} True if overlay is shown
475+ */
476+function isOverlayDisplayed() {
477+ return !!loaderPopup;
478+}
479+
480+/**
481+ * Shows the blocking loader overlay.
482+ * Internal function - use showActionLoader() instead.
483+ * @param {HTMLElement|string|null} [customContent] - Custom content for the overlay
484+ */
485+function showOverlay(customContent = null) {
486+ // Two loaders don't make sense. Don't await, we can overlay the old loader while it closes
487+ if (loaderPopup) loaderPopup.complete(POPUP_RESULT.CANCELLED);
488+
489+ const content = getOverlayContent(customContent);
490+
491+ loaderPopup = new Popup(content, POPUP_TYPE.DISPLAY, null, { transparent: true, animation: 'none', wide: true, large: true });
492+
493+ // No close button, loaders are not closable
494+ loaderPopup.closeButton.style.display = 'none';
495+
496+ loaderPopup.show();
497+}
498+
499+/**
500+ * Hides the blocking loader overlay with animation.
501+ * Internal function - use hideActionLoader() instead.
502+ * @returns {Promise<void>}
503+ */
504+async function hideOverlay() {
505+ if (!loaderPopup) {
506+ return Promise.resolve();
507+ }
508+
509+ return new Promise((resolve) => {
510+ const loaderElement = $('#loader');
511+ const spinner = $('#load-spinner');
512+
513+ if (!loaderElement.length) {
514+ console.warn('Loader element not found, skipping animation');
515+ cleanup();
516+ return;
517+ }
518+
519+ // Check if transitions are enabled on spinner (which has the transition property)
520+ const transitionDuration = spinner.length && spinner[0] ? getComputedStyle(spinner[0]).transitionDuration : '0s';
521+ const hasTransitions = parseFloat(transitionDuration) > 0;
522+
523+ if (hasTransitions) {
524+ Promise.race([
525+ new Promise((r) => setTimeout(r, 500)), // Fallback timeout
526+ new Promise((r) => loaderElement.one('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', r)),
527+ ]).finally(cleanup);
528+ } else {
529+ cleanup();
530+ }
531+
532+ function cleanup() {
533+ loaderElement.remove();
534+ // Yoink preloader entirely; it only exists to cover up unstyled content while loading JS
535+ // If it's present, we remove it once and then it's gone.
536+ yoinkPreloader();
537+
538+ loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE)
539+ .catch((err) => console.error('Error completing loaderPopup:', err))
540+ .finally(() => {
541+ loaderPopup = null;
542+ resolve();
543+ });
544+ }
545+
546+ // Apply the blur styles to the entire loader element
547+ loaderElement.css({
548+ 'filter': 'blur(15px)',
549+ 'opacity': '0',
550+ });
551+ });
552+}
553+
554+/**
555+ * Removes the initial HTML preloader element.
556+ * Called once after the first loader hide.
557+ */
558+function yoinkPreloader() {
559+ if (preloaderYoinked) return;
560+ document.getElementById('preloader')?.remove();
561+ preloaderYoinked = true;
562+}
563+
564+// ============================================================================
565+// End internal overlay management
566+// ============================================================================
public/scripts/bookmarks.js+8 -3
@@ -25,7 +25,7 @@ import {
2525 saveGroupBookmarkChat,
2626 selected_group,
2727} from './group-chats.js';
2828import { hideLoader, showLoaderloader } from './action-loader.js';
2929import { getLastMessageId } from './macros.js';
3030import { Popup } from './popup.js';
3131import { SlashCommand } from './slash-commands/SlashCommand.js';
@@ -661,15 +661,20 @@ export function initBookmarks() {
661661 return;
662662 }
663663
664+ const loaderHandle = loader.show({
665+ title: t`Chat History`,
666+ message: t`Loading chat…`,
667+ toastMode: loader.ToastMode.STATIC,
668+ });
669+
664670 try {
665- showLoader();
666671 if (selected_group) {
667672 await openGroupChat(selected_group, fileName);
668673 } else {
669674 await openCharacterChat(fileName);
670675 }
671676 } finally {
672677 await hideLoaderloaderHandle.hide();
673678 }
674679
675680 $('#shadow_select_chat_popup').css('display', 'none');
public/scripts/extensions.js+0 -2
@@ -1,7 +1,6 @@
11import { DOMPurify, Popper } from '../lib.js';
22
33import { eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration, CLIENT_VERSION } from '../script.js';
4-import { showLoader } from './loader.js';
54import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
65import { renderTemplate, renderTemplateAsync } from './templates.js';
76import { delay, equalsIgnoreCaseAndAccents, isSubsetOf, sanitizeSelector, setValueByPath, versionCompare } from './utils.js';
@@ -1158,7 +1157,6 @@ async function showExtensionsDetails() {
11581157 abortController.abort();
11591158 }
11601159 if (requiresReload) {
1161- showLoader();
11621160 location.reload();
11631161 }
11641162}
public/scripts/loader.js+49 -70
@@ -1,80 +1,59 @@
11import { POPUP_RESULT, POPUP_TYPE, Popuploader } from './popupaction-loader.js';
22
3-/** @type {Popup} */
3+/**
4-let loaderPopup;
4+ * Handle for the legacy loader created by showLoader().
5-
5+ * @type {import('./action-loader.js').ActionLoaderHandle|null}
6-let preloaderYoinked = false;
6+ */
7-
7+let legacyLoaderHandle = null;
8-export function isLoaderDisplayed() {
8+
9- return !!loaderPopup;
9+/**
10-}
10+ * Shows the loader overlay.
11-
11+ *
12+ * @deprecated Use `showActionLoader()` from action-loader.js instead.
13+ * This function now creates a blocking action loader with no toast.
14+ * The new system supports stacking multiple loaders and provides better control.
15+ *
16+ * @example
17+ * // New recommended approach:
18+ * import { showActionLoader } from './action-loader.js';
19+ * const handle = showActionLoader({ message: 'Loading...' });
20+ * // ... do work ...
21+ * handle.hide();
22+ */
1223export function showLoader() {
13- // Two loaders don't make sense. Don't await, we can overlay the old loader while it closes
24+ // Hide any existing legacy loader first to maintain old behavior
14- if (loaderPopup) loaderPopup.complete(POPUP_RESULT.CANCELLED);
25+ if (legacyLoaderHandle && legacyLoaderHandle.isActive) {
15-
26+ legacyLoaderHandle.hide();
16- loaderPopup = new Popup(`
27+ }
17- <div id="loader">
18- <div id="load-spinner" class="fa-solid fa-gear fa-spin fa-3x"></div>
19- </div>`, POPUP_TYPE.DISPLAY, null, { transparent: true, animation: 'none', wide: true, large: true });
20-
21- // No close button, loaders are not closable
22- loaderPopup.closeButton.style.display = 'none';
2328
24- loaderPopup.show();
29+ // Create a blocking loader with no toast (matches old behavior)
30+ legacyLoaderHandle = loader.show({
31+ blocking: true,
32+ toastMode: loader.ToastMode.NONE,
33+ });
2534}
2635
36+/**
37+ * Hides the loader overlay.
38+ *
39+ * @deprecated Use `hideActionLoader()` or `handle.hide()` from action-loader.js instead.
40+ * This function now hides the legacy loader created by showLoader().
41+ *
42+ * @example
43+ * // New recommended approach:
44+ * import { showActionLoader } from './action-loader.js';
45+ * const handle = showActionLoader({ message: 'Loading...' });
46+ * // ... do work ...
47+ * await handle.hide();
48+ *
49+ * @returns {Promise<void>}
50+ */
2751export async function hideLoader() {
2852 if (!loaderPopuplegacyLoaderHandle || !legacyLoaderHandle.isActive) {
2953 console.warn('There is no loader showing to hide');
3054 return Promise.resolve();
3155 }
3256
33- return new Promise((resolve) => {
57+ await legacyLoaderHandle.hide();
34- const spinner = $('#load-spinner');
58+ legacyLoaderHandle = null;
35- if (!spinner.length) {
36- console.warn('Spinner element not found, skipping animation');
37- cleanup();
38- return;
39- }
40-
41- // Check if transitions are enabled
42- const transitionDuration = spinner[0] ? getComputedStyle(spinner[0]).transitionDuration : '0s';
43- const hasTransitions = parseFloat(transitionDuration) > 0;
44-
45- if (hasTransitions) {
46- Promise.race([
47- new Promise((r) => setTimeout(r, 500)), // Fallback timeout
48- new Promise((r) => spinner.one('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', r)),
49- ]).finally(cleanup);
50- } else {
51- cleanup();
52- }
53-
54- function cleanup() {
55- $('#loader').remove();
56- // Yoink preloader entirely; it only exists to cover up unstyled content while loading JS
57- // If it's present, we remove it once and then it's gone.
58- yoinkPreloader();
59-
60- loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE)
61- .catch((err) => console.error('Error completing loaderPopup:', err))
62- .finally(() => {
63- loaderPopup = null;
64- resolve();
65- });
66- }
67-
68- // Apply the styles
69- spinner.css({
70- 'filter': 'blur(15px)',
71- 'opacity': '0',
72- });
73- });
74-}
75-
76-function yoinkPreloader() {
77- if (preloaderYoinked) return;
78- document.getElementById('preloader').remove();
79- preloaderYoinked = true;
8059}
public/scripts/st-context.js+2 -0
@@ -189,7 +189,9 @@ export function getContext() {
189189 /** @deprecated Use callGenericPopup or Popup instead. */
190190 callPopup,
191191 callGenericPopup,
192+ /** @deprecated Use loader.show instead. */
192193 showLoader,
194+ /** @deprecated Use loader.hide instead. */
193195 hideLoader,
194196 mainApi: main_api,
195197 extensionSettings: extension_settings,
public/scripts/templates/welcomePanel.html+1 -1
@@ -1,6 +1,6 @@
11<div class="welcomePanel">
22 <div class="welcomeHeaderTitle">
33 <img src="img/logo.png" alt="SillyTavern Logo" data-i18n="[alt]SillyTavern Logo" class="welcomeHeaderLogo">
44 <span class="welcomeHeaderVersionDisplay">{{version}}</span>
55 <div class="mes_button showRecentChats" title="Show recent chats" data-i18n="[title]Show recent chats">
66 <i class="fa-solid fa-circle-chevron-down fa-fw fa-lg"></i>