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