AUDIT / src /components /EmptyState.tsx
Arypulka98's picture
feat(audit): deploy full backend cluster node
aea470f verified
Raw
History Blame Contribute Delete
20.2 kB
import { useMemo, useState, useEffect, memo } from "react";
import {
Code2, Globe, PenLine, BarChart3,
Map, Zap, CloudSun, Search, DollarSign, Newspaper, Calculator, BookOpen
} from "lucide-react";
import AgentAvatar from "@/components/AgentAvatar";
import { getProactiveSuggestions, getTimeBasedGreeting } from "@/lib/agent/ProactiveAgent";
import { generateSuggestions } from "@/lib/agent/SmartSuggestions"; // Z7
import type { AgentStep } from "@/lib/storage"; // Z7
interface EmptyStateProps { onSend?: (text: string) => void; }
const CATEGORIES = [
{
icon: Code2,
label: "Codice",
desc: "Scrivi, debugga e analizza",
color: "#38bdf8",
prompt: "Scrivi uno script Python che ordina file per data di modifica",
},
{
icon: Globe,
label: "Web",
desc: "Cerca info e aggiornamenti",
color: "#4ade80",
prompt: "Ultime notizie tech di oggi",
},
{
icon: PenLine,
label: "Testi",
desc: "Email, documenti, contenuti",
color: "#a78bfa",
prompt: "Scrivi una email professionale di presentazione aziendale",
},
{
icon: BarChart3,
label: "Analisi",
desc: "Dati, calcoli e confronti",
color: "#fbbf24",
prompt: "Analizza i pro e contro di React vs Vue nel 2026",
},
{
icon: Map,
label: "Pianifica",
desc: "Itinerari, task, liste",
color: "#f97316",
prompt: "Pianifica un itinerario di 7 giorni in Giappone a maggio",
},
{
icon: Zap,
label: "Quick",
desc: "Domande e conversazione",
color: "#60a5fa",
prompt: "Spiega come funziona la fusione nucleare in modo semplice",
}
];
const TEST_SCENARIOS = [
{
icon: CloudSun,
label: "Meteo Milano",
color: "#38bdf8",
tool: "get_weather",
prompt: "Che tempo fa a Milano oggi? Dammi temperatura, condizioni e previsioni per domani.",
},
{
icon: Search,
label: "Web search",
color: "#4ade80",
tool: "web_search",
prompt: "Cerca le ultime notizie sull'intelligenza artificiale e i modelli LLM più recenti.",
},
{
icon: DollarSign,
label: "Valuta EUR/USD",
color: "#fbbf24",
tool: "get_currency",
prompt: "Quanto vale 100 euro in dollari americani oggi? Mostrami anche il cambio con yen giapponese.",
},
{
icon: Newspaper,
label: "Notizie tech",
color: "#f97316",
tool: "get_news",
prompt: "Top 5 notizie tecnologia di oggi. Fonti, titoli e breve riassunto di ognuna.",
},
{
icon: Calculator,
label: "Calcolo + codice",
color: "#a78bfa",
tool: "run_code",
prompt: "Scrivi ed esegui un script Python che calcola i primi 20 numeri di Fibonacci e mostra il risultato.",
},
{
icon: BookOpen,
label: "Wikipedia",
color: "#60a5fa",
tool: "search_wikipedia",
prompt: "Cerca su Wikipedia la storia dei Large Language Model (LLM) e spiegami i punti principali.",
},
];
// S459: colori priorità per i chip proattivi
const PRIORITY_COLOR: Record<string, string> = {
high: "#f472b6",
medium: "#60a5fa",
low: "rgba(255,255,255,0.35)",
};
const EmptyState = memo(function EmptyState({ onSend }: EmptyStateProps) {
// S459: ProactiveAgent — saluto temporale + suggerimenti contestuali (sync, no LLM)
const greeting = useMemo(() => { try { return getTimeBasedGreeting(); } catch { return ""; } }, []);
const proactiveSugs = useMemo(() => { try { return getProactiveSuggestions(3); } catch { return []; } }, []);
const [testScenariosOpen, setTestScenariosOpen] = useState(false);
// Z7: SmartSuggestions basate sull'ultima conversazione
const [smartSugs, setSmartSugs] = useState<Array<{id:string; text:string; quick:boolean}>>([]);
useEffect(() => {
try {
const raw = localStorage.getItem("agent_conversations");
if (!raw) return;
const convs = JSON.parse(raw) as Array<{
messages?: Array<{role: string; content: string}>;
steps?: AgentStep[];
}>;
const last = convs[convs.length - 1];
if (!last) return;
const lastUser = [...(last.messages ?? [])].reverse().find(m => m.role === "user");
if (!lastUser?.content || lastUser.content.length < 5) return;
const sugs = generateSuggestions(lastUser.content, last.steps ?? [], 3);
if (sugs.length > 0) setSmartSugs(sugs);
} catch { /* non blocca il rendering */ }
}, []);
return (
<div style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "flex-start",
minHeight: "100%",
paddingTop: "clamp(0.8rem, 3dvh, 1.5rem)",
paddingBottom: "1.5rem",
paddingInline: "1rem",
gap: "1.2rem",
userSelect: "none",
animation: "fadeInUp 0.4s cubic-bezier(0.16, 1, 0.3, 1) both",
boxSizing: "border-box",
}}>
{/* ── Brand ── */}
<div style={{ textAlign: "center" }}>
<div style={{ margin: "0 auto 0.4rem", display: "flex", alignItems: "center", justifyContent: "center" }}>
<AgentAvatar size={52} animate={true} glow={true} />
</div>
{/* S459: saluto temporale (Buongiorno / Buon pomeriggio / ecc.) */}
{greeting && (
<p style={{
margin: "0 0 4px",
fontSize: "0.72em",
fontWeight: 600,
color: "#06b6d4",
fontFamily: '"JetBrains Mono", ui-monospace, monospace',
letterSpacing: "0.06em",
opacity: 0.80,
animation: "fadeInUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0.05s both",
}}>
{greeting}
</p>
)}
<h2 style={{
margin: "0 0 6px",
fontSize: "clamp(1.1em, 4vw, 1.5em)",
fontWeight: 800,
fontFamily: '"JetBrains Mono", ui-monospace, monospace',
letterSpacing: "-0.02em",
background: "linear-gradient(135deg, #ffffff 0%, #06b6d4 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
animation: "boot-flicker 0.6s ease both",
}}>
AGENTE AI PRO
</h2>
<p style={{
margin: 0,
color: "var(--text-muted)",
fontSize: "0.78em",
maxWidth: 300,
marginInline: "auto",
lineHeight: 1.55,
fontFamily: '"JetBrains Mono", ui-monospace, monospace',
letterSpacing: "0.05em",
opacity: 0.6,
}}>
web · codice · github · memoria · strumenti
</p>
</div>
{/* ── S459: Proactive Suggestions — contestuali all'ora e alla sessione ── */}
{proactiveSugs.length > 0 && (
<div style={{ width: "100%", maxWidth: 480 }}>
<div style={{
display: "flex", alignItems: "center", gap: 8,
marginBottom: 8,
}}>
<span style={{
fontSize: "0.60rem", fontWeight: 700, letterSpacing: "0.12em",
color: "rgba(96,165,250,0.70)",
fontFamily: '"JetBrains Mono", ui-monospace, monospace',
textTransform: "uppercase",
}}>
💡 Suggeriti ora
</span>
<span style={{
fontSize: "0.58rem",
color: "rgba(255,255,255,0.25)",
fontFamily: '"JetBrains Mono", ui-monospace, monospace',
}}>
— basati su ora e sessione
</span>
</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
{proactiveSugs.map((sug, i) => (
<button
key={sug.id}
onClick={() => onSend?.(sug.text)}
title={sug.reason}
style={{
display: "flex",
alignItems: "center",
gap: 6,
padding: "7px 12px",
cursor: onSend ? "pointer" : "default",
textAlign: "left",
borderRadius: 20,
background: "rgba(96,165,250,0.07)",
border: `1px solid rgba(96,165,250,${sug.priority === "high" ? "0.35" : "0.18"})`,
transition: "all 0.18s ease",
animation: `card-enter 0.35s cubic-bezier(0.16, 1, 0.3, 1) ${i * 0.06}s both`,
flexShrink: 0,
}}
onMouseEnter={e => {
e.currentTarget.style.background = "rgba(96,165,250,0.14)";
e.currentTarget.style.transform = "translateY(-1px)";
}}
onMouseLeave={e => {
e.currentTarget.style.background = "rgba(96,165,250,0.07)";
e.currentTarget.style.transform = "none";
}}
>
<span style={{
width: 6, height: 6, borderRadius: "50%", flexShrink: 0,
background: PRIORITY_COLOR[sug.priority] ?? "rgba(255,255,255,0.35)",
}} />
<span style={{
fontSize: "0.72rem",
fontWeight: 500,
color: "rgba(255,255,255,0.80)",
whiteSpace: "nowrap",
maxWidth: 240,
overflow: "hidden",
textOverflow: "ellipsis",
}}>
{sug.text}
</span>
</button>
))}
</div>
</div>
)}
{/* ── Test Scenari — collapsible accordion ── */}
<div style={{ width: "100%", maxWidth: 480 }}>
<button
type="button"
onClick={() => setTestScenariosOpen(v => !v)}
style={{
all: "unset", display: "flex", alignItems: "center", gap: 8,
marginBottom: testScenariosOpen ? 8 : 0, cursor: "pointer", width: "100%",
}}
>
<span style={{
fontSize: "0.60rem", fontWeight: 700, letterSpacing: "0.12em",
color: "rgba(59,130,246,0.70)",
fontFamily: '"JetBrains Mono", ui-monospace, monospace',
textTransform: "uppercase",
}}>
⚡ Test scenari
</span>
<span style={{
fontSize: "0.58rem",
color: "rgba(255,255,255,0.25)",
fontFamily: '"JetBrains Mono", ui-monospace, monospace',
}}>
— verifica meteo, ricerca, calcoli, notizie
</span>
<span style={{
marginLeft: "auto", fontSize: "0.70rem",
color: "rgba(255,255,255,0.30)",
transition: "transform 0.18s",
display: "inline-block",
transform: testScenariosOpen ? "rotate(180deg)" : "rotate(0deg)",
}}></span>
</button>
{testScenariosOpen && <div style={{
display: "grid",
gridTemplateColumns: "repeat(2, 1fr)",
gap: 6,
}}>
{TEST_SCENARIOS.map((sc, i) => {
const Icon = sc.icon;
return (
<button
key={i}
onClick={() => onSend?.(sc.prompt)}
style={{
display: "flex",
alignItems: "center",
gap: 9,
padding: "9px 11px",
cursor: onSend ? "pointer" : "default",
textAlign: "left",
borderRadius: 8,
background: `rgba(${sc.color === "#38bdf8" ? "56,189,248" : sc.color === "#4ade80" ? "74,222,128" : sc.color === "#fbbf24" ? "251,191,36" : sc.color === "#f97316" ? "249,115,22" : sc.color === "#a78bfa" ? "167,139,250" : "52,211,153"},0.06)`,
border: `1px solid rgba(${sc.color === "#38bdf8" ? "56,189,248" : sc.color === "#4ade80" ? "74,222,128" : sc.color === "#fbbf24" ? "251,191,36" : sc.color === "#f97316" ? "249,115,22" : sc.color === "#a78bfa" ? "167,139,250" : "52,211,153"},0.18)`,
transition: "all 0.18s ease",
animation: `card-enter 0.35s cubic-bezier(0.16, 1, 0.3, 1) ${i * 0.04}s both`,
}}
onMouseEnter={e => {
e.currentTarget.style.transform = "translateY(-1px)";
e.currentTarget.style.background = `rgba(${sc.color === "#38bdf8" ? "56,189,248" : sc.color === "#4ade80" ? "74,222,128" : sc.color === "#fbbf24" ? "251,191,36" : sc.color === "#f97316" ? "249,115,22" : sc.color === "#a78bfa" ? "167,139,250" : "52,211,153"},0.12)`;
}}
onMouseLeave={e => {
e.currentTarget.style.transform = "none";
e.currentTarget.style.background = `rgba(${sc.color === "#38bdf8" ? "56,189,248" : sc.color === "#4ade80" ? "74,222,128" : sc.color === "#fbbf24" ? "251,191,36" : sc.color === "#f97316" ? "249,115,22" : sc.color === "#a78bfa" ? "167,139,250" : "52,211,153"},0.06)`;
}}
>
<div style={{
width: 28, height: 28, borderRadius: 7, flexShrink: 0,
background: `${sc.color}18`,
display: "flex", alignItems: "center", justifyContent: "center",
}}>
<Icon size={14} style={{ color: sc.color }} />
</div>
<div style={{ minWidth: 0 }}>
<div style={{
fontSize: "0.76rem", fontWeight: 680,
color: "rgba(255,255,255,0.80)",
marginBottom: 1,
whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
}}>
{sc.label}
</div>
<div style={{
fontSize: "0.58rem",
color: sc.color,
opacity: 0.65,
fontFamily: '"JetBrains Mono", ui-monospace, monospace',
letterSpacing: "0.04em",
}}>
{sc.tool}
</div>
</div>
</button>
);
})}
</div>}
</div>
{/* ── Z7: SmartSuggestions — continua da dove hai lasciato ── */}
{smartSugs.length > 0 && (
<div style={{ width: "100%", maxWidth: 480 }}>
<div style={{
display: "flex", alignItems: "center", gap: 8, marginBottom: 8,
}}>
<span style={{
fontSize: "0.60rem", fontWeight: 700, letterSpacing: "0.12em",
color: "rgba(167,139,250,0.75)",
fontFamily: '"JetBrains Mono", ui-monospace, monospace',
textTransform: "uppercase",
}}>
✨ Continua da dove hai lasciato
</span>
</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
{smartSugs.map((s, i) => (
<button
key={s.id}
onClick={() => onSend?.(s.text)}
style={{
display: "flex", alignItems: "center", gap: 6,
padding: "7px 13px",
cursor: onSend ? "pointer" : "default",
borderRadius: 20,
background: "rgba(167,139,250,0.07)",
border: "1px solid rgba(167,139,250,0.22)",
transition: "all 0.18s ease",
animation: `card-enter 0.35s cubic-bezier(0.16, 1, 0.3, 1) ${i * 0.07}s both`,
flexShrink: 0,
}}
onMouseEnter={e => {
e.currentTarget.style.background = "rgba(167,139,250,0.14)";
e.currentTarget.style.transform = "translateY(-1px)";
}}
onMouseLeave={e => {
e.currentTarget.style.background = "rgba(167,139,250,0.07)";
e.currentTarget.style.transform = "none";
}}
>
<span style={{ fontSize: "0.70rem", fontWeight: 500, color: "rgba(255,255,255,0.78)", whiteSpace: "nowrap", maxWidth: 260, overflow: "hidden", textOverflow: "ellipsis" }}>
{s.text}
</span>
</button>
))}
</div>
</div>
)}
{/* ── Category cards ── */}
<div style={{ width: "100%", maxWidth: 480 }}>
<div style={{
fontSize: "0.60rem", fontWeight: 700, letterSpacing: "0.12em",
color: "rgba(255,255,255,0.28)",
fontFamily: '"JetBrains Mono", ui-monospace, monospace',
textTransform: "uppercase",
marginBottom: 8,
}}>
Categorie
</div>
<div style={{
display: "grid",
gridTemplateColumns: "repeat(2, 1fr)",
gap: 8,
}}>
{CATEGORIES.map((cat, i) => {
const Icon = cat.icon;
return (
<button
key={i}
className="glass-card"
onClick={() => onSend?.(cat.prompt)}
onMouseEnter={e => {
const el = e.currentTarget;
el.style.transform = "translateY(-2px) scale(1.01)";
el.style.borderColor = "rgba(96, 165, 250, 0.4)";
}}
onMouseLeave={e => {
const el = e.currentTarget;
el.style.transform = "none";
el.style.borderColor = "rgba(96, 165, 250, 0.22)";
}}
style={{
display: "flex",
alignItems: "flex-start",
gap: 11,
padding: "13px 13px",
cursor: onSend ? "pointer" : "default",
textAlign: "left",
transition: "transform 0.2s, border-color 0.2s",
animation: `card-enter 0.35s cubic-bezier(0.16, 1, 0.3, 1) both`,
animationDelay: `${i * 0.04 + 0.24}s`,
}}
>
<div style={{
width: 32, height: 32,
borderRadius: 8,
flexShrink: 0,
background: `${cat.color}15`,
display: "flex", alignItems: "center", justifyContent: "center",
}}>
<Icon size={16} style={{ color: cat.color }} />
</div>
<div style={{ minWidth: 0 }}>
<div style={{
fontSize: "0.80rem",
fontWeight: 700,
color: "#ffffff",
marginBottom: 2,
letterSpacing: "-0.01em",
}}>
{cat.label}
</div>
<div style={{
fontSize: "0.70rem",
color: "rgba(255,255,255,0.5)",
lineHeight: 1.3,
}}>
{cat.desc}
</div>
</div>
</button>
);
})}
</div>
</div>
{/* ── Footer ── */}
<p style={{
fontSize: "0.63rem",
color: "var(--text-muted)",
textAlign: "center",
lineHeight: 2,
margin: 0,
fontFamily: '"JetBrains Mono", ui-monospace, monospace',
letterSpacing: "0.04em",
opacity: 0.7,
}}>
<kbd style={{
background: "rgba(255,255,255,0.05)",
border: "1px solid rgba(255,255,255,0.1)",
borderRadius: 5,
padding: "1px 6px",
fontSize: "0.63rem",
}}>Enter</kbd>
{" "}invia ·{" "}
<kbd style={{
background: "rgba(255,255,255,0.05)",
border: "1px solid rgba(255,255,255,0.1)",
borderRadius: 5,
padding: "1px 6px",
fontSize: "0.63rem",
}}>⇧ Enter</kbd>
{" "}a capo ·{" "}
<kbd style={{
background: "rgba(255,255,255,0.05)",
border: "1px solid rgba(255,255,255,0.1)",
borderRadius: 5,
padding: "1px 6px",
fontSize: "0.63rem",
}}>/</kbd>
{" "}comandi
</p>
</div>
);
});
export default EmptyState;