"use client"; import type { ToolCallMessagePartProps } from "@assistant-ui/react"; import { Dot, Download, Loader2, Presentation, X } from "lucide-react"; import { useParams, usePathname } from "next/navigation"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { z } from "zod"; import { TextShimmerLoader } from "@/components/prompt-kit/loader"; import { Button } from "@/components/ui/button"; 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 { buildCompositionComponent, buildSlideWithWatermark, CombinedPlayer, type CompiledSlide, } from "./combined-player"; import { getPptxExportErrorToast, getVideoDownloadErrorToast } from "./errors"; 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}

); } function ErrorState({ title, error }: { title: string; error: string }) { return (

Video Generation Failed

{title}

{error}

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

{title}

); } function VideoPresentationPlayer({ presentationId, title, shareToken, }: { presentationId: number; title: string; shareToken?: string | null; }) { 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 [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 apiPath = shareToken ? `/api/v1/public/${shareToken}/video-presentations/${presentationId}` : `/api/v1/video-presentations/${presentationId}`; const raw = await baseApiService.get(apiPath); 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 audio and convert to blob URLs. // For public routes the audio endpoints don't need auth, but we // still use blob URLs so Remotion's plain