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>
This commit is contained in:
Wolfsblvt
2026-03-22 02:30:23 +01:00
committed by GitHub
parent 0e5928b13a
commit 45009cd0e4
25 changed files with 495 additions and 124 deletions
+33
View File
@@ -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;
}
+11 -1
View File
@@ -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} شخصية (شخصيات)…"
}
+11 -1
View File
@@ -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…"
}
+11 -1
View File
@@ -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)…"
}
+11 -1
View File
@@ -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)…"
}
+11 -1
View File
@@ -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)…"
}
+11 -1
View File
@@ -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…"
}
+11 -1
View File
@@ -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}人のキャラクターを削除中…"
}
+11 -1
View File
@@ -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}개 캐릭터 삭제 중…"
}
+11 -1
View File
@@ -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…"
}
+11 -1
View File
@@ -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)…"
}
+11 -1
View File
@@ -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} персонажа(ей)…"
}
+11 -1
View File
@@ -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} ตัวละคร…"
}
+11 -1
View File
@@ -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} персонажа(ів)…"
}
+11 -1
View File
@@ -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…"
}
+9 -1
View File
@@ -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} 个角色…"
}
+11 -1
View File
@@ -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} 個角色…"
}
+54 -19
View File
@@ -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);
}
}
+8 -7
View File
@@ -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
+168 -8
View File
@@ -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<void>}
*/
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
// ============================================================================
+8 -3
View File
@@ -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');
-2
View File
@@ -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();
}
}
+47 -68
View File
@@ -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(`
<div id="loader">
<div id="load-spinner" class="fa-solid fa-gear fa-spin fa-3x"></div>
</div>`, 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<void>}
*/
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;
}
+2
View File
@@ -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,
+1 -1
View File
@@ -1,6 +1,6 @@
<div class="welcomePanel">
<div class="welcomeHeaderTitle">
<img src="img/logo.png" alt="SillyTavern Logo" class="welcomeHeaderLogo">
<img src="img/logo.png" alt="SillyTavern Logo" data-i18n="[alt]SillyTavern Logo" class="welcomeHeaderLogo">
<span class="welcomeHeaderVersionDisplay">{{version}}</span>
<div class="mes_button showRecentChats" title="Show recent chats" data-i18n="[title]Show recent chats">
<i class="fa-solid fa-circle-chevron-down fa-fw fa-lg"></i>