"use client"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { makeAssistantToolUI } from "@assistant-ui/react"; import { AlertCircleIcon, Download, Film, Loader2, Presentation, X, } from "lucide-react"; import { z } from "zod"; import { Spinner } from "@/components/ui/spinner"; import { baseApiService } from "@/lib/apis/base-api.service"; import { authenticatedFetch } from "@/lib/auth-utils"; import { compileCheck, compileToComponent } from "@/lib/remotion/compile-check"; import { FPS } from "@/lib/remotion/constants"; import { CombinedPlayer, buildCompositionComponent, type CompiledSlide, } from "./combined-player"; const GenerateVideoPresentationArgsSchema = z.object({ source_content: z.string(), video_title: z.string().nullish(), user_prompt: z.string().nullish(), }); const GenerateVideoPresentationResultSchema = z.object({ status: z.enum(["pending", "generating", "ready", "failed"]), video_presentation_id: z.number().nullish(), title: z.string().nullish(), message: z.string().nullish(), error: z.string().nullish(), }); const VideoPresentationStatusResponseSchema = z.object({ status: z.enum(["pending", "generating", "ready", "failed"]), id: z.number(), title: z.string(), slides: z .array( z.object({ slide_number: z.number(), title: z.string(), subtitle: z.string().nullish(), content_in_markdown: z.string().nullish(), speaker_transcripts: z.array(z.string()).nullish(), background_explanation: z.string().nullish(), audio_url: z.string().nullish(), duration_seconds: z.number().nullish(), duration_in_frames: z.number().nullish(), }), ) .nullish(), scene_codes: z .array( z.object({ slide_number: z.number(), code: z.string(), title: z.string().nullish(), }), ) .nullish(), slide_count: z.number().nullish(), }); type GenerateVideoPresentationArgs = z.infer; type GenerateVideoPresentationResult = z.infer; type VideoPresentationStatusResponse = z.infer; function parseStatusResponse(data: unknown): VideoPresentationStatusResponse | null { const result = VideoPresentationStatusResponseSchema.safeParse(data); if (!result.success) { console.warn("Invalid video presentation status:", result.error.issues); return null; } return result.data; } function GeneratingState({ title }: { title: string }) { return (

{title}

Generating video presentation. This may take a few minutes.
); } function ErrorState({ title, error }: { title: string; error: string }) { return (

{title}

Failed to generate video presentation

{error}

); } function CompilationLoadingState({ title }: { title: string }) { return (

{title}

Compiling scenes...
); } function VideoPresentationPlayer({ presentationId, title, }: { presentationId: number; title: string; }) { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [compiledSlides, setCompiledSlides] = useState([]); const [isRendering, setIsRendering] = useState(false); const [renderProgress, setRenderProgress] = useState(null); const [renderError, setRenderError] = useState(null); const [renderFormat, setRenderFormat] = useState(null); const abortControllerRef = useRef(null); const [isPptxExporting, setIsPptxExporting] = useState(false); const [pptxProgress, setPptxProgress] = useState(null); const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL ?? ""; const audioBlobUrlsRef = useRef([]); const loadPresentation = useCallback(async () => { setIsLoading(true); setError(null); try { const raw = await baseApiService.get( `/api/v1/video-presentations/${presentationId}`, ); const data = parseStatusResponse(raw); if (!data) throw new Error("Invalid response"); if (data.status !== "ready") throw new Error(`Unexpected status: ${data.status}`); if (!data.slides?.length || !data.scene_codes?.length) { throw new Error("No slides or scene codes in response"); } const sceneMap = new Map(data.scene_codes.map((sc) => [sc.slide_number, sc])); const compiled: CompiledSlide[] = []; for (const slide of data.slides) { const scene = sceneMap.get(slide.slide_number); if (!scene) continue; const durationInFrames = slide.duration_in_frames ?? 300; const check = compileCheck(scene.code); if (!check.success) { console.warn( `Slide ${slide.slide_number} failed to compile: ${check.error}`, ); continue; } const component = compileToComponent(scene.code, durationInFrames); compiled.push({ component, title: scene.title ?? slide.title, code: scene.code, durationInFrames, audioUrl: slide.audio_url ? `${backendUrl}${slide.audio_url}` : undefined, }); } if (compiled.length === 0) { throw new Error("No slides compiled successfully"); } // Pre-fetch all audio files with auth headers and convert to blob URLs. // Remotion's