Spaces:
Sleeping
Sleeping
| import { memo, useState } from "react"; | |
| import { generateImage, IMAGE_GEN_MODELS, DEFAULT_IMAGE_MODEL } from "@/lib/imageGen"; | |
| import { getHuggingFaceToken as getToken } from "@/lib/apiKeys"; | |
| interface ImageGenBlockProps { | |
| prompt: string; | |
| } | |
| const ImageGenBlock = memo(function ImageGenBlock({ prompt }: ImageGenBlockProps) { | |
| const [status, setStatus] = useState<"idle" | "loading" | "done" | "error">("idle"); | |
| const [imageUrl, setImageUrl] = useState<string | null>(null); | |
| const [error, setError] = useState<string | null>(null); | |
| const [model, setModel] = useState(DEFAULT_IMAGE_MODEL.id); | |
| // S725-IMG1: imageGen.ts ora restituisce data URL — nessuna revoca necessaria | |
| const generate = async () => { | |
| const token = getToken(); | |
| if (!token) { setError("Token HuggingFace mancante. Vai in Impostazioni."); setStatus("error"); return; } | |
| setStatus("loading"); | |
| setError(null); | |
| try { | |
| const result = await generateImage(token, prompt.trim(), model); | |
| setImageUrl(result.url); | |
| setStatus("done"); | |
| // S725-IMG1: auto-save nel VFS — riutilizzabile dall'agente in step successivi | |
| const _vfsPath = `images/generated_${Date.now()}.png`; | |
| import("@/lib/vfsDb").then(({ vfsAsync }) => | |
| vfsAsync.write(_vfsPath, result.url, "image/png").catch(() => {}) | |
| ).catch(() => {}); | |
| } catch (err) { | |
| setError(err instanceof Error ? err.message : "Errore generazione"); | |
| setStatus("error"); | |
| } | |
| }; | |
| const download = () => { | |
| if (!imageUrl) return; | |
| const _fn = `image-${Date.now()}.png`; | |
| // S725-XLSX1: tenta a.click() (desktop) + agent:download-ready per iOS DownloadQueue | |
| try { | |
| const a = document.createElement("a"); | |
| a.href = imageUrl; a.download = _fn; | |
| document.body.appendChild(a); a.click(); document.body.removeChild(a); | |
| } catch { /* noop */ } | |
| window.dispatchEvent(new CustomEvent("agent:download-ready", { detail: { url: imageUrl, filename: _fn } })); | |
| }; | |
| return ( | |
| <div style={{ | |
| background: "#0d0d18", border: "1px solid #1e1e2e", borderRadius: 12, | |
| marginBottom: "0.6em", overflow: "hidden", | |
| }}> | |
| <div style={{ padding: "0.55rem 0.85rem", background: "#0a0a14", borderBottom: "1px solid #1e1e2e", display: "flex", alignItems: "center", gap: 8 }}> | |
| <span style={{ fontSize: "0.7rem", fontWeight: 700, color: "#7c4dff" }}>IMG</span> | |
| <span style={{ fontSize: "0.78rem", color: "#9090a8", fontWeight: 600 }}>Generazione immagine AI</span> | |
| <span style={{ flex: 1 }} /> | |
| {status === "idle" && ( | |
| <select value={model} onChange={e => setModel(e.target.value)} style={{ | |
| background: "rgba(255,255,255,0.05)", border: "1px solid #2a2a40", borderRadius: 5, | |
| color: "#9090a8", fontSize: "0.7rem", padding: "2px 6px", cursor: "pointer", | |
| }}> | |
| {IMAGE_GEN_MODELS.map(m => <option key={m.id} value={m.id}>{m.label}</option>)} | |
| </select> | |
| )} | |
| </div> | |
| <div style={{ padding: "0.75rem 0.85rem" }}> | |
| <div style={{ fontSize: "0.78rem", color: "#6b6b7b", fontStyle: "italic", marginBottom: 10, lineHeight: 1.4 }}> | |
| "{prompt.slice(0, 200)}{prompt.length > 200 ? "..." : ""}" | |
| </div> | |
| {status === "idle" && ( | |
| <button onClick={generate} style={{ | |
| all: "unset", cursor: "pointer", background: "rgba(124,77,255,0.12)", | |
| border: "1px solid rgba(124,77,255,0.25)", color: "#a388f7", | |
| padding: "0.42rem 1rem", borderRadius: 8, fontSize: "0.8rem", fontWeight: 600, | |
| transition: "background 0.15s", | |
| }}>Genera immagine</button> | |
| )} | |
| {status === "loading" && ( | |
| <div style={{ display: "flex", alignItems: "center", gap: 8, color: "#9090a8", fontSize: "0.8rem" }}> | |
| <span className="spin">⟳</span> Generazione in corso... (30-60s) | |
| </div> | |
| )} | |
| {status === "error" && ( | |
| <div> | |
| <div style={{ color: "#fca5a5", fontSize: "0.8rem", marginBottom: 8 }}>Err: {error}</div> | |
| <button onClick={generate} style={{ | |
| all: "unset", cursor: "pointer", color: "#4f8ef7", fontSize: "0.78rem", | |
| padding: "3px 8px", borderRadius: 5, background: "rgba(79,142,247,0.1)", | |
| }}>Riprova</button> | |
| </div> | |
| )} | |
| {status === "done" && imageUrl && ( | |
| <div> | |
| <img src={imageUrl} alt={prompt} loading="lazy" decoding="async" style={{ | |
| width: "100%", maxWidth: 480, borderRadius: 8, display: "block", | |
| border: "1px solid #2a2a3e", | |
| }} /> | |
| <div style={{ display: "flex", gap: 8, marginTop: 8 }}> | |
| <button onClick={download} style={{ | |
| all: "unset", cursor: "pointer", color: "#4f8ef7", fontSize: "0.75rem", | |
| padding: "3px 8px", borderRadius: 5, background: "rgba(79,142,247,0.1)", | |
| border: "1px solid rgba(79,142,247,0.2)", | |
| }}>⬇ Scarica</button> | |
| <button onClick={() => setStatus("idle")} style={{ | |
| all: "unset", cursor: "pointer", color: "#9090a8", fontSize: "0.75rem", | |
| padding: "3px 8px", | |
| }}>↺ Rigenera</button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| ); | |
| export default ImageGenBlock; | |