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>
Signed| @@ -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 | } | ||
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -244,7 +244,7 @@ import { | |||
| 244 | isPersonaPanelOpen, | 244 | isPersonaPanelOpen, |
| 245 | } from './scripts/personas.js'; | 245 | } from './scripts/personas.js'; |
| 246 | import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js'; | 246 | import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js'; |
| 247 | import { hideLoader, showLoader } from './scripts/loader.js'; | 247 | import { loader } from './scripts/action-loader.js'; |
| 248 | import { BulkEditOverlay } from './scripts/BulkEditOverlay.js'; | 248 | import { BulkEditOverlay } from './scripts/BulkEditOverlay.js'; |
| 249 | import { initTextGenModels } from './scripts/textgen-models.js'; | 249 | import { initTextGenModels } from './scripts/textgen-models.js'; |
| 250 | import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, preserveNeutralChat, restoreNeutralChat, formatCreatorNotes, initChatUtilities, addDOMPurifyHooks } from './scripts/chats.js'; | 250 | import { 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 | } |
| 700 | 700 | ||
| 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() { | |||
| 7763 | 7784 | ||
| 7764 | //MARK: getSettings() | 7785 | //MARK: getSettings() |
| 7765 | /////////////////////////////////////////// | 7786 | /////////////////////////////////////////// |
| 7766 | export async function getSettings() { | 7787 | export 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; |
| 7882 | 7903 | ||
| 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 operation | 10479 | * @param {boolean} [param.loader=true] Whether to show loader during the operation |
| 10459 | */ | 10480 | */ |
| 10460 | export async function renameGroupOrCharacterChat({ characterId, groupId, oldFileName, newFileName, loader }) { | 10481 | export 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 | } |
| 10477 | 10498 | ||
| 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; | ||
| 10480 | 10504 | ||
| 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 | } |
| 10520 | 10544 | ||
| @@ -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 | |
| 11100 | const loaderHandle = loader.show({ | ||
| 11101 | title: t`Delete Chat`, | ||
| 11102 | message: t`Deleting chat…`, | ||
| 11103 | toastMode: loader.ToastMode.STATIC, | ||
| 11104 | }); | ||
| 11105 | |||
| 11106 | try { | ||
| 11076 | if (group) { | 11107 | if (group) { |
| 11077 | await deleteGroupChat(group, chatFile); | 11108 | await deleteGroupChat(group, chatFile); |
| 11078 | } else { | 11109 | } else { |
| 11079 | await delChat(`${chatFile}.jsonl`); | 11110 | await delChat(`${chatFile}.jsonl`); |
| 11080 | } | 11111 | } |
| 11112 | } catch (error) { | ||
| 11113 | loaderHandle.hide(); | ||
| 11114 | throw error; | ||
| 11115 | } | ||
| 11081 | 11116 | ||
| 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 | } |
| @@ -14,10 +14,11 @@ import { | |||
| 14 | } from '../script.js'; | 14 | } from '../script.js'; |
| 15 | 15 | ||
| 16 | import { favsToHotswap } from './RossAscends-mods.js'; | 16 | import { favsToHotswap } from './RossAscends-mods.js'; |
| 17 | import { hideLoader, showLoader } from './loader.js'; | 17 | import { loader } from './action-loader.js'; |
| 18 | import { convertCharacterToPersona } from './personas.js'; | 18 | import { convertCharacterToPersona } from './personas.js'; |
| 19 | import { callGenericPopup, POPUP_TYPE } from './popup.js'; | 19 | import { callGenericPopup, POPUP_TYPE } from './popup.js'; |
| 20 | import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap, importTags, tag_import_setting } from './tags.js'; | 20 | import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap, importTags, tag_import_setting } from './tags.js'; |
| 21 | import { t } from './i18n.js'; | ||
| 21 | 22 | ||
| 22 | /** | 23 | /** |
| 23 | * Static object representing the actions of the | 24 | * Static object representing the actions of the |
| @@ -846,15 +847,15 @@ class BulkEditOverlay { | |||
| 846 | 847 | ||
| 847 | const deleteChats = checkbox.prop('checked') ?? false; | 848 | const deleteChats = checkbox.prop('checked') ?? false; |
| 848 | 849 | ||
| 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 | }); |
| 859 | 860 | ||
| 860 | // At this moment the popup is already changed in the dom, but not yet closed/resolved. We build the avatar list here | 861 | // At this moment the popup is already changed in the dom, but not yet closed/resolved. We build the avatar list here |
| @@ -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-loader | 11 | * @module action-loader |
| 9 | */ | 12 | */ |
| 10 | 13 | ||
| 11 | import { t } from './i18n.js'; | 14 | import { t } from './i18n.js'; |
| 12 | import { stopGeneration } from '../script.js'; | 15 | import { stopGeneration } from '../script.js'; |
| 13 | import { showLoader, hideLoader, isLoaderDisplayed } from './loader.js'; | 16 | import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; |
| 14 | 17 | ||
| 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 toast | 36 | * @property {string} [message='Generating...'] - The message to display in the toast |
| 34 | * @property {string} [title] - Optional title for the toast notification | 37 | * @property {string} [title] - Optional title for the toast notification |
| 35 | * @property {string} [stopTooltip='Stop'] - Tooltip text for the stop button | 38 | * @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 toast | 99 | * @param {string} [options.message='Generating...'] - Message to display in the toast |
| 96 | * @param {string} [options.title] - Title for the toast notification | 100 | * @param {string} [options.title] - Title for the toast notification |
| 97 | * @param {string} [options.stopTooltip='Stop'] - Tooltip for the stop button | 101 | * @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 handler | 103 | * @param {(() => void)|null} [options.onStop] - Custom stop handler |
| 99 | * @param {(() => void)|null} [options.onHide] - Custom hide handler | 104 | * @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; |
| 114 | 120 | ||
| 115 | // Warn if non-blocking loader has no toast - it won't be visible to the user | 121 | // 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 | } |
| 119 | 125 | ||
| 120 | // Show the blocking loader overlay if this is the first blocking handle | 126 | // 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 | } |
| 124 | 130 | ||
| 125 | // Register this handle | 131 | // Register this handle |
| @@ -191,7 +197,7 @@ export class ActionLoaderHandle { | |||
| 191 | 197 | ||
| 192 | // Hide the overlay if this was the last blocking handle | 198 | // 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 | } |
| 197 | 203 | ||
| @@ -301,6 +307,12 @@ export const loader = { | |||
| 301 | get: getLoaderHandleById, | 307 | get: getLoaderHandleById, |
| 302 | 308 | ||
| 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 | }; |
| 315 | 333 | ||
| 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 */ | ||
| 431 | let loaderPopup = null; | ||
| 432 | |||
| 433 | /** Whether the initial HTML preloader has been removed */ | ||
| 434 | let preloaderYoinked = false; | ||
| 435 | |||
| 436 | /** | ||
| 437 | * Creates the default loader overlay element. | ||
| 438 | * Always returns a fresh element instance. | ||
| 439 | * | ||
| 440 | * @returns {HTMLDivElement} A new loader overlay element | ||
| 441 | */ | ||
| 442 | export function createDefaultLoaderOverlay() { | ||
| 443 | const loaderElement = document.createElement('div'); | ||
| 444 | loaderElement.id = 'loader'; | ||
| 445 | |||
| 446 | const spinnerElement = document.createElement('div'); | ||
| 447 | spinnerElement.id = 'load-spinner'; | ||
| 448 | spinnerElement.className = 'fa-solid fa-gear fa-spin fa-3x'; | ||
| 449 | |||
| 450 | loaderElement.appendChild(spinnerElement); | ||
| 451 | |||
| 452 | return loaderElement; | ||
| 453 | } | ||
| 454 | |||
| 455 | /** | ||
| 456 | * Normalizes custom overlay content into a value supported by Popup. | ||
| 457 | * @param {string|HTMLElement|null} customContent - Custom overlay content | ||
| 458 | * @returns {string|HTMLElement} Content for Popup | ||
| 459 | */ | ||
| 460 | function getOverlayContent(customContent) { | ||
| 461 | if (typeof customContent === 'string') { | ||
| 462 | return customContent; | ||
| 463 | } | ||
| 464 | |||
| 465 | if (customContent instanceof HTMLElement) { | ||
| 466 | return customContent; | ||
| 467 | } | ||
| 468 | |||
| 469 | return createDefaultLoaderOverlay(); | ||
| 470 | } | ||
| 471 | |||
| 472 | /** | ||
| 473 | * Checks if the loader overlay is currently displayed. | ||
| 474 | * @returns {boolean} True if overlay is shown | ||
| 475 | */ | ||
| 476 | function isOverlayDisplayed() { | ||
| 477 | return !!loaderPopup; | ||
| 478 | } | ||
| 479 | |||
| 480 | /** | ||
| 481 | * Shows the blocking loader overlay. | ||
| 482 | * Internal function - use showActionLoader() instead. | ||
| 483 | * @param {HTMLElement|string|null} [customContent] - Custom content for the overlay | ||
| 484 | */ | ||
| 485 | function showOverlay(customContent = null) { | ||
| 486 | // Two loaders don't make sense. Don't await, we can overlay the old loader while it closes | ||
| 487 | if (loaderPopup) loaderPopup.complete(POPUP_RESULT.CANCELLED); | ||
| 488 | |||
| 489 | const content = getOverlayContent(customContent); | ||
| 490 | |||
| 491 | loaderPopup = new Popup(content, POPUP_TYPE.DISPLAY, null, { transparent: true, animation: 'none', wide: true, large: true }); | ||
| 492 | |||
| 493 | // No close button, loaders are not closable | ||
| 494 | loaderPopup.closeButton.style.display = 'none'; | ||
| 495 | |||
| 496 | loaderPopup.show(); | ||
| 497 | } | ||
| 498 | |||
| 499 | /** | ||
| 500 | * Hides the blocking loader overlay with animation. | ||
| 501 | * Internal function - use hideActionLoader() instead. | ||
| 502 | * @returns {Promise<void>} | ||
| 503 | */ | ||
| 504 | async function hideOverlay() { | ||
| 505 | if (!loaderPopup) { | ||
| 506 | return Promise.resolve(); | ||
| 507 | } | ||
| 508 | |||
| 509 | return new Promise((resolve) => { | ||
| 510 | const loaderElement = $('#loader'); | ||
| 511 | const spinner = $('#load-spinner'); | ||
| 512 | |||
| 513 | if (!loaderElement.length) { | ||
| 514 | console.warn('Loader element not found, skipping animation'); | ||
| 515 | cleanup(); | ||
| 516 | return; | ||
| 517 | } | ||
| 518 | |||
| 519 | // Check if transitions are enabled on spinner (which has the transition property) | ||
| 520 | const transitionDuration = spinner.length && spinner[0] ? getComputedStyle(spinner[0]).transitionDuration : '0s'; | ||
| 521 | const hasTransitions = parseFloat(transitionDuration) > 0; | ||
| 522 | |||
| 523 | if (hasTransitions) { | ||
| 524 | Promise.race([ | ||
| 525 | new Promise((r) => setTimeout(r, 500)), // Fallback timeout | ||
| 526 | new Promise((r) => loaderElement.one('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', r)), | ||
| 527 | ]).finally(cleanup); | ||
| 528 | } else { | ||
| 529 | cleanup(); | ||
| 530 | } | ||
| 531 | |||
| 532 | function cleanup() { | ||
| 533 | loaderElement.remove(); | ||
| 534 | // Yoink preloader entirely; it only exists to cover up unstyled content while loading JS | ||
| 535 | // If it's present, we remove it once and then it's gone. | ||
| 536 | yoinkPreloader(); | ||
| 537 | |||
| 538 | loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE) | ||
| 539 | .catch((err) => console.error('Error completing loaderPopup:', err)) | ||
| 540 | .finally(() => { | ||
| 541 | loaderPopup = null; | ||
| 542 | resolve(); | ||
| 543 | }); | ||
| 544 | } | ||
| 545 | |||
| 546 | // Apply the blur styles to the entire loader element | ||
| 547 | loaderElement.css({ | ||
| 548 | 'filter': 'blur(15px)', | ||
| 549 | 'opacity': '0', | ||
| 550 | }); | ||
| 551 | }); | ||
| 552 | } | ||
| 553 | |||
| 554 | /** | ||
| 555 | * Removes the initial HTML preloader element. | ||
| 556 | * Called once after the first loader hide. | ||
| 557 | */ | ||
| 558 | function yoinkPreloader() { | ||
| 559 | if (preloaderYoinked) return; | ||
| 560 | document.getElementById('preloader')?.remove(); | ||
| 561 | preloaderYoinked = true; | ||
| 562 | } | ||
| 563 | |||
| 564 | // ============================================================================ | ||
| 565 | // End internal overlay management | ||
| 566 | // ============================================================================ | ||
| @@ -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'; |
| 28 | import { hideLoader, showLoader } from './loader.js'; | 28 | import { loader } from './action-loader.js'; |
| 29 | import { getLastMessageId } from './macros.js'; | 29 | import { getLastMessageId } from './macros.js'; |
| 30 | import { Popup } from './popup.js'; | 30 | import { Popup } from './popup.js'; |
| 31 | import { SlashCommand } from './slash-commands/SlashCommand.js'; | 31 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| @@ -661,15 +661,20 @@ export function initBookmarks() { | |||
| 661 | return; | 661 | return; |
| 662 | } | 662 | } |
| 663 | 663 | ||
| 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 | } |
| 674 | 679 | ||
| 675 | $('#shadow_select_chat_popup').css('display', 'none'); | 680 | $('#shadow_select_chat_popup').css('display', 'none'); |
| @@ -1,7 +1,6 @@ | |||
| 1 | import { DOMPurify, Popper } from '../lib.js'; | 1 | import { DOMPurify, Popper } from '../lib.js'; |
| 2 | 2 | ||
| 3 | import { eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration, CLIENT_VERSION } from '../script.js'; | 3 | import { eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration, CLIENT_VERSION } from '../script.js'; |
| 4 | import { showLoader } from './loader.js'; | ||
| 5 | import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; | 4 | import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; |
| 6 | import { renderTemplate, renderTemplateAsync } from './templates.js'; | 5 | import { renderTemplate, renderTemplateAsync } from './templates.js'; |
| 7 | import { delay, equalsIgnoreCaseAndAccents, isSubsetOf, sanitizeSelector, setValueByPath, versionCompare } from './utils.js'; | 6 | import { 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 | } |
| @@ -1,80 +1,59 @@ | |||
| 1 | import { POPUP_RESULT, POPUP_TYPE, Popup } from './popup.js'; | 1 | import { loader } from './action-loader.js'; |
| 2 | 2 | ||
| 3 | /** @type {Popup} */ | 3 | /** |
| 4 | let loaderPopup; | 4 | * Handle for the legacy loader created by showLoader(). |
| 5 | 5 | * @type {import('./action-loader.js').ActionLoaderHandle|null} | |
| 6 | let preloaderYoinked = false; | 6 | */ |
| 7 | 7 | let legacyLoaderHandle = null; | |
| 8 | export function isLoaderDisplayed() { | 8 | |
| 9 | return !!loaderPopup; | 9 | /** |
| 10 | } | 10 | * Shows the loader overlay. |
| 11 | 11 | * | |
| 12 | * @deprecated Use `showActionLoader()` from action-loader.js instead. | ||
| 13 | * This function now creates a blocking action loader with no toast. | ||
| 14 | * The new system supports stacking multiple loaders and provides better control. | ||
| 15 | * | ||
| 16 | * @example | ||
| 17 | * // New recommended approach: | ||
| 18 | * import { showActionLoader } from './action-loader.js'; | ||
| 19 | * const handle = showActionLoader({ message: 'Loading...' }); | ||
| 20 | * // ... do work ... | ||
| 21 | * handle.hide(); | ||
| 22 | */ | ||
| 12 | export function showLoader() { | 23 | export function showLoader() { |
| 13 | // Two loaders don't make sense. Don't await, we can overlay the old loader while it closes | 24 | // Hide any existing legacy loader first to maintain old behavior |
| 14 | if (loaderPopup) loaderPopup.complete(POPUP_RESULT.CANCELLED); | 25 | if (legacyLoaderHandle && legacyLoaderHandle.isActive) { |
| 15 | 26 | legacyLoaderHandle.hide(); | |
| 16 | loaderPopup = new Popup(` | 27 | } |
| 17 | <div id="loader"> | ||
| 18 | <div id="load-spinner" class="fa-solid fa-gear fa-spin fa-3x"></div> | ||
| 19 | </div>`, POPUP_TYPE.DISPLAY, null, { transparent: true, animation: 'none', wide: true, large: true }); | ||
| 20 | |||
| 21 | // No close button, loaders are not closable | ||
| 22 | loaderPopup.closeButton.style.display = 'none'; | ||
| 23 | 28 | ||
| 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 | } |
| 26 | 35 | ||
| 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 | */ | ||
| 27 | export async function hideLoader() { | 51 | export 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 | } |
| 32 | 56 | ||
| 33 | return new Promise((resolve) => { | 57 | await legacyLoaderHandle.hide(); |
| 34 | const spinner = $('#load-spinner'); | 58 | legacyLoaderHandle = null; |
| 35 | if (!spinner.length) { | ||
| 36 | console.warn('Spinner element not found, skipping animation'); | ||
| 37 | cleanup(); | ||
| 38 | return; | ||
| 39 | } | ||
| 40 | |||
| 41 | // Check if transitions are enabled | ||
| 42 | const transitionDuration = spinner[0] ? getComputedStyle(spinner[0]).transitionDuration : '0s'; | ||
| 43 | const hasTransitions = parseFloat(transitionDuration) > 0; | ||
| 44 | |||
| 45 | if (hasTransitions) { | ||
| 46 | Promise.race([ | ||
| 47 | new Promise((r) => setTimeout(r, 500)), // Fallback timeout | ||
| 48 | new Promise((r) => spinner.one('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', r)), | ||
| 49 | ]).finally(cleanup); | ||
| 50 | } else { | ||
| 51 | cleanup(); | ||
| 52 | } | ||
| 53 | |||
| 54 | function cleanup() { | ||
| 55 | $('#loader').remove(); | ||
| 56 | // Yoink preloader entirely; it only exists to cover up unstyled content while loading JS | ||
| 57 | // If it's present, we remove it once and then it's gone. | ||
| 58 | yoinkPreloader(); | ||
| 59 | |||
| 60 | loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE) | ||
| 61 | .catch((err) => console.error('Error completing loaderPopup:', err)) | ||
| 62 | .finally(() => { | ||
| 63 | loaderPopup = null; | ||
| 64 | resolve(); | ||
| 65 | }); | ||
| 66 | } | ||
| 67 | |||
| 68 | // Apply the styles | ||
| 69 | spinner.css({ | ||
| 70 | 'filter': 'blur(15px)', | ||
| 71 | 'opacity': '0', | ||
| 72 | }); | ||
| 73 | }); | ||
| 74 | } | ||
| 75 | |||
| 76 | function yoinkPreloader() { | ||
| 77 | if (preloaderYoinked) return; | ||
| 78 | document.getElementById('preloader').remove(); | ||
| 79 | preloaderYoinked = true; | ||
| 80 | } | 59 | } |
| @@ -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, |
| @@ -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> |