/** * Runs the /preflight sequence in front of the user before a wizard run * actually starts. Three checks + one soft-fail (PowerPoint) gated on the * chosen output format. */ import { useEffect, useState } from 'react' import { CheckCircle2, Loader2, Server, Cpu, Sparkles, Presentation, Film, XCircle, AlertTriangle } from 'lucide-react' import { api } from '../api/client' import type { OutputFormat, PreflightResponse } from '../api/types' import { useFocusTrap } from '../hooks/useFocusTrap' type CheckStatus = 'idle' | 'running' | 'pass' | 'fail' | 'warn' interface CheckRow { key: keyof PreflightResponse['checks'] label: string icon: typeof Server } const ROWS: CheckRow[] = [ { key: 'platform', label: 'Platform', icon: Cpu }, { key: 'backend', label: 'Backend connection', icon: Server }, { key: 'ai_config', label: 'AI configuration', icon: Sparkles }, { key: 'powerpoint', label: 'PowerPoint availability', icon: Presentation }, { key: 'video_engine', label: 'Video engine', icon: Film }, ] function statusDot(s: CheckStatus) { if (s === 'running') return if (s === 'pass') return if (s === 'warn') return if (s === 'fail') return return } interface PreflightModalProps { outputFormat: OutputFormat onCancel: () => void onProceed: () => void } const OUTPUT_LABELS: Record = { html: 'HTML file', images: 'screenshots', pptx: 'PowerPoint deck', video: 'MP4 video', } export default function PreflightModal({ outputFormat, onCancel, onProceed }: PreflightModalProps) { const [data, setData] = useState(null) const [loadingKey, setLoadingKey] = useState('platform') const [error, setError] = useState(null) const dialogRef = useFocusTrap(true) useEffect(() => { let cancelled = false ;(async () => { try { // Walk through the visual rows so the user sees each one light up in // sequence, even though the backend reports them all at once. // Always request fresh — the user is gating a run behind this // modal and needs the actual current state of the backend, not a // 30-second-stale cached result. const res = await api.preflight({ fresh: true }) if (cancelled) return for (const row of ROWS) { setLoadingKey(row.key) await new Promise((r) => setTimeout(r, 350)) if (cancelled) return } setLoadingKey(null) setData(res) } catch (e) { if (cancelled) return setError(e instanceof Error ? e.message : String(e)) setLoadingKey(null) } })() return () => { cancelled = true } }, []) // Escape key closes the modal. useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onCancel() } window.addEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey) }, [onCancel]) const needsPpt = outputFormat === 'pptx' const needsVideo = outputFormat === 'video' const pptOk = data?.checks.powerpoint.ok ?? false const videoEngineOk = data?.checks.video_engine?.ok ?? pptOk const aiOk = data?.checks.ai_config.ok ?? false const backendOk = !!data const blocked = !data ? false : !backendOk || !aiOk || (needsPpt && !pptOk) || (needsVideo && !videoEngineOk) const rowStatus = (key: keyof PreflightResponse['checks']): CheckStatus => { if (!data) { if (loadingKey === key) return 'running' const idx = ROWS.findIndex((r) => r.key === loadingKey) const myIdx = ROWS.findIndex((r) => r.key === key) if (idx >= 0 && myIdx < idx) return 'pass' return 'idle' } const c = data.checks[key] if (!c) return 'idle' if (c.ok) return 'pass' // Soft-fail: PowerPoint only matters for PPTX export; video_engine // only matters for MP4. Anything else is always hard-required. if (key === 'powerpoint' && !needsPpt) return 'warn' if (key === 'video_engine' && !needsVideo) return 'warn' return 'fail' } return (

Pre-flight checks

Verifying the runtime can actually produce{' '} {OUTPUT_LABELS[outputFormat]}.

    {ROWS.map((row) => { const s = rowStatus(row.key) const detail = data?.checks[row.key]?.detail ?? '' const Icon = row.icon return (
  • {statusDot(s)}
    {row.label} {row.key === 'powerpoint' && !needsPpt && ( optional for {OUTPUT_LABELS[outputFormat]} )}
    {detail && (

    {detail}

    )}
  • ) })}
{error && (
Preflight request failed: {error}
)} {data && blocked && (
{!aiOk &&

AI config is missing — edit backend/config/config.py and restart the backend.

} {needsPpt && !pptOk && (

Output is {OUTPUT_LABELS[outputFormat]} but PowerPoint isn't available. Go back to step 1 and pick HTML file or{' '} screenshots, or install PowerPoint on a Windows host.

)}
)}
) }