import React, { useCallback, useEffect, useState } from "react"; import { http, getApiBase, getSessionToken } from "../lib/api"; import { parseAgentStatus } from "../lib/agentStatus"; import { EmoLogo } from "./EmoLogo"; import { Copy, Check, RefreshCw, Download, ShieldAlert, Apple, Box, Cpu, Monitor, Link2 } from "lucide-react"; import { toast } from "sonner"; const detectOS = () => { if (typeof navigator === "undefined") return "windows"; const ua = navigator.userAgent || ""; if (/Win/i.test(ua)) { const plat = navigator.userAgentData?.platform || navigator.platform || ""; if (/arm|aarch/i.test(plat)) return "windows-arm"; return "windows"; } if (/Mac/i.test(ua)) { const plat = navigator.userAgentData?.platform || navigator.platform || ""; return /arm|aarch/i.test(plat) ? "macos-arm" : "macos"; } if (/Linux/i.test(ua)) { const plat = navigator.userAgentData?.platform || ""; return /arm|aarch/i.test(plat) ? "linux-arm" : "linux"; } return "windows"; }; const OS_OPTIONS = [ { id: "windows", label: "Windows x64", filename: "Emo-Desktop.zip", icon: Box }, { id: "windows-arm", label: "Windows ARM", filename: "Emo-Desktop.zip", icon: Box }, { id: "macos", label: "macOS Intel", filename: "Emo-Desktop.zip", icon: Apple }, { id: "macos-arm", label: "macOS Apple Silicon", filename: "Emo-Desktop.zip", icon: Apple }, { id: "linux", label: "Linux x64", filename: "Emo-Desktop.zip", icon: Cpu }, { id: "linux-arm", label: "Linux ARM64", filename: "Emo-Desktop.zip", icon: Cpu }, ]; const DOWNLOAD_NAMES = Object.fromEntries(OS_OPTIONS.map((o) => [o.id, o.filename])); export default function AgentSettingsPanel({ agentOnline, onRefreshStatus }) { const [token, setToken] = useState(""); const [copied, setCopied] = useState(""); const [os, setOs] = useState(detectOS()); const [downloading, setDownloading] = useState(false); const [downloadingZip, setDownloadingZip] = useState(false); const [status, setStatus] = useState({ connected: false, online: false, desktopOnline: false, linked: false, context: null, }); const refreshLocal = useCallback(async () => { try { const r = await http.get("/agent/status"); const parsed = parseAgentStatus(r.data); setStatus({ connected: parsed.desktopOnline || parsed.desktopLinked, online: parsed.agentToolsOnline, desktopOnline: parsed.desktopOnline, linked: parsed.desktopLinked, context: r.data.context || null, }); } catch { /* ignore */ } }, []); useEffect(() => { http.get("/agent/token").then((r) => setToken(r.data.agent_token)); refreshLocal(); const id = setInterval(refreshLocal, 5000); return () => clearInterval(id); }, [refreshLocal]); const connected = status.desktopOnline || agentOnline; const copy = async (text, key) => { try { await navigator.clipboard.writeText(text); setCopied(key); setTimeout(() => setCopied(""), 1500); } catch { toast.error("Copie impossible"); } }; const rotate = async () => { if (!window.confirm("Régénérer le token ? L'ancien agent sera invalidé.")) return; const r = await http.post("/agent/token/rotate"); setToken(r.data.agent_token); toast.success("Token régénéré"); }; const fetchAgentDownload = async (url, fallbackFilename) => { const headers = {}; const session = getSessionToken(); if (session) { headers.Authorization = `Bearer ${session}`; headers["X-Emo-Session"] = session; } const resp = await fetch(url, { credentials: "include", headers }); if (!resp.ok) { let detail = `HTTP ${resp.status}`; try { const err = await resp.json(); detail = err.detail || detail; } catch (_) {} throw new Error(typeof detail === "string" ? detail : "Téléchargement impossible"); } const blob = await resp.blob(); const buf = await blob.arrayBuffer(); const bytes = new Uint8Array(buf); const isZipFile = bytes[0] === 0x50 && bytes[1] === 0x4b; const isZip = isZipFile || resp.headers.get("content-type")?.includes("zip") || resp.headers.get("content-disposition")?.includes(".zip"); const cd = resp.headers.get("content-disposition") || ""; const cdMatch = cd.match(/filename="?([^";]+)"?/i); const filename = cdMatch?.[1] || (isZip ? "Emo-Desktop.zip" : fallbackFilename); const outBlob = new Blob([buf], { type: isZip ? "application/zip" : blob.type }); const a = document.createElement("a"); a.href = URL.createObjectURL(outBlob); a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(a.href); return { isZip, isFallback: resp.headers.get("X-Emo-Fallback") === "python-zip" }; }; const downloadDesktopExe = async () => { setDownloading(true); try { const { isZip, isFallback } = await fetchAgentDownload( `${getApiBase()}/agent/desktop-exe/windows`, "Emo-Desktop.exe" ); if (isZip || isFallback) { toast.success("Version Python (zip) — extrais et lance start.bat"); } else { toast.success("Emo-Desktop.exe téléchargé — double-clic pour lancer"); } } catch (e) { toast.error(e.message || "Téléchargement impossible"); } finally { setDownloading(false); } }; const downloadAgentZip = async () => { setDownloadingZip(true); try { const { isZip } = await fetchAgentDownload( `${getApiBase()}/agent/binary/${os}`, "Emo-Desktop.zip" ); toast.success( isZip ? "Émo Desktop (Python) téléchargé — extrais et lance start.bat ou start.sh" : "Émo Desktop téléchargé" ); } catch (e) { toast.error(e.message || "Téléchargement impossible"); } finally { setDownloadingZip(false); } }; const isWindows = os === "windows" || os === "windows-arm"; const handleRefresh = () => { refreshLocal(); onRefreshStatus?.(); }; const hostname = status.context?.hostname || status.context?.os || ""; return (
{hostname}
) : null} {!connected && (Lance Émo Desktop sur ce PC, clique link, connecte-toi sur le site et appuie sur Accepter.
)}
{token || "…"}