// ─── ImageGenProgressToast.tsx ────────────────────────────────────────────── // S728: Progress toast generazione immagini AI — timer live, shimmer, dark-glass. // Ascolta agent:image-gen-progress { model, label, phase, attempt?, error? }. // start → timer parte, barra shimmer indeterminate. // retry → ⏳ "Modello in avvio… (tentativo N/3)", timer continua. // done → ✅ auto-dismiss 2.5s. // error → ⚠️ auto-dismiss 5s (Pollinations fallback dispatcha subito start). // Posizionato a bottom:168 — sopra ZipProgressToast (120) senza sovrapposizione. // ───────────────────────────────────────────────────────────────────────────── import { useEffect, useRef, useState } from "react"; import { Z_INDEX } from "@/lib/zindex"; interface ImageGenEvent { model: string; label: string; phase: "start" | "retry" | "done" | "error"; attempt?: number; error?: string; } interface ImageGenState { label: string; phase: "generating" | "retry" | "done" | "error"; attempt: number; error?: string; startMs: number; elapsed: number; } export default function ImageGenProgressToast() { const [state, setState] = useState(null); const dismissRef = useRef | null>(null); const timerRef = useRef | null>(null); function _clear() { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } if (dismissRef.current) { clearTimeout(dismissRef.current); dismissRef.current = null; } } useEffect(() => { const handler = (e: Event) => { const { model, label, phase, attempt, error } = (e as CustomEvent).detail ?? {}; if (!model) return; if (phase === "start") { _clear(); const startMs = Date.now(); setState({ label, phase: "generating", attempt: 0, startMs, elapsed: 0 }); timerRef.current = setInterval(() => { setState(prev => prev ? { ...prev, elapsed: Math.floor((Date.now() - prev.startMs) / 1_000) } : prev, ); }, 1_000); } else if (phase === "retry") { setState(prev => prev ? { ...prev, phase: "retry", attempt: attempt ?? 1 } : prev, ); } else if (phase === "done") { _clear(); setState(prev => prev ? { ...prev, phase: "done" } : prev); dismissRef.current = setTimeout(() => setState(null), 2_500); } else if (phase === "error") { _clear(); setState(prev => prev ? { ...prev, phase: "error", error } : { label, phase: "error", attempt: 0, error, startMs: 0, elapsed: 0 }, ); dismissRef.current = setTimeout(() => setState(null), 5_000); } }; window.addEventListener("agent:image-gen-progress", handler); return () => { window.removeEventListener("agent:image-gen-progress", handler); _clear(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); if (!state) return null; const { label, phase, attempt, error, elapsed } = state; const isDone = phase === "done"; const isError = phase === "error"; const isRetry = phase === "retry"; const borderColor = isError ? "rgba(248,113,113,0.35)" : isDone ? "rgba(74,222,128,0.3)" : "rgba(124,77,255,0.35)"; const subtext = isError ? String(error ?? "Errore generazione").slice(0, 100) : isRetry ? `Modello in avvio… (tentativo ${attempt}/3)` : isDone ? "" : "Generazione in corso…"; return (
{/* shimmer keyframe */}
{/* ── Header row ── */}
{isError ? "⚠️" : isDone ? "✅" : isRetry ? "⏳" : "🎨"} {label} {!isDone && !isError && ( {elapsed}s )} {isDone && ( pronto )}
{/* ── Subtext ── */} {subtext && (
{subtext}
)} {/* ── Shimmer bar ── */} {!isDone && !isError && (
)}
); }