diff --git a/docs/development/INTERNATIONALIZATION.md b/docs/development/INTERNATIONALIZATION.md index fba7ca1c..be34727c 100644 --- a/docs/development/INTERNATIONALIZATION.md +++ b/docs/development/INTERNATIONALIZATION.md @@ -17,8 +17,13 @@ Typed JSON under `ui/src/i18n/locales//.json`. | `activity` | Activity footer, Inbox / Legacy, Extra info inspector, Assets catalog chrome | | `extraInfo` | Video Extra info dialog body (title stays `activity.extraInfo`) | | `storyLab` | Story Lab section chrome, world/characters/relationships/structure tabs, reference gallery and editors | -| `director` | Director queue recovery and related chrome | -| `seriesLab` | Series Lab tabs, review assembly and shot-duration chrome | +| `director` | Director queue recovery, spoken-language chrome and direct-video T2V mode | +| `seriesLab` | Series Lab tabs, review assembly, shot-duration chrome and spoken-language labels | +| `videoEditor` | Video Editor transitions, time cards, remake and trim chrome | +| `workspaces` | Production runs panel and Workspace collections | +| `styleSheet` | Style sheet library, import and delete chrome | +| `projects` | Durable projects catalog | +| `auditDev` | Internal audio-hallucination audit panel | Add a namespace only when a new product surface needs its own file. Do not grow a single giant JSON. diff --git a/tests/test_video_editor_replacement_ui.py b/tests/test_video_editor_replacement_ui.py index 41a49922..9e328971 100644 --- a/tests/test_video_editor_replacement_ui.py +++ b/tests/test_video_editor_replacement_ui.py @@ -13,7 +13,10 @@ def test_selected_montage_clip_can_be_opened_in_video_creation(): editor = EDITOR.read_text(encoding="utf-8") - assert "Rehacer en Creación de vídeo" in editor + assert "t('remake.action')" in editor + assert '"action": "Rehacer en Creación de vídeo"' in ( + ROOT / "ui" / "src" / "i18n" / "locales" / "es" / "videoEditor.json" + ).read_text(encoding="utf-8") assert "fetchOutputMetadata(outputName, activeWorkspace)" in editor assert "loadSettingsFromOutput()" in editor assert "writeVideoEditorReplacementTarget" in editor diff --git a/ui/e2e/helpers/apiRoutes.ts b/ui/e2e/helpers/apiRoutes.ts index 05111d8b..e12845ff 100644 --- a/ui/e2e/helpers/apiRoutes.ts +++ b/ui/e2e/helpers/apiRoutes.ts @@ -122,6 +122,19 @@ const EMPTY_STORY_LIBRARY = { projects: {}, } +const EMPTY_WIZARD_CONVERSATION = { + version: 1, + revision: 0, + messages: [], + executions: [], +} + +const EMPTY_WIZARD_WORKFLOWS = { + version: 1, + revision: 0, + workflows: [], +} + function json(body: Json, status = 200) { return { status, @@ -191,6 +204,10 @@ function exactCatalog(): Record | { sse: true }> 'GET /api/v1/loras/installed': json({ loras: [], manifest_last_check_at: null }), 'GET /api/v1/tasks': json({ workspace: 'default', tasks: [], latest_event_id: 0 }), 'GET /api/v1/tasks/events': { sse: true }, + 'GET /api/v1/wizard/conversations': json(EMPTY_WIZARD_CONVERSATION), + 'PUT /api/v1/wizard/conversations': json(EMPTY_WIZARD_CONVERSATION), + 'GET /api/v1/wizard/workflows': json(EMPTY_WIZARD_WORKFLOWS), + 'PUT /api/v1/wizard/workflows': json(EMPTY_WIZARD_WORKFLOWS), 'GET /api/v1/stories/library': json(EMPTY_STORY_LIBRARY), 'PUT /api/v1/stories/library': json(EMPTY_STORY_LIBRARY), 'GET /api/v1/resolutions': json({ resolutions: [] }), diff --git a/ui/e2e/helpers/gotoApp.ts b/ui/e2e/helpers/gotoApp.ts index 71ded7f9..a78a2c02 100644 --- a/ui/e2e/helpers/gotoApp.ts +++ b/ui/e2e/helpers/gotoApp.ts @@ -22,6 +22,9 @@ export async function gotoApp(page: Page): Promise { // Reduced-motion intro may already have dismissed itself. } await expect(page.getByText('HocusPocus UI failed to load')).toHaveCount(0) + const studios = page.getByRole('button', { name: 'Studios' }) + await expect(studios).toBeVisible({ timeout: 15_000 }) + await studios.click() await expect(page.getByRole('tab', { name: 'Story Lab' })).toBeVisible({ timeout: 15_000 }) return session } diff --git a/ui/e2e/specs/boot.spec.ts b/ui/e2e/specs/boot.spec.ts index 1be8262e..c112fa02 100644 --- a/ui/e2e/specs/boot.spec.ts +++ b/ui/e2e/specs/boot.spec.ts @@ -24,6 +24,7 @@ test('boots Story Lab against a simulated API', async ({ page }) => { await expect(page.getByText('HocusPocus could not reach its local server.')).toHaveCount(0) await expect(page.getByRole('heading', { name: 'HocusPocus LAN access' })).toHaveCount(0) + await page.getByRole('button', { name: 'Studios' }).click() const storyTab = page.getByRole('tab', { name: 'Story Lab' }) await expect(storyTab).toBeVisible() await storyTab.click() diff --git a/ui/e2e/specs/story-lab-music-productions.spec.ts b/ui/e2e/specs/story-lab-music-productions.spec.ts index 5b72a609..4c28534b 100644 --- a/ui/e2e/specs/story-lab-music-productions.spec.ts +++ b/ui/e2e/specs/story-lab-music-productions.spec.ts @@ -4,6 +4,7 @@ import { closeApp, gotoApp } from '../helpers/gotoApp' test('opens extracted Story Lab Music, Trailer and Productions chrome', async ({ page }) => { const session = await gotoApp(page) + await page.getByRole('button', { name: 'Studios' }).click() const storyTab = page.getByRole('tab', { name: 'Story Lab' }) await expect(storyTab).toBeVisible() await storyTab.click() diff --git a/ui/scripts/check-i18n-catalogs.mjs b/ui/scripts/check-i18n-catalogs.mjs index c878653e..04af4c90 100644 --- a/ui/scripts/check-i18n-catalogs.mjs +++ b/ui/scripts/check-i18n-catalogs.mjs @@ -4,7 +4,7 @@ import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') -const NAMESPACES = ['common', 'navigation', 'settings', 'wizard', 'activity', 'extraInfo', 'storyLab', 'director', 'seriesLab'] +const NAMESPACES = ['common', 'navigation', 'settings', 'wizard', 'activity', 'extraInfo', 'storyLab', 'director', 'seriesLab', 'videoEditor', 'workspaces', 'styleSheet', 'projects', 'auditDev'] const LANGUAGES = ['en', 'es'] function load(language, namespace) { @@ -99,6 +99,13 @@ const PILOT_FILES = [ 'src/features/stories/CompactSubjectEditor.tsx', 'src/features/stories/CompactBeatEditor.tsx', 'src/features/stories/StoryVideoFormatControls.tsx', + 'src/components/Sidebar/DirectorChat.tsx', + 'src/features/video-editor/VideoEditorPanel.tsx', + 'src/features/workspaces/WorkspacesPanel.tsx', + 'src/features/workspaceCollections/WorkspaceCollectionsPanel.tsx', + 'src/features/styles/StyleSheetPanel.tsx', + 'src/features/projects/ProjectsPanel.tsx', + 'src/features/auditdev/AuditDevPanel.tsx', ] const FORBIDDEN = [ @@ -127,6 +134,15 @@ const FORBIDDEN = [ 'Montaje ordenado', 'Usar en Montaje', 'Hay una cola de generación por recuperar', + 'Idioma hablado del vídeo', + 'Vídeo directo · T2V puro · sin imágenes', + 'Vídeo directo · text only, no images', + 'Momentos después…', + 'Editar vídeo en Video Editor', + 'Buscar run…', + 'Nuevo workspace…', + 'Hoja de estilos', + 'Auditoría interna: marca los clips', ] export function forbiddenLiterals() { diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 7091b56d..6377a5e6 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,6 +1,7 @@ import { lazy, Suspense, useCallback, useEffect, useState } from 'react' import { Menu, Settings } from 'lucide-react' import { Sidebar } from './components/Sidebar/Sidebar' +import { WizardSidebar } from './components/Sidebar/WizardSidebar' import { MainContent } from './components/MainContent/MainContent' import { SettingsDrawer } from './components/SettingsDrawer/SettingsDrawer' import { LoraBrowser } from './components/LoraBrowser/LoraBrowser' @@ -181,6 +182,7 @@ function AppContent() { )}
+
diff --git a/ui/src/components/ActivityFooter.tsx b/ui/src/components/ActivityFooter.tsx index 2a477f09..b5a9d66a 100644 --- a/ui/src/components/ActivityFooter.tsx +++ b/ui/src/components/ActivityFooter.tsx @@ -1,17 +1,13 @@ -import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { AlertCircle, CheckCircle2, ChevronDown, ChevronUp, CircleSlash2, Copy, Eraser, ListVideo, Loader2 } from 'lucide-react' import * as api from '../api/client' import type { CanonicalTask } from '../api/client' import { applyCanonicalTaskEvent, canResumeCanonicalTask, canonicalTaskVisualState, reconcileCanonicalTaskSnapshot } from '../lib/canonicalTaskEvents' import { formatAppAction, formatAppTimestamp } from '../lib/locale' import { useStore } from '../stores/useStore' -import { AgentAvatar } from '../features/agent/AgentAvatar' import { listenForAgentActivityDetails } from '../features/agent/agentUiBus' import { useUiTranslation } from '../i18n' - -const AgentAssistantPanel = lazy(() => - import('../features/agent/AgentAssistantPanel').then(module => ({ default: module.AgentAssistantPanel })), -) +import { publishCanonicalTasks } from '../features/activity/canonicalTaskFeed' const ACTIVE = new Set(['created', 'queued', 'waiting_resource', 'running']) const CONNECTED_RECONCILE_MS = 60_000 @@ -228,7 +224,6 @@ function truncatePrompt(prompt: string, limit = 180): string { } export function ActivityFooter() { - const { t: tWizard } = useUiTranslation('wizard') const { t: tActivity } = useUiTranslation('activity') const activeWorkspace = useStore(state => state.activeWorkspace) const workspaceRef = useRef(activeWorkspace) @@ -237,7 +232,6 @@ export function ActivityFooter() { const [tasks, setTasks] = useState([]) const tasksRef = useRef([]) const [detailsOpen, setDetailsOpen] = useState(false) - const [agentOpen, setAgentOpen] = useState(false) const [clock, setClock] = useState(Date.now()) const [busyIds, setBusyIds] = useState>(() => new Set()) const [controlFailures, setControlFailures] = useState>({}) @@ -263,6 +257,7 @@ export function ActivityFooter() { const commitTasks = (next: CanonicalTask[]) => { tasksRef.current = next setTasks(next) + publishCanonicalTasks(next) } const refresh = async (): Promise => { if (refreshPending) return null @@ -332,6 +327,7 @@ export function ActivityFooter() { tasksRef.current = [] setTasks([]) + publishCanonicalTasks([]) setControlFailures({}) void connectAfterSnapshot() return () => { @@ -456,15 +452,6 @@ export function ActivityFooter() { return (