Spaces:
Sleeping
Sleeping
| 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; | |