AUDIT / src /components /ImageGenProgressToast.tsx
Arypulka98's picture
feat(audit): deploy full backend cluster node
aea470f verified
Raw
History Blame Contribute Delete
6.43 kB
// ─── 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>
);
}