From b9f13fe59c8603c37a9b679173006a0cd2dc3d4e Mon Sep 17 00:00:00 2001 From: Yamen Ahmad Date: Sun, 12 Apr 2026 02:59:40 +0400 Subject: [PATCH] feat: add video polling and inline player for ELAI-generated speaking videos Made-with: Cursor --- src/pages/GenerationPage.tsx | 54 ++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/src/pages/GenerationPage.tsx b/src/pages/GenerationPage.tsx index 210deafb6..8c78355dc 100644 --- a/src/pages/GenerationPage.tsx +++ b/src/pages/GenerationPage.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useMutation } from "@tanstack/react-query"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -318,6 +318,45 @@ export default function GenerationPage() { onError: (err: Error) => toast({ variant: "destructive", title: "Video generation failed", description: err.message }), }); + const pollTimerRef = useRef | null>(null); + useEffect(() => { + if (activeModule !== "speaking") return; + const st = getModuleState("speaking"); + const pendingParts = st.speakingParts + .map((p, i) => ({ videoUrl: p.videoUrl, index: i })) + .filter((p) => p.videoUrl.startsWith("pending:")); + if (pendingParts.length === 0) { + if (pollTimerRef.current) { clearInterval(pollTimerRef.current); pollTimerRef.current = null; } + return; + } + if (pollTimerRef.current) return; + pollTimerRef.current = setInterval(async () => { + const currentSt = getModuleState("speaking"); + let changed = false; + const updatedParts = [...currentSt.speakingParts]; + for (const pp of pendingParts) { + const part = updatedParts[pp.index]; + if (!part || !part.videoUrl.startsWith("pending:")) continue; + const videoId = part.videoUrl.replace("pending:", ""); + try { + const status = await mediaService.getVideoStatus(videoId); + if (status.status === "done" || status.status === "completed" || status.video_url) { + updatedParts[pp.index] = { ...part, videoUrl: status.video_url || status.url || "" }; + changed = true; + toast({ title: "Video ready!", description: "Avatar video has been generated." }); + } else if (status.status === "error" || status.status === "failed") { + updatedParts[pp.index] = { ...part, videoUrl: "" }; + changed = true; + toast({ variant: "destructive", title: "Video generation failed" }); + } + } catch { /* poll again next interval */ } + } + if (changed) updateModuleState("speaking", { speakingParts: updatedParts }); + }, 15000); + return () => { if (pollTimerRef.current) { clearInterval(pollTimerRef.current); pollTimerRef.current = null; } }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeModule, moduleStates]); + const submitMut = useMutation({ mutationFn: (skipApproval: boolean) => { const modulesPayload: Record = {}; @@ -858,7 +897,18 @@ export default function GenerationPage() { {generateVideoMut.isPending ? :