feat: add video polling and inline player for ELAI-generated speaking videos
Made-with: Cursor
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
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 }),
|
onError: (err: Error) => toast({ variant: "destructive", title: "Video generation failed", description: err.message }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pollTimerRef = useRef<ReturnType<typeof setInterval> | 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({
|
const submitMut = useMutation({
|
||||||
mutationFn: (skipApproval: boolean) => {
|
mutationFn: (skipApproval: boolean) => {
|
||||||
const modulesPayload: Record<string, unknown> = {};
|
const modulesPayload: Record<string, unknown> = {};
|
||||||
@@ -858,7 +897,18 @@ export default function GenerationPage() {
|
|||||||
{generateVideoMut.isPending ? <Loader2 className="h-3 w-3 mr-1 animate-spin" /> : <Video className="h-3 w-3 mr-1" />}
|
{generateVideoMut.isPending ? <Loader2 className="h-3 w-3 mr-1 animate-spin" /> : <Video className="h-3 w-3 mr-1" />}
|
||||||
Generate Video
|
Generate Video
|
||||||
</Button>
|
</Button>
|
||||||
{part.videoUrl && <p className="text-xs text-green-600">Video: {part.videoUrl.startsWith("pending:") ? "Processing..." : "Ready"}</p>}
|
{part.videoUrl && part.videoUrl.startsWith("pending:") && (
|
||||||
|
<div className="flex items-center gap-1 mt-1">
|
||||||
|
<Loader2 className="h-3 w-3 animate-spin text-orange-500" />
|
||||||
|
<p className="text-xs text-orange-600">Video processing... (polling every 15s)</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{part.videoUrl && !part.videoUrl.startsWith("pending:") && (
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
<p className="text-xs text-green-600">Video ready</p>
|
||||||
|
<video controls src={part.videoUrl} className="w-full rounded max-h-48" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user