Spaces:
Sleeping
Sleeping
| // ─── 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<ImageGenState | null>(null); | |
| const dismissRef = useRef<ReturnType<typeof setTimeout> | null>(null); | |
| const timerRef = useRef<ReturnType<typeof setInterval> | 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<ImageGenEvent>).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 ( | |
| <div style={{ | |
| position: "fixed", | |
| bottom: 168, | |
| right: 12, | |
| zIndex: Z_INDEX.CRITICAL_OVERLAY, | |
| maxWidth: 280, | |
| width: "calc(100vw - 24px)", | |
| pointerEvents: "none", | |
| }}> | |
| {/* shimmer keyframe */} | |
| <style>{` | |
| @keyframes imgGenSlide { | |
| 0% { left: -65%; } | |
| 100% { left: 105%; } | |
| } | |
| `}</style> | |
| <div style={{ | |
| background: "rgba(14,14,26,0.96)", | |
| backdropFilter: "blur(12px)", | |
| WebkitBackdropFilter: "blur(12px)", // B4: Safari iOS | |
| border: `1px solid ${borderColor}`, | |
| borderRadius: 10, | |
| padding: "0.55rem 0.75rem", | |
| boxShadow: "0 6px 28px rgba(0,0,0,0.55)", | |
| }}> | |
| {/* ── Header row ── */} | |
| <div style={{ display:"flex", alignItems:"center", gap:8, marginBottom: isDone ? 0 : 6 }}> | |
| <span style={{ fontSize:"0.85rem", lineHeight:1, flexShrink:0 }}> | |
| {isError ? "⚠️" : isDone ? "✅" : isRetry ? "⏳" : "🎨"} | |
| </span> | |
| <span style={{ | |
| fontSize:"0.72rem", | |
| color: isError ? "#fca5a5" : isDone ? "#86efac" : "#c5b8f7", | |
| flex:1, overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap", fontWeight:600, | |
| }}> | |
| {label} | |
| </span> | |
| {!isDone && !isError && ( | |
| <span style={{ fontSize:"0.68rem", color:"#6b6b7b", fontVariantNumeric:"tabular-nums", flexShrink:0 }}> | |
| {elapsed}s | |
| </span> | |
| )} | |
| {isDone && ( | |
| <span style={{ fontSize:"0.68rem", color:"#4ade80", flexShrink:0 }}>pronto</span> | |
| )} | |
| </div> | |
| {/* ── Subtext ── */} | |
| {subtext && ( | |
| <div style={{ | |
| fontSize:"0.67rem", | |
| color: isError ? "#fca5a5" : "#6b6b7b", | |
| marginBottom: isError ? 0 : 6, | |
| lineHeight:1.4, | |
| }}> | |
| {subtext} | |
| </div> | |
| )} | |
| {/* ── Shimmer bar ── */} | |
| {!isDone && !isError && ( | |
| <div style={{ height:3, background:"rgba(255,255,255,0.07)", borderRadius:2, overflow:"hidden", position:"relative" }}> | |
| <div style={{ | |
| height:"100%", borderRadius:2, | |
| background:"linear-gradient(90deg,transparent 0%,#7c4dff 40%,#a388f7 60%,transparent 100%)", | |
| width:"65%", position:"absolute", | |
| animation:"imgGenSlide 1.7s ease-in-out infinite", | |
| }} /> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |