AUDIT / src /components /MemoryPanel.tsx
Arypulka98's picture
feat(audit): deploy full backend cluster node
aea470f verified
Raw
History Blame Contribute Delete
24.6 kB
import { useState, useEffect, memo } from "react";
import { X, Database, BarChart2, AlertTriangle, Trash2, History, User, Zap } from "lucide-react";
import { agentMemory, MemoryEntry } from "@/lib/agentMemory";
import { getOutcomeStats } from "@/lib/selfLearning";
import { getAllSessions, deleteSession, clearAllSessions, getLongTermPrefs, SessionEntry } from "@/lib/sessionMemory";
import { getPatterns, clearPatterns, SkillPattern } from "@/lib/skillPersistence";
import { isAutoFixRunning } from "@/lib/errorAutoFix";
import { Z_INDEX } from "@/lib/zindex";
interface MemoryPanelProps { onClose: () => void; }
type Tab = "memory" | "sessions" | "reflection" | "skills";
const MemoryPanel = memo(function MemoryPanel({ onClose }: MemoryPanelProps) {
const [tab, setTab] = useState<Tab>("memory");
const [entries, setEntries] = useState<MemoryEntry[]>([]);
const [sessions, setSessions] = useState<SessionEntry[]>([]);
const [patterns, setPatterns] = useState<SkillPattern[]>([]);
const [autoFixOn, setAutoFixOn] = useState(false);
const [adding, setAdding] = useState(false);
const [search, setSearch] = useState("");
const [form, setForm] = useState({ key: "", value: "", category: "general" });
const [stats, setStats] = useState(() => getOutcomeStats());
const reload = () => {
setEntries(agentMemory.list());
setStats(getOutcomeStats());
setSessions(getAllSessions());
setPatterns(getPatterns());
setAutoFixOn(isAutoFixRunning());
};
useEffect(() => { reload(); const unsub = agentMemory.subscribe(reload); return unsub; }, []);
const save = () => {
if (!form.key.trim() || !form.value.trim()) return;
agentMemory.set(form.key.trim(), form.value.trim(), form.category || "general");
setAdding(false);
setForm({ key: "", value: "", category: "general" });
reload();
};
const q = search.toLowerCase().trim();
const filtered = q
? entries.filter(e =>
e.key.toLowerCase().includes(q) ||
e.value.toLowerCase().includes(q) ||
(e.category ?? "").toLowerCase().includes(q)
)
: entries;
const inputStyle = {
width: "100%", background: "rgba(255,255,255,0.05)",
border: "1px solid #2a2a40", borderRadius: 8,
padding: "0.38rem 0.6rem", color: "#e2e2f0",
fontSize: "0.8rem", outline: "none",
};
const prefs = getLongTermPrefs();
const confColor = (c: number) =>
c >= 0.75 ? "#4ade80" : c >= 0.5 ? "#fbbf24" : "#f87171";
const tabBtn = (t: Tab, label: string, Icon: React.ElementType) => (
<button onClick={() => setTab(t)} style={{
all: "unset", cursor: "pointer", padding: "0.35rem 0.9rem",
borderRadius: 8, fontSize: "0.78rem", fontWeight: tab === t ? 700 : 400,
color: tab === t ? "#e2e2f0" : "#6b6b7b",
background: tab === t ? "rgba(79,142,247,0.15)" : "transparent",
border: tab === t ? "1px solid rgba(79,142,247,0.25)" : "1px solid transparent",
transition: "all 0.15s",
display: "flex", alignItems: "center", gap: 6,
}}>
<Icon size={13} />
{label}
</button>
);
return (
<div style={{
position: "fixed", inset: 0, zIndex: Z_INDEX.MODAL_OVERLAY, display: "flex",
background: "rgba(0,0,0,0.6)",
alignItems: "center", justifyContent: "center",
}} onClick={onClose}>
<div style={{
background: "rgba(8,8,22,0.80)",
border: "1px solid rgba(255,255,255,0.08)", borderRadius: 16,
width: "min(500px, 95vw)", maxHeight: "80vh", display: "flex", flexDirection: "column",
overflow: "hidden", boxShadow: "0 20px 60px rgba(0,0,0,0.7)",
}} onClick={e => e.stopPropagation()}>
{/* Header */}
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "1rem 1.2rem", borderBottom: "1px solid #2a2a40" }}>
<div>
<div style={{ fontWeight: 700, fontSize: "1rem", color: "#e2e2f0", display: "flex", alignItems: "center", gap: 8 }}>
<Database size={16} style={{ color: "#6b6b9b" }} />
Memoria Agente
</div>
<div style={{ fontSize: "0.75rem", color: "#6b6b7b", marginTop: 2 }}>
{tab === "memory"
? (q ? `${filtered.length} / ${entries.length} ricordi` : `${entries.length} ${entries.length === 1 ? "ricordo" : "ricordi"} salvati`)
: tab === "skills"
? `${patterns.length} pattern appresi · Auto-Fix ${autoFixOn ? "attivo" : "inattivo"}`
: `${stats.total} task analizzati · ${Math.round(stats.successRate * 100)}% successo`}
</div>
</div>
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
{tab === "memory" && entries.length > 0 && (
<button onClick={() => { agentMemory.clear(); reload(); }} style={{
all: "unset", cursor: "pointer", fontSize: "0.75rem", color: "#ef4444",
padding: "3px 8px", borderRadius: 8, background: "rgba(239,68,68,0.08)",
}}>Svuota</button>
)}
<button onClick={onClose} style={{
all: "unset", cursor: "pointer", color: "#6b6b7b",
display: "flex", alignItems: "center", justifyContent: "center",
padding: 4, borderRadius: 8, transition: "color 0.15s",
}}
onMouseEnter={e => (e.currentTarget.style.color = "#9090b0")}
onMouseLeave={e => (e.currentTarget.style.color = "#6b6b7b")}
><X size={16} /></button>
</div>
</div>
{/* Tabs */}
<div style={{ display: "flex", gap: 6, padding: "0.6rem 1.2rem", borderBottom: "1px solid #1a1a2e", flexWrap: "wrap" }}>
{tabBtn("memory", "Ricordi", Database)}
{tabBtn("sessions", "Sessioni", History)}
{tabBtn("reflection", "Riflessioni", BarChart2)}
{tabBtn("skills", "Skills", Zap)}
</div>
{/* Search (memory tab only) */}
{tab === "memory" && entries.length > 2 && (
<div style={{ padding: "0.6rem 1.2rem", borderBottom: "1px solid #1a1a2e" }}>
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Cerca nei ricordi…"
style={{ ...inputStyle, background: "rgba(255,255,255,0.04)", border: "1px solid #1e1e2e", fontSize: "0.82rem", paddingLeft: "0.5rem" }}
autoFocus={false}
/>
</div>
)}
{/* Body */}
<div style={{ flex: 1, overflowY: "auto", padding: "1rem 1.2rem" }}>
{/* MEMORY TAB */}
{tab === "memory" && (<>
{entries.length === 0 && !adding && (
<div style={{ textAlign: "center", color: "#4a4a62", padding: "2rem 0", fontSize: "0.85rem" }}>
Nessun ricordo. L'agente salva informazioni con il tool <code style={{ fontFamily: "monospace" }}>remember</code>.
</div>
)}
{q && filtered.length === 0 && entries.length > 0 && (
<div style={{ textAlign: "center", color: "#4a4a62", padding: "1rem 0", fontSize: "0.82rem" }}>
Nessun ricordo corrisponde a "<strong>{search}</strong>".
</div>
)}
{filtered.map(e => (
<div key={e.key} style={{ background: "rgba(255,255,255,0.03)", border: "1px solid #1e1e2e", borderRadius: 8, padding: "0.65rem 0.85rem", marginBottom: 6, display: "flex", alignItems: "flex-start", gap: 8 }}>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 2 }}>
<span style={{ fontFamily: "monospace", fontSize: "0.78rem", color: "#4f8ef7", fontWeight: 600 }}>{e.key}</span>
{e.category !== "general" && (
<span style={{ fontSize: "0.65rem", background: "rgba(79,142,247,0.1)", color: "#4f8ef7", padding: "1px 5px", borderRadius: 4 }}>{e.category}</span>
)}
</div>
<div style={{ fontSize: "0.8rem", color: "#9090a8", wordBreak: "break-word", whiteSpace: "pre-wrap" }}>{e.value}</div>
</div>
<button onClick={() => { agentMemory.delete(e.key); reload(); }} style={{
all: "unset", cursor: "pointer", color: "#4a4a62",
display: "flex", alignItems: "center", justifyContent: "center",
flexShrink: 0, paddingTop: 2, transition: "color 0.15s",
}}
onMouseEnter={e2 => (e2.currentTarget.style.color = "#ef4444")}
onMouseLeave={e2 => (e2.currentTarget.style.color = "#4a4a62")}
><Trash2 size={13} /></button>
</div>
))}
{adding ? (
<div style={{ background: "rgba(79,142,247,0.04)", border: "1px solid rgba(79,142,247,0.15)", borderRadius: 10, padding: "0.9rem" }}>
{([{ key: "key", label: "Chiave", placeholder: "nome_info" }, { key: "value", label: "Valore", placeholder: "contenuto da ricordare" }, { key: "category", label: "Categoria", placeholder: "general" }] as Array<{ key: string; label: string; placeholder: string }>).map(({ key, label, placeholder }) => (
<div key={key} style={{ marginBottom: 8 }}>
<div style={{ fontSize: "0.72rem", color: "#6b6b7b", marginBottom: 3 }}>{label}</div>
{key === "value" ? (
<textarea value={(form as Record<string, string>)[key]} placeholder={placeholder} rows={3} style={{ ...inputStyle, resize: "vertical", lineHeight: 1.5 }} onChange={e => setForm(f => ({ ...f, [key]: e.target.value }))} />
) : (
<input value={(form as Record<string, string>)[key]} placeholder={placeholder} style={inputStyle} onChange={e => setForm(f => ({ ...f, [key]: e.target.value }))} />
)}
</div>
))}
<div style={{ display: "flex", gap: 8, marginTop: 8 }}>
<button onClick={save} style={{ all: "unset", cursor: "pointer", background: "#4f8ef7", color: "#fff", padding: "0.38rem 1rem", borderRadius: 7, fontSize: "0.8rem", fontWeight: 600 }}>Salva</button>
<button onClick={() => setAdding(false)} style={{ all: "unset", cursor: "pointer", color: "#6b6b7b", fontSize: "0.8rem" }}>Annulla</button>
</div>
</div>
) : (
<button onClick={() => setAdding(true)} style={{ all: "unset", cursor: "pointer", display: "flex", alignItems: "center", gap: 6, padding: "0.5rem 0.8rem", borderRadius: 8, width: "100%", background: "rgba(79,142,247,0.06)", border: "1px dashed rgba(79,142,247,0.25)", color: "#4f8ef7", fontSize: "0.8rem", marginTop: filtered.length ? 8 : 0 }}>
+ Aggiungi ricordo
</button>
)}
</>)}
{/* SESSIONS TAB */}
{tab === "sessions" && (
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
{prefs.totalSessions > 0 && (
<div style={{ background: "rgba(79,142,247,0.06)", border: "1px solid rgba(79,142,247,0.18)", borderRadius: 10, padding: "0.7rem 1rem", display: "flex", gap: 10, alignItems: "flex-start" }}>
<User size={14} style={{ color: "#4f8ef7", flexShrink: 0, marginTop: 2 }} />
<div>
{prefs.userName && <div style={{ fontSize: "0.82rem", color: "#e2e2f0", fontWeight: 600, marginBottom: 3 }}>{prefs.userName}</div>}
<div style={{ fontSize: "0.75rem", color: "#6b6b7b" }}>{prefs.totalSessions} sessioni totali</div>
{prefs.frequentTopics.length > 0 && (
<div style={{ display: "flex", flexWrap: "wrap", gap: 4, marginTop: 6 }}>
{prefs.frequentTopics.slice(0, 7).map(t => (
<span key={t} style={{ fontSize: "0.68rem", background: "rgba(79,142,247,0.1)", color: "#4f8ef7", padding: "1px 7px", borderRadius: 10 }}>{t}</span>
))}
</div>
)}
</div>
</div>
)}
{sessions.length === 0 ? (
<div style={{ textAlign: "center", color: "#4a4a62", padding: "2rem 0", fontSize: "0.85rem" }}>
Nessuna sessione registrata. Le conversazioni vengono salvate automaticamente.
</div>
) : (
<>
{sessions.map(s => (
<div key={s.id} style={{ background: "rgba(255,255,255,0.03)", border: "1px solid #1e1e2e", borderRadius: 9, padding: "0.65rem 0.85rem", display: "flex", gap: 8, alignItems: "flex-start" }}>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 3 }}>
<span style={{ fontSize: "0.7rem", color: "#4a4a62" }}>
{new Date(s.date).toLocaleDateString("it-IT", { day: "2-digit", month: "short", year: "2-digit" })}
</span>
{s.topics.slice(0, 3).map(t => (
<span key={t} style={{ fontSize: "0.63rem", background: "rgba(129,140,248,0.1)", color: "#60a5fa", padding: "0px 5px", borderRadius: 4 }}>{t}</span>
))}
</div>
<div style={{ fontSize: "0.8rem", color: "#c0c0d0", fontWeight: 500, marginBottom: 2, wordBreak: "break-word" }}>{s.title}</div>
{s.summary && <div style={{ fontSize: "0.73rem", color: "#6b6b7b", wordBreak: "break-word", lineHeight: 1.4 }}>{s.summary.slice(0, 100)}{s.summary.length > 100 ? "…" : ""}</div>}
{s.keyFacts.length > 0 && (
<div style={{ fontSize: "0.68rem", color: "#4f8ef7", marginTop: 4 }}>
{s.keyFacts.slice(0, 2).join(" • ")}
</div>
)}
</div>
<button onClick={() => { deleteSession(s.id); reload(); }} style={{
all: "unset", cursor: "pointer", color: "#3a3a52", flexShrink: 0,
display: "flex", alignItems: "center", paddingTop: 2, transition: "color 0.15s",
}}
onMouseEnter={e2 => (e2.currentTarget.style.color = "#ef4444")}
onMouseLeave={e2 => (e2.currentTarget.style.color = "#3a3a52")}
><Trash2 size={13} /></button>
</div>
))}
<button onClick={() => { if (confirm("Cancellare tutte le sessioni e le preferenze utente?")) { clearAllSessions(); reload(); } }} style={{
all: "unset", cursor: "pointer", fontSize: "0.72rem", color: "#ef4444",
textAlign: "center", padding: "0.4rem", borderRadius: 8,
background: "rgba(239,68,68,0.06)", border: "1px solid rgba(239,68,68,0.15)",
}}>Cancella tutte le sessioni</button>
</>
)}
</div>
)}
{/* REFLECTION TAB */}
{tab === "reflection" && (
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
{stats.total === 0 ? (
<div style={{ textAlign: "center", color: "#4a4a62", padding: "2rem 0", fontSize: "0.85rem" }}>
Nessun dato di riflessione disponibile. Usa l'agente per accumulare dati.
</div>
) : (<>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 8 }}>
{[
{ label: "Task totali", value: String(stats.total), color: "#60a5fa" },
{ label: "Successo", value: Math.round(stats.successRate * 100) + "%", color: stats.successRate >= 0.7 ? "#4ade80" : "#fbbf24" },
{ label: "Fiducia media", value: Math.round(stats.avgConfidence * 100) + "%", color: stats.avgConfidence >= 0.6 ? "#4ade80" : "#f87171" },
].map(c => (
<div key={c.label} style={{ background: "rgba(255,255,255,0.03)", border: "1px solid #1e1e2e", borderRadius: 10, padding: "0.7rem 0.8rem", textAlign: "center" }}>
<div style={{ fontSize: "1.2rem", fontWeight: 700, color: c.color }}>{c.value}</div>
<div style={{ fontSize: "0.65rem", color: "#6b6b7b", marginTop: 2 }}>{c.label}</div>
</div>
))}
</div>
<div>
<div style={{ fontSize: "0.72rem", color: "#6b6b7b", marginBottom: 6 }}>Livello fiducia medio</div>
<div style={{ height: 6, background: "rgba(255,255,255,0.08)", borderRadius: 3, overflow: "hidden" }}>
<div style={{ height: "100%", width: Math.round(stats.avgConfidence * 100) + "%", background: stats.avgConfidence >= 0.6 ? "#4ade80" : "#fbbf24", borderRadius: 3, transition: "width 0.6s ease" }} />
</div>
</div>
{stats.failurePatterns.length > 0 && (
<div>
<div style={{ fontSize: "0.72rem", color: "#6b6b7b", marginBottom: 6 }}>Problemi frequenti</div>
{stats.failurePatterns.map((fp, i) => (
<div key={i} style={{ background: "rgba(239,68,68,0.05)", border: "1px solid rgba(239,68,68,0.12)", borderRadius: 8, padding: "0.5rem 0.75rem", marginBottom: 5, display: "flex", alignItems: "center", gap: 8 }}>
<AlertTriangle size={12} style={{ color: "#f87171", flexShrink: 0 }} />
<span style={{ fontSize: "0.78rem", color: "#f87171", flex: 1 }}>{fp.reason}</span>
<span style={{ fontSize: "0.7rem", color: "#4a4a62" }}>×{fp.count}</span>
</div>
))}
</div>
)}
<button onClick={() => { if (confirm("Cancellare tutti i dati di riflessione?")) { try {
localStorage.removeItem("agent_outcomes"); // selfLearning outcomes
localStorage.removeItem("agente_cg_learned"); // S484: confidenceGate _learnedPatterns
reload();
} catch {} } }} style={{ all: "unset", cursor: "pointer", fontSize: "0.72rem", color: "#ef4444", textAlign: "center", padding: "0.4rem", borderRadius: 8, background: "rgba(239,68,68,0.06)", border: "1px solid rgba(239,68,68,0.15)" }}>
Cancella dati riflessione
</button>
</>)}
</div>
)}
{/* SKILLS TAB */}
{tab === "skills" && (
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
{/* Auto-Fix status */}
<div style={{
display: "flex", alignItems: "center", gap: 8,
background: autoFixOn ? "rgba(74,222,128,0.07)" : "rgba(107,107,123,0.08)",
border: `1px solid ${autoFixOn ? "rgba(74,222,128,0.22)" : "#2a2a40"}`,
borderRadius: 10, padding: "0.6rem 0.9rem",
}}>
<Zap size={13} style={{ color: autoFixOn ? "#4ade80" : "#4a4a62", flexShrink: 0 }} />
<div style={{ flex: 1 }}>
<span style={{ fontSize: "0.8rem", fontWeight: 600, color: autoFixOn ? "#4ade80" : "#4a4a62" }}>
Auto-Fix loop {autoFixOn ? "attivo" : "inattivo"}
</span>
<div style={{ fontSize: "0.68rem", color: "#6b6b7b", marginTop: 1 }}>
{autoFixOn
? "Controlla errori ogni 30s — interviene se ≥3 irrisolti"
: "Si avvia automaticamente all'init dell'agente"}
</div>
</div>
<div style={{
width: 7, height: 7, borderRadius: "50%",
background: autoFixOn ? "#4ade80" : "#3a3a52",
boxShadow: autoFixOn ? "0 0 6px #4ade80" : "none",
flexShrink: 0,
}} />
</div>
{/* Pattern list */}
{patterns.length === 0 ? (
<div style={{ textAlign: "center", color: "#4a4a62", padding: "2rem 0", fontSize: "0.85rem", lineHeight: 1.7 }}>
Nessun pattern ancora appreso.<br />
<span style={{ fontSize: "0.78rem" }}>
I pattern si accumulano automaticamente dopo ogni task riuscito.
</span>
</div>
) : (
<>
<div style={{ fontSize: "0.72rem", color: "#6b6b7b", paddingLeft: 2 }}>
{patterns.length} pattern · ordinati per fiducia
</div>
{[...patterns]
.sort((a, b) => b.confidence - a.confidence)
.map(p => (
<div key={p.id} style={{
background: "rgba(255,255,255,0.03)", border: "1px solid #1e1e2e",
borderRadius: 10, padding: "0.75rem 0.9rem",
}}>
{/* Task signature */}
<div style={{ fontSize: "0.78rem", color: "#c0c0d0", fontWeight: 500, marginBottom: 6, wordBreak: "break-word", lineHeight: 1.4 }}>
{p.taskSignature.slice(0, 120)}{p.taskSignature.length > 120 ? "…" : ""}
</div>
{/* Tool sequence chips */}
<div style={{ display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 8 }}>
{p.toolSequence.map((tool, i) => (
<span key={i} style={{
fontSize: "0.65rem", fontFamily: "monospace",
background: "rgba(79,142,247,0.1)", color: "#4f8ef7",
padding: "2px 7px", borderRadius: 5,
border: "1px solid rgba(79,142,247,0.2)",
}}>{tool}</span>
))}
</div>
{/* Confidence bar + stats */}
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div style={{ flex: 1 }}>
<div style={{ height: 4, background: "rgba(255,255,255,0.06)", borderRadius: 2, overflow: "hidden" }}>
<div style={{
height: "100%",
width: Math.round(p.confidence * 100) + "%",
background: confColor(p.confidence),
borderRadius: 2,
transition: "width 0.4s ease",
}} />
</div>
</div>
<span style={{ fontSize: "0.68rem", fontWeight: 700, color: confColor(p.confidence), flexShrink: 0 }}>
{Math.round(p.confidence * 100)}%
</span>
<span style={{ fontSize: "0.65rem", color: "#4a4a62", flexShrink: 0 }}>
{p.successCount}/{p.totalCount} ok
</span>
</div>
</div>
))}
<button onClick={() => {
if (confirm("Cancellare tutti i pattern appresi?")) { clearPatterns(); reload(); }
}} style={{
all: "unset", cursor: "pointer", fontSize: "0.72rem", color: "#ef4444",
textAlign: "center", padding: "0.4rem", borderRadius: 8,
background: "rgba(239,68,68,0.06)", border: "1px solid rgba(239,68,68,0.15)",
}}>Cancella pattern</button>
</>
)}
</div>
)}
</div>
</div>
</div>
);
});
export default MemoryPanel;