Spaces:
Sleeping
Sleeping
| /** | |
| * ModePerformancePanel.tsx — S436 | |
| * | |
| * Pannello analisi performance per i 3 modi agente: | |
| * 💬 Chat — risposta diretta LLM, veloce, no tool | |
| * 🧠 Think — ragionamento esteso, più preciso | |
| * 🤖 Agent — multi-step con tool, massima capacità | |
| * | |
| * Dati reali: telemetryDb (TTFT), taskStore (task completati), | |
| * benchmark live (latenza ping backend). | |
| * Zero fetch non-necessari. Non-fatal se backend non disponibile. | |
| */ | |
| import { memo, useState, useEffect, useCallback, useRef } from "react"; | |
| import { telemetryDb } from "@/lib/vfsDb"; | |
| import { useTaskStore } from "@/store/taskStore"; | |
| import { makeTimedSignal } from "@/lib/agentLoop/networkConstants"; // Loop-13: iOS-safe | |
| const BACKEND = (import.meta.env.VITE_BACKEND_URL ?? "").replace(/\/$/, ""); | |
| // ─── Design tokens ──────────────────────────────────────────────────────────── | |
| const D = { | |
| mono: "var(--font-mono,'JetBrains Mono',monospace)", | |
| border: "rgba(255,255,255,0.07)", | |
| textSec: "rgba(148,163,184,0.80)", | |
| dim: "rgba(148,163,184,0.45)", | |
| }; | |
| // ─── Mode definitions ───────────────────────────────────────────────────────── | |
| interface ModeConfig { | |
| key: "chat" | "think" | "agent"; | |
| emoji: string; | |
| label: string; | |
| subtitle: string; | |
| color: string; | |
| rgb: string; | |
| traits: string[]; | |
| expectedTTFT: string; | |
| bestFor: string; | |
| } | |
| const MODES: ModeConfig[] = [ | |
| { | |
| key: "chat", | |
| emoji: "💬", | |
| label: "Chat", | |
| subtitle: "Risposta diretta", | |
| color: "#60a5fa", | |
| rgb: "96,165,250", | |
| traits: ["Risposta immediata", "Nessun tool", "Basso consumo"], | |
| expectedTTFT: "< 0.8s", | |
| bestFor: "Domande, conversazione, sintesi", | |
| }, | |
| { | |
| key: "think", | |
| emoji: "🧠", | |
| label: "Think", | |
| subtitle: "Ragionamento esteso", | |
| color: "#a78bfa", | |
| rgb: "167,139,250", | |
| traits: ["Chain-of-thought", "Maggiore precisione", "Analisi profonda"], | |
| expectedTTFT: "1–3s", | |
| bestFor: "Matematica, logica, codice complesso", | |
| }, | |
| { | |
| key: "agent", | |
| emoji: "🤖", | |
| label: "Agent", | |
| subtitle: "Multi-step con tool", | |
| color: "#34d399", | |
| rgb: "52,211,153", | |
| traits: ["Ricerca web", "Scrittura file", "Esecuzione codice"], | |
| expectedTTFT: "2–8s", | |
| bestFor: "Task complessi, dati real-time, progetti", | |
| }, | |
| ]; | |
| // ─── Benchmark result ───────────────────────────────────────────────────────── | |
| interface BenchResult { | |
| mode: "chat" | "think" | "agent"; | |
| latencyMs: number | null; | |
| status: "pending" | "ok" | "error"; | |
| error?: string; | |
| } | |
| // ─── Helpers ────────────────────────────────────────────────────────────────── | |
| function fmtMs(ms: number | null): string { | |
| if (ms === null) return "—"; | |
| if (ms < 1000) return `${Math.round(ms)}ms`; | |
| return `${(ms / 1000).toFixed(2)}s`; | |
| } | |
| function pct(n: number, d: number): string { | |
| if (d === 0) return "—"; | |
| return `${Math.round((n / d) * 100)}%`; | |
| } | |
| // ─── Stat row ───────────────────────────────────────────────────────────────── | |
| const StatRow = memo(({ label, value, color }: { label: string; value: string; color?: string }) => ( | |
| <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "3px 0" }}> | |
| <span style={{ fontSize: "0.64rem", color: D.dim, fontFamily: D.mono }}>{label}</span> | |
| <span style={{ fontSize: "0.67rem", fontWeight: 600, color: color ?? "rgba(248,250,252,0.82)", fontFamily: D.mono }}> | |
| {value} | |
| </span> | |
| </div> | |
| )); | |
| StatRow.displayName = "StatRow"; | |
| // ─── Loading spinner ────────────────────────────────────────────────────────── | |
| const Spin = ({ color }: { color: string }) => ( | |
| <svg width={11} height={11} viewBox="0 0 24 24" fill="none" | |
| stroke={color} strokeWidth="2.5" strokeLinecap="round" | |
| style={{ animation: "mpp-spin 0.8s linear infinite", flexShrink: 0 }}> | |
| <path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4" | |
| strokeOpacity="0.15"/> | |
| <path d="M12 2v4"/> | |
| </svg> | |
| ); | |
| // ─── Mode Card ──────────────────────────────────────────────────────────────── | |
| interface ModeCardProps { | |
| mode: ModeConfig; | |
| avgTTFT: number | null; | |
| taskCount: number; | |
| successRate: number | null; | |
| toolCalls: number; | |
| bench: BenchResult | null; | |
| benchRunning: boolean; | |
| } | |
| const ModeCard = memo(({ | |
| mode, avgTTFT, taskCount, successRate, toolCalls, bench, benchRunning, | |
| }: ModeCardProps) => { | |
| const { color, rgb } = mode; | |
| const benchMs = bench?.latencyMs ?? null; | |
| const benchStatus = bench?.status ?? null; | |
| return ( | |
| <div style={{ | |
| flex: 1, minWidth: "min(100%, 180px)", | |
| borderRadius: 11, | |
| border: `1px solid rgba(${rgb},0.22)`, | |
| background: `rgba(${rgb},0.04)`, | |
| overflow: "hidden", | |
| }}> | |
| {/* Header */} | |
| <div style={{ | |
| padding: "10px 12px 8px", | |
| borderBottom: `1px solid rgba(${rgb},0.12)`, | |
| background: `rgba(${rgb},0.07)`, | |
| }}> | |
| <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 3 }}> | |
| <span style={{ fontSize: "1rem", lineHeight: 1 }}>{mode.emoji}</span> | |
| <span style={{ fontSize: "0.75rem", fontWeight: 700, color, fontFamily: D.mono }}> | |
| {mode.label} | |
| </span> | |
| </div> | |
| <div style={{ fontSize: "0.60rem", color: D.textSec, lineHeight: 1.4 }}> | |
| {mode.subtitle} | |
| </div> | |
| </div> | |
| {/* Traits */} | |
| <div style={{ padding: "8px 12px 6px", display: "flex", flexDirection: "column", gap: 3 }}> | |
| {mode.traits.map(t => ( | |
| <div key={t} style={{ display: "flex", alignItems: "center", gap: 5 }}> | |
| <span style={{ width: 4, height: 4, borderRadius: "50%", background: `rgba(${rgb},0.65)`, flexShrink: 0 }} /> | |
| <span style={{ fontSize: "0.60rem", color: D.textSec }}>{t}</span> | |
| </div> | |
| ))} | |
| </div> | |
| {/* Metrics */} | |
| <div style={{ | |
| padding: "6px 12px 10px", | |
| borderTop: `1px solid ${D.border}`, | |
| display: "flex", flexDirection: "column", gap: 1, | |
| }}> | |
| <StatRow | |
| label="TTFT medio" | |
| value={avgTTFT !== null ? fmtMs(avgTTFT) : mode.expectedTTFT} | |
| color={avgTTFT !== null ? color : D.dim} | |
| /> | |
| {mode.key !== "chat" && ( | |
| <StatRow label="Task completati" value={taskCount > 0 ? String(taskCount) : "—"} /> | |
| )} | |
| {successRate !== null && ( | |
| <StatRow | |
| label="Successo" | |
| value={pct(Math.round(successRate * taskCount), taskCount)} | |
| color={successRate >= 0.7 ? "#22c55e" : successRate >= 0.4 ? "#f59e0b" : "#ef4444"} | |
| /> | |
| )} | |
| {mode.key === "agent" && toolCalls > 0 && ( | |
| <StatRow label="Tool calls" value={String(toolCalls)} /> | |
| )} | |
| {/* Benchmark result */} | |
| {(benchRunning || benchStatus) && ( | |
| <div style={{ | |
| marginTop: 4, padding: "4px 8px", borderRadius: 6, | |
| background: benchStatus === "error" ? "rgba(239,68,68,0.06)" : `rgba(${rgb},0.06)`, | |
| border: `1px solid ${benchStatus === "error" ? "rgba(239,68,68,0.18)" : `rgba(${rgb},0.15)`}`, | |
| display: "flex", alignItems: "center", gap: 6, | |
| }}> | |
| {benchRunning ? ( | |
| <> | |
| <Spin color={color} /> | |
| <span style={{ fontSize: "0.58rem", color: D.dim, fontFamily: D.mono }}>ping…</span> | |
| </> | |
| ) : benchStatus === "ok" ? ( | |
| <> | |
| <span style={{ width: 5, height: 5, borderRadius: "50%", background: "#22c55e", flexShrink: 0 }} /> | |
| <span style={{ fontSize: "0.60rem", color, fontFamily: D.mono, fontWeight: 600 }}> | |
| {fmtMs(benchMs)} | |
| </span> | |
| <span style={{ fontSize: "0.57rem", color: D.dim, fontFamily: D.mono }}>backend ping</span> | |
| </> | |
| ) : ( | |
| <> | |
| <span style={{ fontSize: "0.60rem", color: "#ef4444", fontFamily: D.mono }}> | |
| {bench?.error?.slice(0, 32) ?? "errore"} | |
| </span> | |
| </> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| {/* Best for */} | |
| <div style={{ | |
| padding: "5px 12px 9px", | |
| borderTop: `1px solid ${D.border}`, | |
| }}> | |
| <div style={{ fontSize: "0.56rem", color: D.dim, marginBottom: 2, textTransform: "uppercase" as const, letterSpacing: "0.06em", fontFamily: D.mono }}> | |
| Ideale per | |
| </div> | |
| <div style={{ fontSize: "0.62rem", color: D.textSec, lineHeight: 1.45 }}> | |
| {mode.bestFor} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }); | |
| ModeCard.displayName = "ModeCard"; | |
| // ─── Inject animations ──────────────────────────────────────────────────────── | |
| let _mppAnim = false; | |
| function abortSignalAny(signals: AbortSignal[]): AbortSignal { | |
| if (typeof AbortSignal.any === "function") return AbortSignal.any(signals); | |
| const ctrl = new AbortController(); | |
| for (const s of signals) { | |
| if (s.aborted) { ctrl.abort(s.reason); break; } | |
| s.addEventListener("abort", () => ctrl.abort(s.reason), { once: true }); | |
| } | |
| return ctrl.signal; | |
| } | |
| function _injectMppAnim() { | |
| if (_mppAnim || typeof document === "undefined") return; | |
| _mppAnim = true; | |
| const el = document.createElement("style"); | |
| el.dataset.id = "mpp-anim"; | |
| el.textContent = `@keyframes mpp-spin { to{transform:rotate(360deg)} }`; | |
| document.head.appendChild(el); | |
| } | |
| _injectMppAnim(); | |
| // ─── Main component ─────────────────────────────────────────────────────────── | |
| const ModePerformancePanel = memo(() => { | |
| const tasks = useTaskStore(s => s.tasks); | |
| const [chatTTFT, setChatTTFT] = useState<number | null>(null); | |
| const [thinkTTFT, setThinkTTFT] = useState<number | null>(null); | |
| const [agentTTFT, setAgentTTFT] = useState<number | null>(null); | |
| const [benchResults, setBenchResults] = useState<BenchResult[]>([]); | |
| const [benchRunning, setBenchRunning] = useState(false); | |
| const abortRef = useRef<AbortController | null>(null); | |
| // Derivazioni dai task | |
| const agentTasks = tasks.filter(t => | |
| t.status === "SUCCESS" || t.status === "ERROR" || t.status === "TIMEOUT" | |
| ); | |
| const agentDone = agentTasks.filter(t => t.status === "SUCCESS").length; | |
| const agentTotal = agentTasks.length; | |
| const agentSR = agentTotal > 0 ? agentDone / agentTotal : null; | |
| const totalToolCalls = tasks.reduce((sum, t) => sum + t.actions.length, 0); | |
| // Load TTFT dal telemetryDb (usa tutti i provider, media globale) | |
| useEffect(() => { | |
| let cancelled = false; | |
| void (async () => { | |
| try { | |
| const providers = ["groq", "openrouter", "gemini", "hf-router", "cloudflare", "deepseek"]; | |
| const all = await Promise.all(providers.map(p => telemetryDb.avgTTFT(p, 10))); | |
| const vals = all.filter((v): v is number => v !== null); | |
| if (vals.length === 0 || cancelled) return; | |
| const avg = vals.reduce((a, b) => a + b, 0) / vals.length; | |
| if (!cancelled) { | |
| setChatTTFT(avg * 0.75); // Chat: ~75% del TTFT base (no reasoning) | |
| setThinkTTFT(avg * 1.60); // Think: ~160% (reasoning chain) | |
| setAgentTTFT(avg * 3.20); // Agent: ~320% (tool calls + iterations) | |
| } | |
| } catch { /* non-fatal */ } | |
| })(); | |
| return () => { cancelled = true; }; | |
| }, []); | |
| // Benchmark: pinga il backend per ciascun modo in sequenza | |
| const runBenchmark = useCallback(async () => { | |
| abortRef.current?.abort(); | |
| const ctrl = new AbortController(); | |
| abortRef.current = ctrl; | |
| setBenchRunning(true); | |
| setBenchResults([]); | |
| const modeKeys: Array<"chat" | "think" | "agent"> = ["chat", "think", "agent"]; | |
| for (const mk of modeKeys) { | |
| const t0 = performance.now(); | |
| try { | |
| if (!BACKEND) throw new Error("VITE_BACKEND_URL non configurato"); | |
| const res = await fetch(`${BACKEND}/api/health`, { | |
| signal: abortSignalAny([ctrl.signal, makeTimedSignal(8_000)]), | |
| }); | |
| const latencyMs = Math.round(performance.now() - t0); | |
| if (!res.ok) throw new Error(`${res.status}`); | |
| setBenchResults(prev => [ | |
| ...prev.filter(r => r.mode !== mk), | |
| { mode: mk, latencyMs, status: "ok" }, | |
| ]); | |
| } catch (err) { | |
| if (ctrl.signal.aborted) break; | |
| const latencyMs = Math.round(performance.now() - t0); | |
| setBenchResults(prev => [ | |
| ...prev.filter(r => r.mode !== mk), | |
| { mode: mk, latencyMs, status: "error", error: (err as Error).message }, | |
| ]); | |
| } | |
| // Piccola pausa tra i ping | |
| await new Promise(r => setTimeout(r, 250)); | |
| } | |
| if (!ctrl.signal.aborted) setBenchRunning(false); | |
| }, []); | |
| const getBench = (key: "chat" | "think" | "agent"): BenchResult | null => | |
| benchResults.find(r => r.mode === key) ?? null; | |
| const isModeRunning = (key: "chat" | "think" | "agent"): boolean => { | |
| if (!benchRunning) return false; | |
| const done = new Set(benchResults.map(r => r.mode)); | |
| const modeKeys: Array<"chat" | "think" | "agent"> = ["chat", "think", "agent"]; | |
| const nextIdx = modeKeys.findIndex(k => !done.has(k)); | |
| return modeKeys[nextIdx] === key; | |
| }; | |
| const ttftMap = { chat: chatTTFT, think: thinkTTFT, agent: agentTTFT }; | |
| const taskCountMap = { chat: 0, think: 0, agent: agentTotal }; | |
| const srMap = { chat: null, think: null, agent: agentSR }; | |
| const toolMap = { chat: 0, think: 0, agent: totalToolCalls }; | |
| return ( | |
| <div> | |
| {/* Mode cards */} | |
| <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}> | |
| {MODES.map(mode => ( | |
| <ModeCard | |
| key={mode.key} | |
| mode={mode} | |
| avgTTFT={ttftMap[mode.key]} | |
| taskCount={taskCountMap[mode.key]} | |
| successRate={srMap[mode.key]} | |
| toolCalls={toolMap[mode.key]} | |
| bench={getBench(mode.key)} | |
| benchRunning={isModeRunning(mode.key)} | |
| /> | |
| ))} | |
| {/* S437: mobile spacer per evitare overflow cards */} | |
| <div style={{ flexBasis: "100%", height: 0, display: "none" }} aria-hidden /> | |
| </div> | |
| {/* Benchmark button */} | |
| <div style={{ marginTop: 10, display: "flex", alignItems: "center", gap: 8 }}> | |
| <button | |
| onClick={runBenchmark} | |
| disabled={benchRunning} | |
| style={{ | |
| all: "unset", cursor: benchRunning ? "default" : "pointer", | |
| fontSize: "0.65rem", fontFamily: D.mono, | |
| padding: "5px 12px", borderRadius: 7, | |
| background: benchRunning ? "rgba(99,102,241,0.04)" : "rgba(99,102,241,0.10)", | |
| border: `1px solid rgba(99,102,241,${benchRunning ? "0.12" : "0.28"})`, | |
| color: benchRunning ? "rgba(99,102,241,0.45)" : "#818cf8", | |
| display: "inline-flex", alignItems: "center", gap: 6, | |
| transition: "all 0.15s ease", | |
| }} | |
| > | |
| {benchRunning ? ( | |
| <><Spin color="#818cf8" /> Benchmark in corso…</> | |
| ) : ( | |
| <>⚡ Esegui benchmark</> | |
| )} | |
| </button> | |
| {!BACKEND && ( | |
| <span style={{ fontSize: "0.60rem", color: D.dim, fontFamily: D.mono }}> | |
| (configura VITE_BACKEND_URL per il benchmark) | |
| </span> | |
| )} | |
| {benchResults.length === 3 && !benchRunning && ( | |
| <span style={{ fontSize: "0.60rem", color: "#22c55e", fontFamily: D.mono }}> | |
| ✓ completato | |
| </span> | |
| )} | |
| </div> | |
| {/* Legend */} | |
| <div style={{ | |
| marginTop: 8, padding: "6px 10px", borderRadius: 7, | |
| background: "rgba(255,255,255,0.02)", border: `1px solid ${D.border}`, | |
| display: "flex", gap: 12, flexWrap: "wrap", | |
| }}> | |
| <span style={{ fontSize: "0.58rem", color: D.dim, fontFamily: D.mono, lineHeight: 1.5 }}> | |
| TTFT stimato da storico provider · Task Agent da sessione corrente | |
| </span> | |
| </div> | |
| </div> | |
| ); | |
| }); | |
| ModePerformancePanel.displayName = "ModePerformancePanel"; | |
| export default ModePerformancePanel; | |