AUDIT / src /components /ModePerformancePanel.tsx
Arypulka98's picture
feat(audit): deploy full backend cluster node
aea470f verified
Raw
History Blame Contribute Delete
17.3 kB
/**
* 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;