"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { AlertCircle, Cloud, Download, LoaderCircle, Play, Sparkles } from "lucide-react"; import { useJobPolling } from "@/hooks/useJobPolling"; import { generateBrainVideoPlan } from "@/lib/api/brain"; import { resolveMediaUrl, normalizeApiError } from "@/lib/api/client"; import { createVideoRenderJob, createVideoScenePlan, getVideoRenderJob } from "@/lib/api/video"; import type { BrainVideoPlanResponse, VideoFormat, VideoPlanDurationType, VideoRenderJob, VideoScenePlanResponse, } from "@/lib/api/types"; import { prepareScenePlanForRender } from "@/lib/video/durations"; type RenderStage = "planning" | "voice" | "building" | "ready" | "failed"; type Props = { topic: string; sourceId: string | null; minutes: number; format?: "16:9" | "9:16"; }; function modeForMinutes(minutes: number): VideoPlanDurationType { if (minutes <= 1) return "quick_concept"; if (minutes <= 5) return "quick_concept"; if (minutes >= 30) return "deep_masterclass"; return "exam_focus"; } function stageLabel(stage: RenderStage, job: VideoRenderJob | null): string { if (stage === "ready") return "Ready"; if (stage === "failed") return "Failed"; if (stage === "planning") return "Planning lesson"; if (stage === "voice") return "Generating voice"; if (stage === "building") { const step = job?.current_step ?? ""; if (step === "rendering_video") return "Rendering video"; if (step === "validating_output") return "Validating output"; if (step === "uploading_or_saving") return "Saving file"; return "Building video"; } return "Working"; } function scenePlanFromBrainPlan( plan: BrainVideoPlanResponse, language: string, visualStyle: string, format: VideoFormat, ): VideoScenePlanResponse { const scenes = plan.scenes.map((scene, index) => { const durationSeconds = Number(scene.duration_seconds) || 30; const subtitleText = Array.isArray(scene.subtitle_segments) ? scene.subtitle_segments.join(" ") : scene.narration; return { scene_id: index + 1, type: scene.scene_type || "explain", duration_seconds: durationSeconds, screen_text: scene.on_screen_text || scene.title, voice_text: scene.narration, subtitle_text: subtitleText, subtitle_segments: [{ start: 0, end: durationSeconds, text: subtitleText }], keywords: plan.revision_keywords.slice(0, 5), visual_hint: scene.visual_direction || visualStyle, purpose: scene.exam_tip || scene.title, }; }); const totalDuration = scenes.reduce((sum, scene) => sum + scene.duration_seconds, 0); return { title: plan.title, duration_minutes: Math.max(0.1, Math.round((totalDuration / 60) * 10) / 10), total_duration_seconds: totalDuration, language, style: plan.narration_style, visual_style: visualStyle, video_format: format, learning_goal: plan.exam_focus ? "Exam-focused lesson" : "Concept lesson", voice_mode: "english_soft", scenes, recap: plan.revision_keywords, quick_test: plan.quiz_moments, evidence_label: plan.evidence_label, }; } export function InlineVideoRender({ topic, sourceId, minutes, format = "16:9" }: Props) { const [stage, setStage] = useState("planning"); const [job, setJob] = useState(null); const [scenePlan, setScenePlan] = useState(null); const [error, setError] = useState(null); const startedRef = useRef(false); const pollFetcher = useCallback(async () => { if (!job) throw new Error("No job"); return getVideoRenderJob(job.id); }, [job]); useJobPolling({ enabled: Boolean(job && (job.status === "queued" || job.status === "running")), fetcher: pollFetcher, shouldContinue: (next) => next.status === "queued" || next.status === "running", onData: (next) => { setJob(next); if (next.status === "ready") setStage("ready"); else if (next.status === "failed") { setStage("failed"); setError("Render failed. Try a different source or topic."); } else if (next.current_step?.startsWith("generating_audio")) setStage("voice"); else setStage("building"); }, onError: () => setError("Connection lost. Refresh to check status."), }); const run = useCallback(async () => { try { setStage("planning"); setError(null); const mode = modeForMinutes(minutes); const language = "English"; const visualStyle = "clean_explainer"; const brainPlan = await generateBrainVideoPlan({ topic, video_mode: mode, teaching_style: "visual_tutor", language_preference: language, format, source_id: sourceId, source_ids: sourceId ? [sourceId] : [], level: "intermediate", goal: "good marks", }); const targetSeconds = minutes * 60; let plan: VideoScenePlanResponse; if (sourceId) { try { const generated = await createVideoScenePlan({ document_id: sourceId, title: brainPlan.title, language, duration_minutes: minutes, visual_style: visualStyle, video_format: format, }); plan = prepareScenePlanForRender(generated, targetSeconds); } catch { plan = prepareScenePlanForRender( scenePlanFromBrainPlan(brainPlan, language, visualStyle, format), targetSeconds, ); } } else { plan = prepareScenePlanForRender( scenePlanFromBrainPlan(brainPlan, language, visualStyle, format), targetSeconds, ); } setScenePlan(plan); setStage("voice"); const nextJob = await createVideoRenderJob({ scene_plan: plan, title: plan.title, allow_mock_audio: process.env.NODE_ENV !== "production", voice_mode: "english_soft", voice: "calm_teacher", language: plan.language, }); setJob(nextJob); setStage(nextJob.status === "ready" ? "ready" : "building"); } catch (caught) { setError(`Could not build video. ${normalizeApiError(caught)}`); setStage("failed"); } }, [format, minutes, sourceId, topic]); useEffect(() => { if (startedRef.current) return; startedRef.current = true; void run(); }, [run]); const progress = job?.progress ?? (stage === "planning" ? 20 : stage === "voice" ? 45 : stage === "building" ? 70 : 0); const finalUrl = resolveMediaUrl(job?.public_url || job?.download_url); const isWorking = stage === "planning" || stage === "voice" || stage === "building"; return (
Study video

{scenePlan?.title || topic}

{minutes}min · {format}
{finalUrl && stage === "ready" ? (
Download MP4 {job?.storage_provider === "cloudinary" || /res\.cloudinary\.com/i.test(finalUrl) ? ( Saved to cloud ) : null} {job?.duration_seconds ? ( {Math.round(job.duration_seconds)}s · {job.scene_count} scenes ) : null}
) : null} {isWorking ? (

{stageLabel(stage, job)}

{Math.round(progress)}%
{scenePlan ? (

{scenePlan.scenes.length} scenes · {Math.round(scenePlan.total_duration_seconds ?? 0)}s lesson

) : (

{sourceId ? "Reading source" : "Working from topic"} · this can take a few minutes

)}
) : null} {stage === "failed" || error ? (

{error || "Render failed."}

) : null} {stage === "ready" && !finalUrl ? (
Lesson plan built. Video render unavailable — try again later.
) : null}
); }