Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions docs/development/INTERNATIONALIZATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@ Typed JSON under `ui/src/i18n/locales/<lng>/<namespace>.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.

Expand Down
5 changes: 4 additions & 1 deletion tests/test_video_editor_replacement_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions ui/e2e/helpers/apiRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -191,6 +204,10 @@ function exactCatalog(): Record<string, ReturnType<typeof json> | { 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: [] }),
Expand Down
3 changes: 3 additions & 0 deletions ui/e2e/helpers/gotoApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export async function gotoApp(page: Page): Promise<ApiRouteSession> {
// 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
}
Expand Down
1 change: 1 addition & 0 deletions ui/e2e/specs/boot.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions ui/e2e/specs/story-lab-music-productions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
18 changes: 17 additions & 1 deletion ui/scripts/check-i18n-catalogs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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() {
Expand Down
2 changes: 2 additions & 0 deletions ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -181,6 +182,7 @@ function AppContent() {
)}

<div className="flex flex-1 min-h-0 w-full">
<WizardSidebar />
<Sidebar />
<MainContent />
</div>
Expand Down
36 changes: 5 additions & 31 deletions ui/src/components/ActivityFooter.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -237,7 +232,6 @@ export function ActivityFooter() {
const [tasks, setTasks] = useState<CanonicalTask[]>([])
const tasksRef = useRef<CanonicalTask[]>([])
const [detailsOpen, setDetailsOpen] = useState(false)
const [agentOpen, setAgentOpen] = useState(false)
const [clock, setClock] = useState(Date.now())
const [busyIds, setBusyIds] = useState<Set<string>>(() => new Set())
const [controlFailures, setControlFailures] = useState<Record<string, TaskControlFailure>>({})
Expand All @@ -263,6 +257,7 @@ export function ActivityFooter() {
const commitTasks = (next: CanonicalTask[]) => {
tasksRef.current = next
setTasks(next)
publishCanonicalTasks(next)
}
const refresh = async (): Promise<number | null> => {
if (refreshPending) return null
Expand Down Expand Up @@ -332,6 +327,7 @@ export function ActivityFooter() {

tasksRef.current = []
setTasks([])
publishCanonicalTasks([])
setControlFailures({})
void connectAfterSnapshot()
return () => {
Expand Down Expand Up @@ -456,15 +452,6 @@ export function ActivityFooter() {

return (
<footer className="relative h-10 shrink-0 border-t border-border bg-bg-secondary px-3 sm:px-4 flex items-center gap-3 text-[10px] z-40">
{agentOpen && (
<Suspense fallback={null}>
<AgentAssistantPanel
workspace={activeWorkspace}
tasks={tasks}
onClose={() => setAgentOpen(false)}
/>
</Suspense>
)}
{detailsOpen && roots.length > 0 && (
<div className="absolute bottom-full left-3 mb-2 w-[min(48rem,calc(100vw-1.5rem))] max-h-80 overflow-y-auto rounded-lg border border-border bg-bg-secondary p-2 shadow-2xl">
<div className="mb-1.5 flex items-center justify-between px-1">
Expand Down Expand Up @@ -631,20 +618,7 @@ export function ActivityFooter() {
</div>
)}

<div className="flex h-full w-44 shrink-0 items-center border-r border-amber-200/15 pr-3">
<button
type="button"
onClick={() => { setAgentOpen(open => !open); setDetailsOpen(false) }}
className={`hp-agent-trigger flex w-full items-center gap-2 rounded-lg border px-2 py-0.5 text-left transition ${agentOpen ? 'border-amber-200/45 bg-amber-200/10 text-amber-50' : 'border-amber-200/15 bg-amber-100/[.025] text-amber-100/75 hover:border-amber-200/35 hover:bg-amber-100/[.055] hover:text-amber-50'}`}
aria-expanded={agentOpen}
title={tWizard('open')}
>
<AgentAvatar state={agentOpen ? 'listening' : 'idle'} size={24} />
<span className="font-medium whitespace-nowrap">{tWizard('title')}</span>
</button>
</div>

<button type="button" onClick={() => { setDetailsOpen(open => !open); setAgentOpen(false) }} className="flex items-center gap-1.5 shrink-0" aria-expanded={detailsOpen} title={tActivity('openHistory')}>
<button type="button" onClick={() => setDetailsOpen(open => !open)} className="flex items-center gap-1.5 shrink-0" aria-expanded={detailsOpen} title={tActivity('openHistory')}>
{isActive
? <Loader2 size={13} className="animate-spin text-accent-blue" />
: hasError
Expand Down
Loading