/** * DownloadProjectButton.tsx — S363-Blueprint: Final Artifact * * "Instant Download ZIP" — l'ultima frase deve sempre consegnare un artefatto. * Mostra un bottone compatto che scarica il VFS come ZIP. * Appare in fondo al messaggio finale quando ci sono file nel VFS. */ import { useState, memo } from "react"; import { makeTimedSignal } from "@/lib/agentLoop/networkConstants"; interface Props { conversationId: string; fileCount?: number; } export const DownloadProjectButton = memo(function DownloadProjectButton({ conversationId, fileCount }: Props) { const [loading, setLoading] = useState(false); const [done, setDone] = useState(false); const handleDownload = async () => { setLoading(true); try { const backendUrl = (window as Window & { __AGENTE_BACKEND_URL?: string }).__AGENTE_BACKEND_URL || import.meta.env.VITE_BACKEND_URL || "https://agente-ai.pages.dev"; const url = `${backendUrl}/api/project/export?conversation_id=${encodeURIComponent(conversationId)}`; // S485-fix: AbortSignal.timeout evita hang infinito se il backend non risponde const res = await fetch(url, { signal: makeTimedSignal(30_000) }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const blob = await res.blob(); const _zipFilename = `project_${conversationId.slice(0, 8)}.zip`; const _zipUrl = URL.createObjectURL(blob); // S725-iOS-FIX: a.click() dopo fetch bloccato su Safari iOS — delega a DownloadQueue window.dispatchEvent(new CustomEvent("agent:download-ready", { detail: { url: _zipUrl, filename: _zipFilename } })); setDone(true); setTimeout(() => setDone(false), 3000); } catch (e) { console.error("Download failed:", e); } finally { setLoading(false); } }; return ( ); }); export default DownloadProjectButton;