/** * 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 }) => (
{label} {value}
)); StatRow.displayName = "StatRow"; // ─── Loading spinner ────────────────────────────────────────────────────────── const Spin = ({ color }: { color: string }) => ( ); // ─── 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 (
{/* Header */}
{mode.emoji} {mode.label}
{mode.subtitle}
{/* Traits */}
{mode.traits.map(t => (
{t}
))}
{/* Metrics */}
{mode.key !== "chat" && ( 0 ? String(taskCount) : "β€”"} /> )} {successRate !== null && ( = 0.7 ? "#22c55e" : successRate >= 0.4 ? "#f59e0b" : "#ef4444"} /> )} {mode.key === "agent" && toolCalls > 0 && ( )} {/* Benchmark result */} {(benchRunning || benchStatus) && (
{benchRunning ? ( <> ping… ) : benchStatus === "ok" ? ( <> {fmtMs(benchMs)} backend ping ) : ( <> {bench?.error?.slice(0, 32) ?? "errore"} )}
)}
{/* Best for */}
Ideale per
{mode.bestFor}
); }); 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(null); const [thinkTTFT, setThinkTTFT] = useState(null); const [agentTTFT, setAgentTTFT] = useState(null); const [benchResults, setBenchResults] = useState([]); const [benchRunning, setBenchRunning] = useState(false); const abortRef = useRef(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 (
{/* Mode cards */}
{MODES.map(mode => ( ))} {/* S437: mobile spacer per evitare overflow cards */}
{/* Benchmark button */}
{!BACKEND && ( (configura VITE_BACKEND_URL per il benchmark) )} {benchResults.length === 3 && !benchRunning && ( βœ“ completato )}
{/* Legend */}
TTFT stimato da storico provider Β· Task Agent da sessione corrente
); }); ModePerformancePanel.displayName = "ModePerformancePanel"; export default ModePerformancePanel;