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, +497 -126Ignore whitespace
public/css/loader.css+33 -0
@@ -53,3 +53,36 @@
53 opacity: 1;53 opacity: 1;
54 color: color-mix(in srgb, currentColor 40%, #e74c3c 60%);54 color: color-mix(in srgb, currentColor 40%, #e74c3c 60%);
55}55}
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 @@
1444 "Interleaved Thinking": "التفكير المتداخل",1444 "Interleaved Thinking": "التفكير المتداخل",
1445 "Since Last User Message": "منذ آخر رسالة من المستخدم",1445 "Since Last User Message": "منذ آخر رسالة من المستخدم",
1446 "Active Tool Chain": "سلسلة الأدوات النشطة",1446 "Active Tool Chain": "سلسلة الأدوات النشطة",
1447 "openrouter_interleaved_thinking_hint": "يرسل الاستدلال من رسائل المساعد السابقة مع طلبات استدعاء الأدوات للحفاظ على سياق التفكير المتداخل."1447 "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} شخصية (شخصيات)…"
1448}1458}
public/locales/de-de.json+11 -1
@@ -1444,5 +1444,15 @@
1444 "Interleaved Thinking": "Verschränktes Denken",1444 "Interleaved Thinking": "Verschränktes Denken",
1445 "Since Last User Message": "Seit letzter Nutzernachricht",1445 "Since Last User Message": "Seit letzter Nutzernachricht",
1446 "Active Tool Chain": "Aktive Tool-Kette",1446 "Active Tool Chain": "Aktive Tool-Kette",
1447 "openrouter_interleaved_thinking_hint": "Sendet Begründungen aus vorherigen Assistentennachrichten mit Tool-Aufrufen, um den Kontext für verschränktes Denken zu erhalten."1447 "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…"
1448}1458}
public/locales/es-es.json+11 -1
@@ -1551,5 +1551,15 @@
1551 "Interleaved Thinking": "Razonamiento intercalado",1551 "Interleaved Thinking": "Razonamiento intercalado",
1552 "Since Last User Message": "Desde el último mensaje del usuario",1552 "Since Last User Message": "Desde el último mensaje del usuario",
1553 "Active Tool Chain": "Cadena de herramientas activa",1553 "Active Tool Chain": "Cadena de herramientas activa",
1554 "openrouter_interleaved_thinking_hint": "Envía razonamiento de turnos anteriores del asistente junto con solicitudes de herramientas para mantener el contexto del razonamiento intercalado."1554 "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)…"
1555}1565}
public/locales/fr-fr.json+11 -1
@@ -2052,5 +2052,15 @@
2052 "Interleaved Thinking": "Raisonnement intercalé",2052 "Interleaved Thinking": "Raisonnement intercalé",
2053 "Since Last User Message": "Depuis le dernier message utilisateur",2053 "Since Last User Message": "Depuis le dernier message utilisateur",
2054 "Active Tool Chain": "Chaîne d'outils active",2054 "Active Tool Chain": "Chaîne d'outils active",
2055 "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é."2055 "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)…"
2056}2066}
public/locales/is-is.json+11 -1
@@ -1442,5 +1442,15 @@
1442 "Interleaved Thinking": "Fléttuð hugsun",1442 "Interleaved Thinking": "Fléttuð hugsun",
1443 "Since Last User Message": "Frá síðustu notandaskilaboðum",1443 "Since Last User Message": "Frá síðustu notandaskilaboðum",
1444 "Active Tool Chain": "Virk verkfærakeðja",1444 "Active Tool Chain": "Virk verkfærakeðja",
1445 "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."1445 "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)…"
1446}1456}
public/locales/it-it.json+11 -1
@@ -1444,5 +1444,15 @@
1444 "Interleaved Thinking": "Ragionamento intercalato",1444 "Interleaved Thinking": "Ragionamento intercalato",
1445 "Since Last User Message": "Dall'ultimo messaggio utente",1445 "Since Last User Message": "Dall'ultimo messaggio utente",
1446 "Active Tool Chain": "Catena di strumenti attiva",1446 "Active Tool Chain": "Catena di strumenti attiva",
1447 "openrouter_interleaved_thinking_hint": "Invia il ragionamento dai turni precedenti dell'assistente con le richieste di strumenti per mantenere il contesto del ragionamento intercalato."1447 "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…"
1448}1458}
public/locales/ja-jp.json+11 -1
@@ -1449,5 +1449,15 @@
1449 "Interleaved Thinking": "インターリーブ思考",1449 "Interleaved Thinking": "インターリーブ思考",
1450 "Since Last User Message": "直前のユーザーメッセージ以降",1450 "Since Last User Message": "直前のユーザーメッセージ以降",
1451 "Active Tool Chain": "アクティブなツールチェーン",1451 "Active Tool Chain": "アクティブなツールチェーン",
1452 "openrouter_interleaved_thinking_hint": "インターリーブ思考の文脈を維持するため、直前のアシスタントターンの推論をツール呼び出しリクエストとともに送信します。"1452 "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}人のキャラクターを削除中…"
1453}1463}
public/locales/ko-kr.json+11 -1
@@ -1623,5 +1623,15 @@
1623 "Interleaved Thinking": "인터리브드 사고",1623 "Interleaved Thinking": "인터리브드 사고",
1624 "Since Last User Message": "마지막 사용자 메시지 이후",1624 "Since Last User Message": "마지막 사용자 메시지 이후",
1625 "Active Tool Chain": "활성 도구 체인",1625 "Active Tool Chain": "활성 도구 체인",
1626 "openrouter_interleaved_thinking_hint": "인터리브드 사고 컨텍스트를 유지하기 위해 이전 어시스턴트 턴의 추론을 도구 호출 요청과 함께 전송합니다."1626 "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}개 캐릭터 삭제 중…"
1627}1637}
public/locales/nl-nl.json+11 -1
@@ -1440,5 +1440,15 @@
1440 "Interleaved Thinking": "Verweven redenering",1440 "Interleaved Thinking": "Verweven redenering",
1441 "Since Last User Message": "Sinds laatste gebruikersbericht",1441 "Since Last User Message": "Sinds laatste gebruikersbericht",
1442 "Active Tool Chain": "Actieve toolketen",1442 "Active Tool Chain": "Actieve toolketen",
1443 "openrouter_interleaved_thinking_hint": "Verstuurt redenering uit eerdere assistentbeurten met tool-aanvragen om de context van verweven redenering te behouden."1443 "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…"
1444}1454}
public/locales/pt-pt.json+11 -1
@@ -1442,5 +1442,15 @@
1442 "Interleaved Thinking": "Raciocínio intercalado",1442 "Interleaved Thinking": "Raciocínio intercalado",
1443 "Since Last User Message": "Desde a última mensagem do utilizador",1443 "Since Last User Message": "Desde a última mensagem do utilizador",
1444 "Active Tool Chain": "Cadeia de ferramentas ativa",1444 "Active Tool Chain": "Cadeia de ferramentas ativa",
1445 "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."1445 "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)…"
1446}1456}
public/locales/ru-ru.json+11 -1
@@ -2744,5 +2744,15 @@
2744 "Interleaved Thinking": "Чередующееся рассуждение",2744 "Interleaved Thinking": "Чередующееся рассуждение",
2745 "Since Last User Message": "С момента последнего сообщения пользователя",2745 "Since Last User Message": "С момента последнего сообщения пользователя",
2746 "Active Tool Chain": "Активная цепочка инструментов",2746 "Active Tool Chain": "Активная цепочка инструментов",
2747 "openrouter_interleaved_thinking_hint": "Отправляет рассуждение из предыдущих ходов ассистента вместе с запросами вызова инструментов, чтобы сохранять контекст чередующегося рассуждения."2747 "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} персонажа(ей)…"
2748}2758}
public/locales/th-th.json+11 -1
@@ -1461,5 +1461,15 @@
1461 "Interleaved Thinking": "การคิดแบบสลับขั้น",1461 "Interleaved Thinking": "การคิดแบบสลับขั้น",
1462 "Since Last User Message": "ตั้งแต่ข้อความล่าสุดของผู้ใช้",1462 "Since Last User Message": "ตั้งแต่ข้อความล่าสุดของผู้ใช้",
1463 "Active Tool Chain": "สายโซ่เครื่องมือที่ใช้งานอยู่",1463 "Active Tool Chain": "สายโซ่เครื่องมือที่ใช้งานอยู่",
1464 "openrouter_interleaved_thinking_hint": "ส่งเหตุผลจากเทิร์นก่อนหน้าของผู้ช่วยไปพร้อมกับคำขอเรียกใช้เครื่องมือ เพื่อคงบริบทการคิดแบบสลับขั้นไว้"1464 "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} ตัวละคร…"
1465}1475}
public/locales/uk-ua.json+11 -1
@@ -1442,5 +1442,15 @@
1442 "Interleaved Thinking": "Черговане мислення",1442 "Interleaved Thinking": "Черговане мислення",
1443 "Since Last User Message": "Від останнього повідомлення користувача",1443 "Since Last User Message": "Від останнього повідомлення користувача",
1444 "Active Tool Chain": "Активний ланцюжок інструментів",1444 "Active Tool Chain": "Активний ланцюжок інструментів",
1445 "openrouter_interleaved_thinking_hint": "Надсилає міркування з попередніх ходів асистента разом із запитами виклику інструментів, щоб зберегти контекст чергованого мислення."1445 "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} персонажа(ів)…"
1446}1456}
public/locales/vi-vn.json+11 -1
@@ -1442,5 +1442,15 @@
1442 "Interleaved Thinking": "Lập luận xen kẽ",1442 "Interleaved Thinking": "Lập luận xen kẽ",
1443 "Since Last User Message": "Kể từ tin nhắn người dùng gần nhất",1443 "Since Last User Message": "Kể từ tin nhắn người dùng gần nhất",
1444 "Active Tool Chain": "Chuỗi công cụ đang hoạt động",1444 "Active Tool Chain": "Chuỗi công cụ đang hoạt động",
1445 "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ẽ."1445 "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…"
1446}1456}
public/locales/zh-cn.json+9 -1
@@ -3635,5 +3635,13 @@
3635 "sd_prompt_9": "你的人设 (多模态模式)",3635 "sd_prompt_9": "你的人设 (多模态模式)",
3636 "Show only favorites": "仅显示收藏",3636 "Show only favorites": "仅显示收藏",
3637 "Show only groups": "仅显示群聊",3637 "Show only groups": "仅显示群聊",
3638 "Show Tag List": "显示标签列表"3638 "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} 个角色…"
3639}3647}
public/locales/zh-tw.json+11 -1
@@ -2764,5 +2764,15 @@
2764 "Interleaved Thinking": "交錯思維",2764 "Interleaved Thinking": "交錯思維",
2765 "Since Last User Message": "自上一則使用者訊息起",2765 "Since Last User Message": "自上一則使用者訊息起",
2766 "Active Tool Chain": "使用中的工具鏈",2766 "Active Tool Chain": "使用中的工具鏈",
2767 "openrouter_interleaved_thinking_hint": "會在工具呼叫請求中附帶先前助理回合的推理,以維持交錯思維的上下文。"2767 "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} 個角色…"
2768}2778}
public/script.js+54 -19
@@ -244,7 +244,7 @@ import {
244 isPersonaPanelOpen,244 isPersonaPanelOpen,
245} from './scripts/personas.js';245} from './scripts/personas.js';
246import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';246import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';
247import { hideLoader, showLoader } from './scripts/loader.js';247import { loader } from './scripts/action-loader.js';
248import { BulkEditOverlay } from './scripts/BulkEditOverlay.js';248import { BulkEditOverlay } from './scripts/BulkEditOverlay.js';
249import { initTextGenModels } from './scripts/textgen-models.js';249import { initTextGenModels } from './scripts/textgen-models.js';
250import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, preserveNeutralChat, restoreNeutralChat, formatCreatorNotes, initChatUtilities, addDOMPurifyHooks } from './scripts/chats.js';250import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, preserveNeutralChat, restoreNeutralChat, formatCreatorNotes, initChatUtilities, addDOMPurifyHooks } from './scripts/chats.js';
@@ -698,7 +698,28 @@ async function firstLoadInit() {
698 throw new Error('Initialization failed');698 throw new Error('Initialization failed');
699 }699 }
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
702 registerPromptManagerMigration();723 registerPromptManagerMigration();
703 initDomHandlers();724 initDomHandlers();
704 initStandaloneMode();725 initStandaloneMode();
@@ -724,7 +745,7 @@ async function firstLoadInit() {
724 ToolManager.initToolSlashCommands();745 ToolManager.initToolSlashCommands();
725 await initPresetManager();746 await initPresetManager();
726 await initSystemMessages();747 await initSystemMessages();
727 await getSettings();748 await getSettings(initLoaderHandle);
728 initKeyboard();749 initKeyboard();
729 initDynamicStyles();750 initDynamicStyles();
730 initTags();751 initTags();
@@ -758,7 +779,7 @@ async function firstLoadInit() {
758 addDebugFunctions();779 addDebugFunctions();
759 doDailyExtensionUpdatesCheck();780 doDailyExtensionUpdatesCheck();
760 await eventSource.emit(event_types.APP_INITIALIZED);781 await eventSource.emit(event_types.APP_INITIALIZED);
761 await hideLoader();782 await initLoaderHandle.hide();
762 await fixViewport();783 await fixViewport();
763 await eventSource.emit(event_types.APP_READY);784 await eventSource.emit(event_types.APP_READY);
764}785}
@@ -7763,7 +7784,7 @@ function reloadLoop() {
77637784
7764//MARK: getSettings()7785//MARK: getSettings()
7765///////////////////////////////////////////7786///////////////////////////////////////////
7766export async function getSettings() {7787export async function getSettings(initLoaderHandle = null) {
7767 const response = await fetch('/api/settings/get', {7788 const response = await fetch('/api/settings/get', {
7768 method: 'POST',7789 method: 'POST',
7769 headers: getRequestHeaders(),7790 headers: getRequestHeaders(),
@@ -7881,7 +7902,7 @@ export async function getSettings() {
7881 firstRun = !!settings.firstRun;7902 firstRun = !!settings.firstRun;
78827903
7883 if (firstRun) {7904 if (firstRun) {
7884 hideLoader();7905 await initLoaderHandle?.hide();
7885 await doOnboarding(user_avatar);7906 await doOnboarding(user_avatar);
7886 firstRun = false;7907 firstRun = false;
7887 }7908 }
@@ -10457,7 +10478,7 @@ export async function doNewChat({ deleteCurrentChat = false } = {}) {
10457 * @param {string} param.newFileName New name for the chat (no JSONL extension)10478 * @param {string} param.newFileName New name for the chat (no JSONL extension)
10458 * @param {boolean} [param.loader=true] Whether to show loader during the operation10479 * @param {boolean} [param.loader=true] Whether to show loader during the operation
10459 */10480 */
10460export async function renameGroupOrCharacterChat({ characterId, groupId, oldFileName, newFileName, loader }) {10481export async function renameGroupOrCharacterChat({ characterId, groupId, oldFileName, newFileName, loader: showLoader }) {
10461 const currentChatId = getCurrentChatId();10482 const currentChatId = getCurrentChatId();
10462 const body = {10483 const body = {
10463 is_group: !!groupId,10484 is_group: !!groupId,
@@ -10475,9 +10496,13 @@ export async function renameGroupOrCharacterChat({ characterId, groupId, oldFile
10475 return;10496 return;
10476 }10497 }
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 {
10481 const response = await fetch('/api/chats/rename', {10506 const response = await fetch('/api/chats/rename', {
10482 method: 'POST',10507 method: 'POST',
10483 body: JSON.stringify(body),10508 body: JSON.stringify(body),
@@ -10510,11 +10535,10 @@ export async function renameGroupOrCharacterChat({ characterId, groupId, oldFile
10510 await reloadCurrentChat();10535 await reloadCurrentChat();
10511 }10536 }
10512 } catch {10537 } catch {
10513 loader && hideLoader();
10514 await delay(500);10538 await delay(500);
10515 await callGenericPopup('An error has occurred. Chat was not renamed.', POPUP_TYPE.TEXT);10539 await callGenericPopup('An error has occurred. Chat was not renamed.', POPUP_TYPE.TEXT);
10516 } finally {10540 } finally {
10517 loader && hideLoader();10541 await loaderHandle?.hide();
10518 }10542 }
10519}10543}
1052010544
@@ -11072,21 +11096,32 @@ jQuery(async function () {
11072 async function handleDeleteChat(chatFile, group, fromSlashCommand = false) {11096 async function handleDeleteChat(chatFile, group, fromSlashCommand = false) {
11073 // Close past chat popup.11097 // Close past chat popup.
11074 $('#select_chat_cross').trigger('click');11098 $('#select_chat_cross').trigger('click');
11075 showLoader();11099
11076 if (group) {11100 const loaderHandle = loader.show({
11077 await deleteGroupChat(group, chatFile);11101 title: t`Delete Chat`,
11078 } else {11102 message: t`Deleting chat…`,
11079 await delChat(`${chatFile}.jsonl`);11103 toastMode: loader.ToastMode.STATIC,
11104 });
11105
11106 try {
11107 if (group) {
11108 await deleteGroupChat(group, chatFile);
11109 } else {
11110 await delChat(`${chatFile}.jsonl`);
11111 }
11112 } catch (error) {
11113 loaderHandle.hide();
11114 throw error;
11080 }11115 }
1108111116
11082 if (fromSlashCommand) { // When called from `/delchat` command, don't re-open the history view.11117 if (fromSlashCommand) { // When called from `/delchat` command, don't re-open the history view.
11083 $('#options').hide(); // Hide option popup menu.11118 $('#options').hide(); // Hide option popup menu.
11084 hideLoader();11119 await loaderHandle.hide();
11085 } else { // Open the history view again after 2 seconds (delay to avoid edge cases for deleting last chat).11120 } else { // Open the history view again after 2 seconds (delay to avoid edge cases for deleting last chat).
11086 setTimeout(function () {11121 setTimeout(async function () {
11087 $('#option_select_chat').trigger('click');11122 $('#option_select_chat').trigger('click');
11088 $('#options').hide(); // Hide option popup menu.11123 $('#options').hide(); // Hide option popup menu.
11089 hideLoader();11124 await loaderHandle.hide();
11090 }, 2000);11125 }, 2000);
11091 }11126 }
11092 }11127 }
public/scripts/BulkEditOverlay.js+8 -7
@@ -14,10 +14,11 @@ import {
14} from '../script.js';14} from '../script.js';
1515
16import { favsToHotswap } from './RossAscends-mods.js';16import { favsToHotswap } from './RossAscends-mods.js';
17import { hideLoader, showLoader } from './loader.js';17import { loader } from './action-loader.js';
18import { convertCharacterToPersona } from './personas.js';18import { convertCharacterToPersona } from './personas.js';
19import { callGenericPopup, POPUP_TYPE } from './popup.js';19import { callGenericPopup, POPUP_TYPE } from './popup.js';
20import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap, importTags, tag_import_setting } from './tags.js';20import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap, importTags, tag_import_setting } from './tags.js';
21import { t } from './i18n.js';
2122
22/**23/**
23 * Static object representing the actions of the24 * Static object representing the actions of the
@@ -846,15 +847,15 @@ class BulkEditOverlay {
846847
847 const deleteChats = checkbox.prop('checked') ?? false;848 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 });
851 const avatarList = characterIds.map(id => characters[id]?.avatar).filter(a => a);855 const avatarList = characterIds.map(id => characters[id]?.avatar).filter(a => a);
852 return CharacterContextMenu.delete(avatarList, deleteChats)856 return CharacterContextMenu.delete(avatarList, deleteChats)
853 .then(() => this.browseState())857 .then(() => this.browseState())
854 .finally(() => {858 .finally(() => loaderHandle.hide());
855 toastr.clear(toast);
856 hideLoader();
857 });
858 });859 });
859860
860 // At this moment the popup is already changed in the dom, but not yet closed/resolved. We build the avatar list here861 // 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 @@
1/**1/**
2 * Action loader utility - shows loader overlay with stoppable toast notification.2 * Unified action loader system - shows loader overlay with optional toast notifications.
3 * Designed to be flexible and reusable for various long-running operations.3 * Designed to be flexible and reusable for various long-running operations.
4 * Supports stacking multiple loaders - overlay stays single, but toasts can stack.
5 *4 *
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
7 *10 *
8 * @module action-loader11 * @module action-loader
9 */12 */
1013
11import { t } from './i18n.js';14import { t } from './i18n.js';
12import { stopGeneration } from '../script.js';15import { stopGeneration } from '../script.js';
13import { showLoader, hideLoader, isLoaderDisplayed } from './loader.js';16import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
1417
15/**18/**
16 * Enum representing the toast display mode for the action loader.19 * Enum representing the toast display mode for the action loader.
@@ -33,6 +36,7 @@ export const ActionLoaderToastMode = {
33 * @property {string} [message='Generating...'] - The message to display in the toast36 * @property {string} [message='Generating...'] - The message to display in the toast
34 * @property {string} [title] - Optional title for the toast notification37 * @property {string} [title] - Optional title for the toast notification
35 * @property {string} [stopTooltip='Stop'] - Tooltip text for the stop button38 * @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)
36 * @property {(() => void)|null} [onStop=null] - Custom stop handler. If null, calls `stopGeneration()`40 * @property {(() => void)|null} [onStop=null] - Custom stop handler. If null, calls `stopGeneration()`
37 * @property {(() => void)|null} [onHide=null] - Custom hide handler. Called when the loader is hidden (not stopped).41 * @property {(() => void)|null} [onHide=null] - Custom hide handler. Called when the loader is hidden (not stopped).
38 */42 */
@@ -95,6 +99,7 @@ export class ActionLoaderHandle {
95 * @param {string} [options.message='Generating...'] - Message to display in the toast99 * @param {string} [options.message='Generating...'] - Message to display in the toast
96 * @param {string} [options.title] - Title for the toast notification100 * @param {string} [options.title] - Title for the toast notification
97 * @param {string} [options.stopTooltip='Stop'] - Tooltip for the stop button101 * @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)
98 * @param {(() => void)|null} [options.onStop] - Custom stop handler103 * @param {(() => void)|null} [options.onStop] - Custom stop handler
99 * @param {(() => void)|null} [options.onHide] - Custom hide handler104 * @param {(() => void)|null} [options.onHide] - Custom hide handler
100 */105 */
@@ -104,6 +109,7 @@ export class ActionLoaderHandle {
104 message = t`Generating...`,109 message = t`Generating...`,
105 title = '',110 title = '',
106 stopTooltip = t`Stop`,111 stopTooltip = t`Stop`,
112 overlayContent = null,
107 onStop = null,113 onStop = null,
108 onHide = null,114 onHide = null,
109 } = {}) {115 } = {}) {
@@ -113,13 +119,13 @@ export class ActionLoaderHandle {
113 this.#onHide = onHide;119 this.#onHide = onHide;
114120
115 // Warn if non-blocking loader has no toast - it won't be visible to the user121 // Warn if non-blocking loader has no toast - it won't be visible to the user
116 if (!blocking && toastMode === ActionLoaderToastMode.NONE) {122 if (!blocking && toastMode === ActionLoaderToastMode.NONE && !overlayContent) {
117 console.warn('[ActionLoader] Non-blocking loader created without a toast. This loader will not be visible to the user.');123 console.warn('[ActionLoader] Non-blocking loader created without a toast. This loader will not be visible to the user.');
118 }124 }
119125
120 // Show the blocking loader overlay if this is the first blocking handle126 // Show the blocking loader overlay if this is the first blocking handle
121 if (blocking && !hasBlockingLoaders() && !isLoaderDisplayed()) {127 if (blocking && !hasBlockingLoaders() && !isOverlayDisplayed()) {
122 showLoader();128 showOverlay(overlayContent);
123 }129 }
124130
125 // Register this handle131 // Register this handle
@@ -191,7 +197,7 @@ export class ActionLoaderHandle {
191197
192 // Hide the overlay if this was the last blocking handle198 // Hide the overlay if this was the last blocking handle
193 if (this.#blocking && !hasBlockingLoaders()) {199 if (this.#blocking && !hasBlockingLoaders()) {
194 await hideLoader();200 await hideOverlay();
195 }201 }
196 }202 }
197203
@@ -301,6 +307,12 @@ export const loader = {
301 get: getLoaderHandleById,307 get: getLoaderHandleById,
302308
303 /**309 /**
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 /**
304 * Toast display mode constants.316 * Toast display mode constants.
305 * @type {typeof ActionLoaderToastMode}317 * @type {typeof ActionLoaderToastMode}
306 */318 */
@@ -311,6 +323,12 @@ export const loader = {
311 * @type {typeof ActionLoaderHandle}323 * @type {typeof ActionLoaderHandle}
312 */324 */
313 Handle: ActionLoaderHandle,325 Handle: ActionLoaderHandle,
326
327 /**
328 * Creates a fresh default loader overlay element.
329 * @type {typeof createDefaultLoaderOverlay}
330 */
331 createOverlay: createDefaultLoaderOverlay,
314};332};
315333
316/**334/**
@@ -404,3 +422,145 @@ export function getLoaderHandleById(id) {
404 }422 }
405 return undefined;423 return undefined;
406}424}
425
426// ============================================================================
427// Internal overlay management
428// ============================================================================
429
430/** @type {Popup|null} The current loader overlay popup */
431let loaderPopup = null;
432
433/** Whether the initial HTML preloader has been removed */
434let 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 */
442export 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 */
460function 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 */
476function 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 */
485function 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 */
504async 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 */
558function 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 {
25 saveGroupBookmarkChat,25 saveGroupBookmarkChat,
26 selected_group,26 selected_group,
27} from './group-chats.js';27} from './group-chats.js';
28import { hideLoader, showLoader } from './loader.js';28import { loader } from './action-loader.js';
29import { getLastMessageId } from './macros.js';29import { getLastMessageId } from './macros.js';
30import { Popup } from './popup.js';30import { Popup } from './popup.js';
31import { SlashCommand } from './slash-commands/SlashCommand.js';31import { SlashCommand } from './slash-commands/SlashCommand.js';
@@ -661,15 +661,20 @@ export function initBookmarks() {
661 return;661 return;
662 }662 }
663663
664 const loaderHandle = loader.show({
665 title: t`Chat History`,
666 message: t`Loading chat…`,
667 toastMode: loader.ToastMode.STATIC,
668 });
669
664 try {670 try {
665 showLoader();
666 if (selected_group) {671 if (selected_group) {
667 await openGroupChat(selected_group, fileName);672 await openGroupChat(selected_group, fileName);
668 } else {673 } else {
669 await openCharacterChat(fileName);674 await openCharacterChat(fileName);
670 }675 }
671 } finally {676 } finally {
672 await hideLoader();677 await loaderHandle.hide();
673 }678 }
674679
675 $('#shadow_select_chat_popup').css('display', 'none');680 $('#shadow_select_chat_popup').css('display', 'none');
public/scripts/extensions.js+0 -2
@@ -1,7 +1,6 @@
1import { DOMPurify, Popper } from '../lib.js';1import { DOMPurify, Popper } from '../lib.js';
22
3import { eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration, CLIENT_VERSION } from '../script.js';3import { eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration, CLIENT_VERSION } from '../script.js';
4import { showLoader } from './loader.js';
5import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';4import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
6import { renderTemplate, renderTemplateAsync } from './templates.js';5import { renderTemplate, renderTemplateAsync } from './templates.js';
7import { delay, equalsIgnoreCaseAndAccents, isSubsetOf, sanitizeSelector, setValueByPath, versionCompare } from './utils.js';6import { delay, equalsIgnoreCaseAndAccents, isSubsetOf, sanitizeSelector, setValueByPath, versionCompare } from './utils.js';
@@ -1158,7 +1157,6 @@ async function showExtensionsDetails() {
1158 abortController.abort();1157 abortController.abort();
1159 }1158 }
1160 if (requiresReload) {1159 if (requiresReload) {
1161 showLoader();
1162 location.reload();1160 location.reload();
1163 }1161 }
1164}1162}
public/scripts/loader.js+49 -70
@@ -1,80 +1,59 @@
1import { POPUP_RESULT, POPUP_TYPE, Popup } from './popup.js';1import { loader } from './action-loader.js';
22
3/** @type {Popup} */3/**
4let loaderPopup;4 * Handle for the legacy loader created by showLoader().
55 * @type {import('./action-loader.js').ActionLoaderHandle|null}
6let preloaderYoinked = false;6 */
77let legacyLoaderHandle = null;
8export function isLoaderDisplayed() {8
9 return !!loaderPopup;9/**
10}10 * Shows the loader overlay.
1111 *
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 */
12export function showLoader() {23export function showLoader() {
13 // Two loaders don't make sense. Don't await, we can overlay the old loader while it closes24 // Hide any existing legacy loader first to maintain old behavior
14 if (loaderPopup) loaderPopup.complete(POPUP_RESULT.CANCELLED);25 if (legacyLoaderHandle && legacyLoaderHandle.isActive) {
1526 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 });
25}34}
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 */
27export async function hideLoader() {51export async function hideLoader() {
28 if (!loaderPopup) {52 if (!legacyLoaderHandle || !legacyLoaderHandle.isActive) {
29 console.warn('There is no loader showing to hide');53 console.warn('There is no loader showing to hide');
30 return Promise.resolve();54 return Promise.resolve();
31 }55 }
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
76function yoinkPreloader() {
77 if (preloaderYoinked) return;
78 document.getElementById('preloader').remove();
79 preloaderYoinked = true;
80}59}
public/scripts/st-context.js+2 -0
@@ -189,7 +189,9 @@ export function getContext() {
189 /** @deprecated Use callGenericPopup or Popup instead. */189 /** @deprecated Use callGenericPopup or Popup instead. */
190 callPopup,190 callPopup,
191 callGenericPopup,191 callGenericPopup,
192 /** @deprecated Use loader.show instead. */
192 showLoader,193 showLoader,
194 /** @deprecated Use loader.hide instead. */
193 hideLoader,195 hideLoader,
194 mainApi: main_api,196 mainApi: main_api,
195 extensionSettings: extension_settings,197 extensionSettings: extension_settings,
public/scripts/templates/welcomePanel.html+1 -1
@@ -1,6 +1,6 @@
1<div class="welcomePanel">1<div class="welcomePanel">
2 <div class="welcomeHeaderTitle">2 <div class="welcomeHeaderTitle">
3 <img src="img/logo.png" alt="SillyTavern Logo" class="welcomeHeaderLogo">3 <img src="img/logo.png" alt="SillyTavern Logo" data-i18n="[alt]SillyTavern Logo" class="welcomeHeaderLogo">
4 <span class="welcomeHeaderVersionDisplay">{{version}}</span>4 <span class="welcomeHeaderVersionDisplay">{{version}}</span>
5 <div class="mes_button showRecentChats" title="Show recent chats" data-i18n="[title]Show recent chats">5 <div class="mes_button showRecentChats" title="Show recent chats" data-i18n="[title]Show recent chats">
6 <i class="fa-solid fa-circle-chevron-down fa-fw fa-lg"></i>6 <i class="fa-solid fa-circle-chevron-down fa-fw fa-lg"></i>