diff --git a/components/settings/IdbUnlockModal.tsx b/components/settings/IdbUnlockModal.tsx index 518e79ff7..3ad9b0ee2 100644 --- a/components/settings/IdbUnlockModal.tsx +++ b/components/settings/IdbUnlockModal.tsx @@ -12,43 +12,83 @@ interface Props { const ATTEMPT_STORAGE_KEY = 'worldscript-idb-unlock-attempts'; const LOCKOUT_STORAGE_KEY = 'worldscript-idb-unlock-lockout'; +const LEGACY_ATTEMPT_STORAGE_KEY = 'storycraft-idb-unlock-attempts'; +const LEGACY_LOCKOUT_STORAGE_KEY = 'storycraft-idb-unlock-lockout'; -function getAttemptCount(): number { +// QNBS-v3: lockout backoff is capped at 60s (see lockoutMs), so a valid lockout timestamp never +// exceeds now + 60s; the attempt counter is a tiny integer. These bounds clamp corrupt storage. +const MAX_LOCKOUT_MS = 60_000; +const MAX_ATTEMPTS = 1000; + +function readInt(key: string, legacyKey: string, max = Number.MAX_SAFE_INTEGER): number { + if (typeof window === 'undefined') { + return 0; + } try { - return Number.parseInt(localStorage.getItem(ATTEMPT_STORAGE_KEY) ?? '0', 10); + let raw = window.localStorage.getItem(key); + // QNBS-v3: Rebrand migration — read legacy StoryCraft lockout state once, then move to the new key. + if (raw === null) { + raw = window.localStorage.getItem(legacyKey); + if (raw !== null) { + window.localStorage.setItem(key, raw); + window.localStorage.removeItem(legacyKey); + } + } + // QNBS-v3: corrupt/non-numeric storage values parse to NaN, which would poison the lockout + // math (Date.now() + NaN) and the countdown UI — normalize any invalid parse to 0. + const parsed = Number.parseInt(raw ?? '0', 10); + if (Number.isNaN(parsed)) return 0; + // QNBS-v3 (CodeAnt): clamp to a non-negative, bounded range so a corrupt/huge value — e.g. a + // far-future lockout timestamp — cannot block unlock indefinitely. + return Math.min(Math.max(parsed, 0), max); } catch { return 0; } } +function getAttemptCount(): number { + return readInt(ATTEMPT_STORAGE_KEY, LEGACY_ATTEMPT_STORAGE_KEY, MAX_ATTEMPTS); +} + function setAttemptCount(n: number): void { + if (typeof window === 'undefined') { + return; + } try { - localStorage.setItem(ATTEMPT_STORAGE_KEY, String(n)); + window.localStorage.setItem(ATTEMPT_STORAGE_KEY, String(n)); } catch { /* storage blocked */ } } function getLockoutUntil(): number { - try { - return Number.parseInt(localStorage.getItem(LOCKOUT_STORAGE_KEY) ?? '0', 10); - } catch { - return 0; - } + const ts = readInt(LOCKOUT_STORAGE_KEY, LEGACY_LOCKOUT_STORAGE_KEY); + // QNBS-v3 (CodeAnt): a valid lockout never exceeds now + the max backoff window. Treat a + // beyond-bound (corrupt) value as INVALID → no lockout (0). A moving `now + MAX` clamp would + // re-clamp every read and never count down, permanently locking the user out. + return ts > Date.now() + MAX_LOCKOUT_MS ? 0 : ts; } function setLockoutUntil(ts: number): void { + if (typeof window === 'undefined') { + return; + } try { - localStorage.setItem(LOCKOUT_STORAGE_KEY, String(ts)); + window.localStorage.setItem(LOCKOUT_STORAGE_KEY, String(ts)); } catch { /* storage blocked */ } } function clearAttemptTracking(): void { + if (typeof window === 'undefined') { + return; + } try { - localStorage.removeItem(ATTEMPT_STORAGE_KEY); - localStorage.removeItem(LOCKOUT_STORAGE_KEY); + window.localStorage.removeItem(ATTEMPT_STORAGE_KEY); + window.localStorage.removeItem(LOCKOUT_STORAGE_KEY); + window.localStorage.removeItem(LEGACY_ATTEMPT_STORAGE_KEY); + window.localStorage.removeItem(LEGACY_LOCKOUT_STORAGE_KEY); } catch { /* storage blocked */ } diff --git a/hooks/useTemplateView.ts b/hooks/useTemplateView.ts index 32c3c4e00..6d84c9ae1 100644 --- a/hooks/useTemplateView.ts +++ b/hooks/useTemplateView.ts @@ -125,8 +125,10 @@ export const useTemplateView = ({ onNavigate }: UseTemplateViewProps) => { const newManuscript: StorySection[] = ct.sections.map((s, i) => ({ id: `sec-${stamp}-${i}`, title: s.title, - content: s.description ? `# ${s.title}\n\n${s.description}` : '', - prompt: '', + // QNBS-v3: section guidance goes into `prompt` (writing brief), NOT `content` — mirrors the + // built-in-template path so a freshly applied template starts with an EMPTY manuscript. + content: '', + prompt: s.description ?? '', })); const newOutline: OutlineSection[] = ct.sections.map((s, i) => ({ id: `out-${stamp}-${i}`, diff --git a/locales/ar/settings.json b/locales/ar/settings.json index ce314e493..8997c32d4 100644 --- a/locales/ar/settings.json +++ b/locales/ar/settings.json @@ -797,5 +797,6 @@ "voice.modelDownload.error": "فشل التحميل: {{error}}", "voice.modelDownload.progress": "{{percent}}% مكتمل", "voice.modelDownload.retry": "إعادة المحاولة", - "voice.modelDownload.title": "تحميل نموذج الصوت" + "voice.modelDownload.title": "تحميل نموذج الصوت", + "settings.about.productName": "WorldScript Studio" } diff --git a/locales/de/settings.json b/locales/de/settings.json index ef42387c2..ae605b255 100644 --- a/locales/de/settings.json +++ b/locales/de/settings.json @@ -797,5 +797,6 @@ "voice.modelDownload.error": "Download fehlgeschlagen: {{error}}", "voice.modelDownload.progress": "{{percent}}% abgeschlossen", "voice.modelDownload.retry": "Wiederholen", - "voice.modelDownload.title": "Sprachmodell-Download" + "voice.modelDownload.title": "Sprachmodell-Download", + "settings.about.productName": "WorldScript Studio" } diff --git a/locales/el/settings.json b/locales/el/settings.json index 0966be03a..c2cf0008f 100644 --- a/locales/el/settings.json +++ b/locales/el/settings.json @@ -797,5 +797,6 @@ "voice.modelDownload.error": "Η λήψη απέτυχε: {{error}}", "voice.modelDownload.progress": "{{percent}}% ολοκληρώθηκε", "voice.modelDownload.retry": "Δοκιμάζω πάλι", - "voice.modelDownload.title": "Λήψη φωνητικού μοντέλου" + "voice.modelDownload.title": "Λήψη φωνητικού μοντέλου", + "settings.about.productName": "WorldScript Studio" } diff --git a/locales/en/settings.json b/locales/en/settings.json index 7365a72ab..ba83beac8 100644 --- a/locales/en/settings.json +++ b/locales/en/settings.json @@ -797,5 +797,6 @@ "settings.ai.localAi.modelLabel.phi4mini": "Phi-4 Mini 3.8B (~2.3 GB)", "settings.ai.localAi.modelLabel.gemma3_1b": "Gemma 3 1B (~0.8 GB)", "settings.ai.localAi.modelLabel.gemma3_4b": "Gemma 3 4B (~4.9 GB)", - "settings.ai.localAi.modelLabel.llama33_70b": "Llama 3.3 70B (high-end, ~35 GB)" + "settings.ai.localAi.modelLabel.llama33_70b": "Llama 3.3 70B (high-end, ~35 GB)", + "settings.about.productName": "WorldScript Studio" } diff --git a/locales/es/settings.json b/locales/es/settings.json index 3caa50706..c5f5f2411 100644 --- a/locales/es/settings.json +++ b/locales/es/settings.json @@ -797,5 +797,6 @@ "voice.modelDownload.error": "Descarga fallida: {{error}}", "voice.modelDownload.progress": "{{percent}}% completado", "voice.modelDownload.retry": "Reintentar", - "voice.modelDownload.title": "Descarga de modelo de voz" + "voice.modelDownload.title": "Descarga de modelo de voz", + "settings.about.productName": "WorldScript Studio" } diff --git a/locales/fr/settings.json b/locales/fr/settings.json index 61f4bd52f..7b92eb436 100644 --- a/locales/fr/settings.json +++ b/locales/fr/settings.json @@ -797,5 +797,6 @@ "voice.modelDownload.error": "Échec du téléchargement : {{error}}", "voice.modelDownload.progress": "{{percent}}% terminé", "voice.modelDownload.retry": "Réessayer", - "voice.modelDownload.title": "Téléchargement du modèle vocal" + "voice.modelDownload.title": "Téléchargement du modèle vocal", + "settings.about.productName": "WorldScript Studio" } diff --git a/locales/he/settings.json b/locales/he/settings.json index f92429c12..7c5eb5b02 100644 --- a/locales/he/settings.json +++ b/locales/he/settings.json @@ -797,5 +797,6 @@ "voice.modelDownload.error": "ההורדה נכשלה: {{error}}", "voice.modelDownload.progress": "{{percent}}% הושלם", "voice.modelDownload.retry": "נסה שוב", - "voice.modelDownload.title": "הורדת מודל קול" + "voice.modelDownload.title": "הורדת מודל קול", + "settings.about.productName": "WorldScript Studio" } diff --git a/locales/it/settings.json b/locales/it/settings.json index ca788902e..cec0eeebf 100644 --- a/locales/it/settings.json +++ b/locales/it/settings.json @@ -797,5 +797,6 @@ "voice.modelDownload.error": "Download fallito: {{error}}", "voice.modelDownload.progress": "{{percent}}% completato", "voice.modelDownload.retry": "Riprova", - "voice.modelDownload.title": "Download modello vocale" + "voice.modelDownload.title": "Download modello vocale", + "settings.about.productName": "WorldScript Studio" } diff --git a/locales/ja/settings.json b/locales/ja/settings.json index c919af012..e8b4be3b8 100644 --- a/locales/ja/settings.json +++ b/locales/ja/settings.json @@ -797,5 +797,6 @@ "voice.modelDownload.error": "ダウンロードに失敗しました: {{error}}", "voice.modelDownload.progress": "{{percent}}% 完了", "voice.modelDownload.retry": "リトライ", - "voice.modelDownload.title": "音声モデルのダウンロード" + "voice.modelDownload.title": "音声モデルのダウンロード", + "settings.about.productName": "WorldScript Studio" } diff --git a/locales/pt/settings.json b/locales/pt/settings.json index e2c7c7380..24f970344 100644 --- a/locales/pt/settings.json +++ b/locales/pt/settings.json @@ -797,5 +797,6 @@ "voice.modelDownload.error": "Falha no download: {{error}}", "voice.modelDownload.progress": "{{percent}}% concluído", "voice.modelDownload.retry": "Tentar novamente", - "voice.modelDownload.title": "Download do modelo de voz" + "voice.modelDownload.title": "Download do modelo de voz", + "settings.about.productName": "WorldScript Studio" } diff --git a/locales/zh/settings.json b/locales/zh/settings.json index 07d2eb9e3..940319595 100644 --- a/locales/zh/settings.json +++ b/locales/zh/settings.json @@ -797,5 +797,6 @@ "voice.modelDownload.error": "下载失败:{{error}}", "voice.modelDownload.progress": "{{percent}}% 完成", "voice.modelDownload.retry": "重试", - "voice.modelDownload.title": "语音模型下载" + "voice.modelDownload.title": "语音模型下载", + "settings.about.productName": "WorldScript Studio" } diff --git a/public/locales/ar/bundle.json b/public/locales/ar/bundle.json index e6a2c7883..8b47c0558 100644 --- a/public/locales/ar/bundle.json +++ b/public/locales/ar/bundle.json @@ -2265,6 +2265,7 @@ "voice.modelDownload.progress": "{{percent}}% مكتمل", "voice.modelDownload.retry": "إعادة المحاولة", "voice.modelDownload.title": "تحميل نموذج الصوت", + "settings.about.productName": "WorldScript Studio", "help.advanced.adaptiveAi.content": "The Adaptive AI engine detects your hardware at runtime — WebGPU, WebNN, DirectML, CPU cores, VRAM tier, and battery — and routes local inference to the fastest available backend. Enable it under Settings → Early Access Features and inspect the live readout under Settings → AI. Eco Mode picks the smallest viable model to save battery and memory, and a tab-leader election ensures only one browser tab uses the GPU at a time to avoid VRAM collisions.", "help.advanced.adaptiveAi.title": "Adaptive AI, GPU & Eco Mode", "help.advanced.cloudSync.content": "Optional end-to-end encrypted sync keeps your library in step across devices using a Cloudflare R2 backend. Enable “Cloud sync” under Settings → Early Access Features and configure it under Settings → Connections. Project data is encrypted locally with AES-256-GCM before upload, and your AI API keys are never sent to the cloud. Sync is entirely opt-in; with it off, WorldScript remains a fully offline-first app.", diff --git a/public/locales/de/bundle.json b/public/locales/de/bundle.json index 8a84f6c21..e49aa15cc 100644 --- a/public/locales/de/bundle.json +++ b/public/locales/de/bundle.json @@ -2265,6 +2265,7 @@ "voice.modelDownload.progress": "{{percent}}% abgeschlossen", "voice.modelDownload.retry": "Wiederholen", "voice.modelDownload.title": "Sprachmodell-Download", + "settings.about.productName": "WorldScript Studio", "help.advanced.adaptiveAi.content": "Die adaptive KI-Engine erkennt deine Hardware zur Laufzeit – WebGPU, WebNN, DirectML, CPU-Kerne, VRAM-Stufe und Akku – und leitet die lokale Inferenz an das schnellste verfügbare Backend. Aktiviere sie unter Einstellungen → Early-Access-Funktionen und prüfe die Live-Anzeige unter Einstellungen → KI. Der Eco-Modus wählt das kleinste brauchbare Modell, um Akku und Speicher zu schonen, und eine Tab-Leader-Wahl stellt sicher, dass nur ein Browser-Tab gleichzeitig die GPU nutzt, um VRAM-Kollisionen zu vermeiden.", "help.advanced.adaptiveAi.title": "Adaptive KI, GPU & Eco-Modus", "help.advanced.cloudSync.content": "Die optionale Ende-zu-Ende-verschlüsselte Synchronisierung hält deine Bibliothek über mehrere Geräte hinweg aktuell – mit einem Cloudflare-R2-Backend. Aktiviere „Cloud-Sync“ unter Einstellungen → Early-Access-Funktionen und richte sie unter Einstellungen → Verbindungen ein. Projektdaten werden vor dem Hochladen lokal mit AES-256-GCM verschlüsselt, und deine KI-API-Schlüssel werden nie in die Cloud gesendet. Die Synchronisierung ist komplett optional; ohne sie bleibt WorldScript eine vollständig offline-first arbeitende App.", diff --git a/public/locales/el/bundle.json b/public/locales/el/bundle.json index 94473d399..3bcb5c630 100644 --- a/public/locales/el/bundle.json +++ b/public/locales/el/bundle.json @@ -2265,6 +2265,7 @@ "voice.modelDownload.progress": "{{percent}}% ολοκληρώθηκε", "voice.modelDownload.retry": "Δοκιμάζω πάλι", "voice.modelDownload.title": "Λήψη φωνητικού μοντέλου", + "settings.about.productName": "WorldScript Studio", "help.advanced.adaptiveAi.content": "The Adaptive AI engine detects your hardware at runtime — WebGPU, WebNN, DirectML, CPU cores, VRAM tier, and battery — and routes local inference to the fastest available backend. Enable it under Ρυθμίσεις → Early Access Features and inspect the live readout under Ρυθμίσεις → AI. Eco Mode picks the smallest viable model to save battery and memory, and a tab-leader election ensures only one browser tab uses the GPU at a time to avoid VRAM collisions.", "help.advanced.adaptiveAi.title": "Adaptive AI, GPU & Eco Mode", "help.advanced.cloudSync.content": "Optional end-to-end encrypted sync keeps your library in step across devices using a Cloudflare R2 backend. Enable “Cloud sync” under Ρυθμίσεις → Early Access Features and configure it under Ρυθμίσεις → Connections. Project data is encrypted locally with AES-256-GCM before upload, and your AI API keys are never sent to the cloud. Sync is entirely opt-in; with it off, WorldScript remains a fully offline-first app.", diff --git a/public/locales/en/bundle.json b/public/locales/en/bundle.json index 14871ef32..ee55d204b 100644 --- a/public/locales/en/bundle.json +++ b/public/locales/en/bundle.json @@ -2265,6 +2265,7 @@ "settings.ai.localAi.modelLabel.gemma3_1b": "Gemma 3 1B (~0.8 GB)", "settings.ai.localAi.modelLabel.gemma3_4b": "Gemma 3 4B (~4.9 GB)", "settings.ai.localAi.modelLabel.llama33_70b": "Llama 3.3 70B (high-end, ~35 GB)", + "settings.about.productName": "WorldScript Studio", "help.advanced.adaptiveAi.content": "The Adaptive AI engine detects your hardware at runtime — WebGPU, WebNN, DirectML, CPU cores, VRAM tier, and battery — and routes local inference to the fastest available backend. Enable it under Settings → Early Access Features and inspect the live readout under Settings → AI. Eco Mode picks the smallest viable model to save battery and memory, and a tab-leader election ensures only one browser tab uses the GPU at a time to avoid VRAM collisions.", "help.advanced.adaptiveAi.title": "Adaptive AI, GPU & Eco Mode", "help.advanced.cloudSync.content": "Optional end-to-end encrypted sync keeps your library in step across devices using a Cloudflare R2 backend. Enable “Cloud sync” under Settings → Early Access Features and configure it under Settings → Connections. Project data is encrypted locally with AES-256-GCM before upload, and your AI API keys are never sent to the cloud. Sync is entirely opt-in; with it off, WorldScript remains a fully offline-first app.", diff --git a/public/locales/es/bundle.json b/public/locales/es/bundle.json index 73f24e23d..79cf77cc7 100644 --- a/public/locales/es/bundle.json +++ b/public/locales/es/bundle.json @@ -2265,6 +2265,7 @@ "voice.modelDownload.progress": "{{percent}}% completado", "voice.modelDownload.retry": "Reintentar", "voice.modelDownload.title": "Descarga de modelo de voz", + "settings.about.productName": "WorldScript Studio", "help.advanced.adaptiveAi.content": "El motor de IA adaptativa detecta tu hardware en tiempo de ejecución —WebGPU, WebNN, DirectML, núcleos de CPU, nivel de VRAM y batería— y enruta la inferencia local al backend más rápido disponible. Actívalo en Ajustes → Funciones de acceso anticipado y consulta la lectura en vivo en Ajustes → IA. El modo Eco elige el modelo más pequeño viable para ahorrar batería y memoria, y una elección de pestaña líder garantiza que solo una pestaña del navegador use la GPU a la vez para evitar colisiones de VRAM.", "help.advanced.adaptiveAi.title": "IA adaptativa, GPU y modo Eco", "help.advanced.cloudSync.content": "La sincronización opcional cifrada de extremo a extremo mantiene tu biblioteca al día en todos tus dispositivos mediante un backend de Cloudflare R2. Activa «Sincronización en la nube» en Ajustes → Funciones de acceso anticipado y configúrala en Ajustes → Conexiones. Los datos del proyecto se cifran localmente con AES-256-GCM antes de subirse, y tus claves de API de IA nunca se envían a la nube. La sincronización es totalmente opcional; sin ella, WorldScript sigue siendo una app totalmente «offline-first».", diff --git a/public/locales/fr/bundle.json b/public/locales/fr/bundle.json index 5fc48d463..033e657b0 100644 --- a/public/locales/fr/bundle.json +++ b/public/locales/fr/bundle.json @@ -2265,6 +2265,7 @@ "voice.modelDownload.progress": "{{percent}}% terminé", "voice.modelDownload.retry": "Réessayer", "voice.modelDownload.title": "Téléchargement du modèle vocal", + "settings.about.productName": "WorldScript Studio", "help.advanced.adaptiveAi.content": "Le moteur d’IA adaptatif détecte votre matériel à l’exécution — WebGPU, WebNN, DirectML, cœurs CPU, niveau de VRAM et batterie — et achemine l’inférence locale vers le backend le plus rapide disponible. Activez-le dans Paramètres → Fonctionnalités en accès anticipé et consultez l’affichage en direct dans Paramètres → IA. Le mode Éco choisit le plus petit modèle viable pour économiser batterie et mémoire, et une élection de l’onglet meneur garantit qu’un seul onglet du navigateur utilise le GPU à la fois pour éviter les collisions de VRAM.", "help.advanced.adaptiveAi.title": "IA adaptative, GPU et mode Éco", "help.advanced.cloudSync.content": "La synchronisation chiffrée de bout en bout, optionnelle, maintient votre bibliothèque à jour sur tous vos appareils via un backend Cloudflare R2. Activez « Synchronisation cloud » dans Paramètres → Fonctionnalités en accès anticipé et configurez-la dans Paramètres → Connexions. Les données du projet sont chiffrées localement en AES-256-GCM avant l’envoi, et vos clés d’API IA ne sont jamais transmises au cloud. La synchronisation est entièrement optionnelle ; sans elle, WorldScript reste une application entièrement « offline-first ».", diff --git a/public/locales/he/bundle.json b/public/locales/he/bundle.json index ae8e386f3..2651dc9e1 100644 --- a/public/locales/he/bundle.json +++ b/public/locales/he/bundle.json @@ -2265,6 +2265,7 @@ "voice.modelDownload.progress": "{{percent}}% הושלם", "voice.modelDownload.retry": "נסה שוב", "voice.modelDownload.title": "הורדת מודל קול", + "settings.about.productName": "WorldScript Studio", "help.advanced.adaptiveAi.content": "The Adaptive AI engine detects your hardware at runtime — WebGPU, WebNN, DirectML, CPU cores, VRAM tier, and battery — and routes local inference to the fastest available backend. Enable it under Settings → Early Access Features and inspect the live readout under Settings → AI. Eco Mode picks the smallest viable model to save battery and memory, and a tab-leader election ensures only one browser tab uses the GPU at a time to avoid VRAM collisions.", "help.advanced.adaptiveAi.title": "Adaptive AI, GPU & Eco Mode", "help.advanced.cloudSync.content": "Optional end-to-end encrypted sync keeps your library in step across devices using a Cloudflare R2 backend. Enable “Cloud sync” under Settings → Early Access Features and configure it under Settings → Connections. Project data is encrypted locally with AES-256-GCM before upload, and your AI API keys are never sent to the cloud. Sync is entirely opt-in; with it off, WorldScript remains a fully offline-first app.", diff --git a/public/locales/it/bundle.json b/public/locales/it/bundle.json index 71c7957e8..88d1edce5 100644 --- a/public/locales/it/bundle.json +++ b/public/locales/it/bundle.json @@ -2265,6 +2265,7 @@ "voice.modelDownload.progress": "{{percent}}% completato", "voice.modelDownload.retry": "Riprova", "voice.modelDownload.title": "Download modello vocale", + "settings.about.productName": "WorldScript Studio", "help.advanced.adaptiveAi.content": "Il motore IA adattivo rileva l’hardware in fase di esecuzione — WebGPU, WebNN, DirectML, core CPU, livello VRAM e batteria — e instrada l’inferenza locale al backend più veloce disponibile. Attivalo in Impostazioni → Funzioni ad accesso anticipato e controlla la lettura in tempo reale in Impostazioni → IA. La modalità Eco sceglie il modello più piccolo utilizzabile per risparmiare batteria e memoria, e un’elezione della scheda capofila garantisce che solo una scheda del browser usi la GPU alla volta per evitare collisioni di VRAM.", "help.advanced.adaptiveAi.title": "IA adattiva, GPU e modalità Eco", "help.advanced.cloudSync.content": "La sincronizzazione opzionale crittografata end-to-end mantiene allineata la tua libreria su più dispositivi tramite un backend Cloudflare R2. Attiva «Sincronizzazione cloud» in Impostazioni → Funzioni ad accesso anticipato e configurala in Impostazioni → Connessioni. I dati del progetto vengono crittografati localmente con AES-256-GCM prima del caricamento e le tue chiavi API dell’IA non vengono mai inviate al cloud. La sincronizzazione è del tutto facoltativa; senza di essa WorldScript resta un’app completamente offline-first.", diff --git a/public/locales/ja/bundle.json b/public/locales/ja/bundle.json index 63e9e720e..ff38538eb 100644 --- a/public/locales/ja/bundle.json +++ b/public/locales/ja/bundle.json @@ -2265,6 +2265,7 @@ "voice.modelDownload.progress": "{{percent}}% 完了", "voice.modelDownload.retry": "リトライ", "voice.modelDownload.title": "音声モデルのダウンロード", + "settings.about.productName": "WorldScript Studio", "help.advanced.adaptiveAi.content": "The Adaptive AI engine detects your hardware at runtime — WebGPU, WebNN, DirectML, CPU cores, VRAM tier, and battery — and routes local inference to the fastest available backend. Enable it under 設定 → Early Access Features and inspect the live readout under 設定 → AI. Eco Mode picks the smallest viable model to save battery and memory, and a tab-leader election ensures only one browser tab uses the GPU at a time to avoid VRAM collisions.", "help.advanced.adaptiveAi.title": "アダプティブ AI、GPU、エコモード", "help.advanced.cloudSync.content": "Optional end-to-end encrypted sync keeps your library in step across devices using a Cloudflare R2 backend. Enable “Cloud sync” under 設定 → Early Access Features and configure it under 設定 → Connections. Project data is encrypted locally with AES-256-GCM before upload, and your AI API keys are never sent to the cloud. Sync is entirely opt-in; with it off, WorldScript remains a fully offline-first app.", diff --git a/public/locales/pt/bundle.json b/public/locales/pt/bundle.json index d9232220a..dc473ac2d 100644 --- a/public/locales/pt/bundle.json +++ b/public/locales/pt/bundle.json @@ -2265,6 +2265,7 @@ "voice.modelDownload.progress": "{{percent}}% concluído", "voice.modelDownload.retry": "Tentar novamente", "voice.modelDownload.title": "Download do modelo de voz", + "settings.about.productName": "WorldScript Studio", "help.advanced.adaptiveAi.content": "The Adaptive IA engine detects your hardware at runtime — WebGPU, WebNN, DirectML, CPU cores, VRAM tier, and battery — and routes local inference to the fastest available backend. Enable it under Configurações → Early Access Features and inspect the live readout under Configurações → IA. Eco Mode picks the smallest viable model to save battery and memory, and a tab-leader election ensures only one browser tab uses the GPU at a time to avoid VRAM collisions.", "help.advanced.adaptiveAi.title": "Adaptive IA, GPU & Eco Mode", "help.advanced.cloudSync.content": "Optional end-to-end encrypted sync keeps your library in step across devices using a Cloudflare R2 backend. Enable “Cloud sync” under Configurações → Early Access Features and configure it under Configurações → Connections. Project data is encrypted locally with AES-256-GCM before upload, and your IA API keys are never sent to the cloud. Sync is entirely opt-in; with it off, WorldScript remains a fully offline-first app.", diff --git a/public/locales/zh/bundle.json b/public/locales/zh/bundle.json index 9d583f99f..b210d49a1 100644 --- a/public/locales/zh/bundle.json +++ b/public/locales/zh/bundle.json @@ -2265,6 +2265,7 @@ "voice.modelDownload.progress": "{{percent}}% 完成", "voice.modelDownload.retry": "重试", "voice.modelDownload.title": "语音模型下载", + "settings.about.productName": "WorldScript Studio", "help.advanced.adaptiveAi.content": "The Adaptive AI engine detects your hardware at runtime — WebGPU, WebNN, DirectML, CPU cores, VRAM tier, and battery — and routes local inference to the fastest available backend. Enable it under 设置 → Early Access Features and inspect the live readout under 设置 → AI. Eco Mode picks the smallest viable model to save battery and memory, and a tab-leader election ensures only one browser tab uses the GPU at a time to avoid VRAM collisions.", "help.advanced.adaptiveAi.title": "自适应 AI、GPU 和 Eco 模式", "help.advanced.cloudSync.content": "Optional end-to-end encrypted sync keeps your library in step across devices using a Cloudflare R2 backend. Enable “Cloud sync” under 设置 → Early Access Features and configure it under 设置 → Connections. Project data is encrypted locally with AES-256-GCM before upload, and your AI API keys are never sent to the cloud. Sync is entirely opt-in; with it off, WorldScript remains a fully offline-first app.", diff --git a/tests/unit/hooks/useTemplateView.test.ts b/tests/unit/hooks/useTemplateView.test.ts index 267b84a22..989b1c1ac 100644 --- a/tests/unit/hooks/useTemplateView.test.ts +++ b/tests/unit/hooks/useTemplateView.test.ts @@ -204,11 +204,15 @@ describe('applyCommunityTemplate', () => { (call: unknown[]) => (call[0] as { type?: string } | undefined)?.type === 'project/setManuscript', ); - const manuscript = (manuscriptCall?.[0] as { payload: Array<{ content: string }> } | undefined) - ?.payload; + const manuscript = ( + manuscriptCall?.[0] as { payload: Array<{ content: string; prompt: string }> } | undefined + )?.payload; expect(manuscript).toHaveLength(2); - expect(manuscript?.[0]?.content).toContain('The crew assembles'); + // Guidance goes into the writing brief (prompt); the manuscript content starts empty. + expect(manuscript?.[0]?.content).toBe(''); + expect(manuscript?.[0]?.prompt).toContain('The crew assembles'); expect(manuscript?.[1]?.content).toBe(''); + expect(manuscript?.[1]?.prompt).toBe(''); expect(mockDispatch).toHaveBeenCalledWith( expect.objectContaining({ type: 'project/setOutline' }), ); diff --git a/tests/unit/settings/GeneralSections.test.tsx b/tests/unit/settings/GeneralSections.test.tsx index 03cd9bc59..d48a16743 100644 --- a/tests/unit/settings/GeneralSections.test.tsx +++ b/tests/unit/settings/GeneralSections.test.tsx @@ -6,6 +6,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { beforeEach, describe, expect, it, vi } from 'vitest'; +import packageJson from '../../../package.json'; // --------------------------------------------------------------------------- // Mocks @@ -30,7 +31,7 @@ const defaultMockSettings = () => ({ vi.mock('../../../contexts/SettingsViewContext', () => ({ useSettingsViewContext: () => ({ - t: (k: string) => k, + t: (k: string) => (k === 'settings.about.productName' ? 'WorldScript Studio' : k), language: 'en', handleLanguageChange: mockHandleLanguageChange, settings: { ...defaultMockSettings(), ...settingsRef.current }, @@ -210,8 +211,12 @@ describe('AboutSection', () => { it('renders app version information', async () => { render(); - // App name should be present - expect(screen.getByText(/WorldScript/i)).toBeInTheDocument(); + // QNBS-v3: assert the exact product display name so a regression (e.g. dropping "Studio" + // or rendering a different label that still contains "WorldScript") fails the test. + expect(screen.getByText('WorldScript Studio')).toBeInTheDocument(); + // QNBS-v3 (CodeAnt): also assert the app version actually renders, so a version regression + // (e.g. packageJson.version no longer shown) fails this test as its name implies. + expect(screen.getByText(packageJson.version, { exact: false })).toBeInTheDocument(); }); });