/** * useSlashCommands.ts — Hook che gestisce tutti i comandi /slash della chat * * v4 (P-COMMANDS): redesign chirurgico. * AGGIUNTI (dati diretti, output strutturato): * /meteo → get_weather * /news [topic] → get_news * /stock → get_stock * /cambio [n] → get_currency * /wiki → search_wikipedia * /cerca → web search + sintesi (alias /search) * /ricerca → deep_research * /leggi → read_file (alias /read) * /traduci to → translate (alias /translate) * RIMOSSI (ridondanti / non funzionanti): * /agent — ogni messaggio va già all'agente * /deploy — endpoint Vercel, questo è CF Pages * /project — ridondante con /code * /summarize — si chiede all'agente normalmente * /new — sintassi impraticabile (contenuto in una riga) * /git — sintassi confusa (owner/repo/path) * /test — incollare codice nel comando è impraticabile * /lint — idem * /analyze → rimpiazzato da /ricerca + /cerca * /search → rinominato /cerca */ import { useCallback } from "react"; import type * as React from "react"; import { useConversationStore } from "@/store/conversationStore"; import { vfsAsync } from "@/lib/vfsDb"; import { promptLibrary } from "@/lib/promptLibrary"; import { getRepoConfig } from "@/lib/github"; import type { Message } from "@/lib/storage"; let _slashAgentLoopPromise: Promise | null = null; let _slashTelemPromise: Promise | null = null; const _loadTelemetry = () => (_slashTelemPromise ??= import("@/lib/agentTelemetry")); const _loadAgentLoop = (): Promise => (_slashAgentLoopPromise ??= import("@/lib/agentLoop")); interface UseSlashCommandsProps { setAgentMode: (v: boolean) => void; persistMsgs: (id: string, msgs: Message[], opts?: { skipLocalStorage?: boolean }) => void; sendMsgRef: React.MutableRefObject<((text: string, prefix?: string) => Promise) | null>; } export function useSlashCommands({ setAgentMode, persistMsgs, sendMsgRef, }: UseSlashCommandsProps): (text: string) => Promise { return useCallback(async (text: string): Promise => { const lower = text.trim().toLowerCase(); const conv = useConversationStore.getState(); const addMsg = (content: string) => conv.setMessages(prev => [...prev, { id: crypto.randomUUID(), role: "assistant", content, createdAt: Date.now() }]); const updateLast = (content: string) => conv.setMessages(prev => { const c = [...prev]; c[c.length-1] = { ...c[c.length-1], content }; return c; }); const executeTool = async (name: string, args: Record, signal?: AbortSignal): Promise => (await _loadAgentLoop()).executeTool(name, args, signal); // ── /commit — push tutti i file VFS su GitHub ────────────────────── if (lower.startsWith("/commit")) { const msg = text.slice(7).trim() || "Update da Agente AI"; const files = (await vfsAsync.list()).filter(f => f.content && f.content.length < 400_000); if (!files.length) { addMsg("Nessun file nel VFS da pushare."); return true; } addMsg(`🚀 Push di **${files.length} file** su GitHub...`); try { const { owner, repo } = getRepoConfig(); updateLast(`**Commit completato**\n\n${await executeTool("git_push_multiple", { owner, repo, message: msg, files: files.map(f => ({ path: f.path.replace(/^\//, ""), content: f.content || "" })), })}`); } catch (e) { updateLast(`❌ Push fallito: ${(e as Error).message}`); } return true; } // ── /zip — crea archivio ZIP ───────────────────────────────────────────── if (lower === "/zip") { addMsg("📦 Creo archivio ZIP..."); updateLast(await executeTool("zip_project", {})); return true; } // ── /clear — cancella conversazione ────────────────────────────────────── if (lower === "/clear") { const { activeId: aid, setMessages: sm, persistMsgs: pm } = useConversationStore.getState(); if (aid) { sm([]); pm(aid, []); } return true; } // ── /help — guida comandi ──────────────────────────────────────────────── if (lower === "/help" || lower === "/aiuto") { addMsg( `## 🤖 Comandi slash\n\n` + `### 📡 Dati diretti\n` + `| Comando | Output |\n|---|---|\n` + `| \`/meteo \` | Temperatura, umidità, vento, condizioni |\n` + `| \`/news [topic]\` | Ultime notizie (topic opzionale) |\n` + `| \`/stock \` | Quotazione borsa + variazione + storico 5gg |\n` + `| \`/cambio [importo]\` | Tasso di cambio valute |\n` + `| \`/wiki \` | Estratto Wikipedia |\n\n` + `### 🤖 Agente specializzato\n` + `| Comando | Modalità |\n|---|---|\n` + `| \`/code \` | Coder — scrivi, modifica, refactoring |\n` + `| \`/fix \` | Debugger — analizza e correggi bug |\n` + `| \`/cerca \` | Web search + sintesi AI |\n` + `| \`/ricerca \` | Deep research — più fonti, report strutturato |\n\n` + `### 🎭 Persona\n` + `\`/persona auto\` · \`/persona coder\` · \`/persona researcher\` · \`/persona reasoner\` · \`/persona analyst\`\n\n` + `### 📁 File & VFS\n` + `| Comando | Azione |\n|---|---|\n` + `| \`/commit \` | Push di tutti i file VFS su GitHub |\n` + `| \`/leggi \` | Leggi file dal VFS (es. /leggi /src/app.ts) |\n` + `| \`/zip\` | Scarica il progetto come archivio ZIP |\n\n` + `### 🛠️ Dev\n` + `| Comando | Azione |\n|---|---|\n` + `| \`/run \` | Esegui JS/Python o file VFS |\n` + `| \`/traduci to \` | Traduzione (en/it/fr/de/es/ja…) |\n` + `| \`/bench\` | Benchmark qualità AI — score /100 |\n\n` + `### ⚙️ Utility\n` + `| Comando | Azione |\n|---|---|\n` + `| \`/salva \` | Salva prompt come template |\n` + `| \`/template [nome]\` | Lista template o espandi uno |\n` + `| \`/elimina-template \` | Elimina template salvato |\n` + `| \`/clear\` | Cancella conversazione |\n` ); return true; } // ── /meteo — meteo diretto, output strutturato ─────────────────── if (lower.startsWith("/meteo")) { const city = text.slice(6).trim() || "Milano"; addMsg(`🌤️ Meteo: **${city}**...`); updateLast(await executeTool("get_weather", { city })); return true; } // ── /news [topic] — notizie dirette ────────────────────────────────────── if (lower === "/news" || lower.startsWith("/news ")) { const query = text.slice(5).trim() || "ultime notizie"; addMsg(`📰 Notizie: **${query}**...`); updateLast(await executeTool("get_news", { query })); return true; } // ── /stock — quotazione borsa diretta ──────────────────────────── if (lower.startsWith("/stock ")) { const symbol = text.slice(7).trim().toUpperCase(); if (!symbol) { addMsg("Sintassi: `/stock AAPL` — inserisci il ticker borsistico"); return true; } addMsg(`📈 Quotazione: **${symbol}**...`); updateLast(await executeTool("get_stock", { symbol })); return true; } // ── /cambio [importo] — tasso di cambio diretto ───────────── if (lower.startsWith("/cambio")) { const parts = text.slice(7).trim().split(/\s+/); const from = (parts[0] ?? "EUR").toUpperCase(); const to = (parts[1] ?? "USD").toUpperCase(); const amount = parseFloat(parts[2] ?? "1") || 1; if (!parts[0]) { addMsg("Sintassi: `/cambio EUR USD 100` — es. converti 100 EUR in USD"); return true; } addMsg(`💱 Cambio: **${amount} ${from} → ${to}**...`); updateLast(await executeTool("get_currency", { from, to, amount })); return true; } // ── /wiki — Wikipedia diretto ──────────────────────────────────── if (lower.startsWith("/wiki ")) { const query = text.slice(6).trim(); if (!query) { addMsg("Sintassi: `/wiki quantum computing`"); return true; } addMsg(`📖 Wikipedia: **${query}**...`); updateLast(await executeTool("search_wikipedia", { query })); return true; } // ── /leggi — leggi file VFS ─────────────────────────────────────── if (lower.startsWith("/leggi ") || lower.startsWith("/read ")) { const path = text.replace(/^\/leggi\s+|^\/read\s+/i, "").trim(); const fp = path.startsWith("/") ? path : `/${path}`; addMsg(`📂 Leggo \`${fp}\`...`); updateLast(await executeTool("read_file", { path: fp })); return true; } // ── /cerca — web search con sintesi AI ─────────────────────────── if (lower.startsWith("/cerca ") || lower.startsWith("/search ")) { const query = text.replace(/^\/cerca\s+|^\/search\s+/i, "").trim(); setAgentMode(true); if (typeof window !== "undefined") (window as unknown as Record).__taskHint = "web"; setTimeout(() => void sendMsgRef.current?.(query), 50); return true; } // ── /ricerca — deep research ───────────────────────────────────── if (lower.startsWith("/ricerca ")) { const topic = text.slice(9).trim(); setAgentMode(true); if (typeof window !== "undefined") (window as unknown as Record).__taskHint = "researcher"; setTimeout(() => void sendMsgRef.current?.(topic), 50); return true; } // ── /run — esegui codice o file VFS ────────────────────────── if (lower.startsWith("/run ")) { const arg = text.slice(5).trim(); const isFile = /\.(py|js|ts|sh|rb|go|rs|jsx|tsx)$/i.test(arg); if (isFile) { const fp = arg.startsWith("/") ? arg : `/${arg}`; addMsg(`▶️ Eseguo \`${arg}\`...`); const content = await executeTool("read_file", { path: fp }); if (!content || content.startsWith("File non trovato") || content.startsWith("Errore")) { updateLast(`❌ File non trovato: \`${arg}\``); return true; } const ext = arg.split(".").pop()?.toLowerCase() ?? "js"; const langMap: Record = { py: "python", js: "javascript", ts: "javascript", jsx: "javascript", tsx: "javascript", sh: "bash", rb: "ruby", go: "go", rs: "rust" }; updateLast(await executeTool("run_code", { code: content, language: langMap[ext] ?? "javascript", filename: arg })); } else { addMsg("▶️ Eseguo codice..."); updateLast(await executeTool("run_code", { code: arg, language: "javascript" })); } return true; } // ── /traduci to — traduzione diretta ──────────────────── if (lower.startsWith("/traduci ") || lower.startsWith("/translate ")) { const raw = text.replace(/^\/traduci\s+|^\/translate\s+/i, "").trim(); const parts = raw.split(/ to /i); const src = (parts[0] ?? "").trim(); const lang = (parts[1] ?? "en").trim(); if (!src) { addMsg("Sintassi: `/traduci ciao to en`"); return true; } addMsg(`🌐 Traduco in **${lang}**...`); updateLast(await executeTool("translate", { text: src, to: lang })); return true; } // ── /code — modalità coder (routing via SLASH_INTENT_MAP) ───────── if (lower.startsWith("/code ")) { setAgentMode(true); setTimeout(() => void sendMsgRef.current?.(text.slice(6).trim()), 50); return true; } // ── /fix — modalità debugger ──────────────────────────────────── if (lower.startsWith("/fix ")) { setAgentMode(true); setTimeout(() => void sendMsgRef.current?.(`Debugga e correggi: ${text.slice(5).trim()}`), 50); return true; } // ── /bench — benchmark qualità AI ──────────────────────────────────────── if (lower === "/bench" || lower.startsWith("/bench")) { addMsg("📊 Benchmark qualità AI in corso...\nTest 4 categorie, attendi ~20 secondi."); try { const seed = "agente-ai-bench-2026"; const day = new Date().toISOString().slice(0, 10); const enc = new TextEncoder(); const key = await crypto.subtle.importKey("raw", enc.encode(seed), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); const sig = await crypto.subtle.sign("HMAC", key, enc.encode(day)); const tok = Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, "0")).join(""); const runRes = await fetch(`/api/benchmark/quality/run?token=${tok}`, { method: "POST" }); if (!runRes.ok) { const err = await runRes.text().catch(() => String(runRes.status)); updateLast(`❌ Benchmark fallito: ${err.slice(0, 200)}`); return true; } const { task_id } = await runRes.json() as { task_id: string }; let result: Record | null = null; for (let i = 0; i < 23; i++) { await new Promise(r => setTimeout(r, 4000)); const st = await fetch(`/api/benchmark/quality/status/${task_id}`).then(r => r.json()).catch(() => null) as Record | null; if (st?.status === "done") { result = st; break; } } if (!result) { updateLast("⏱ Benchmark timeout — le chiamate LLM hanno impiegato troppo."); return true; } type QResult = { id: string; label: string; score: number; criteria: { id: string; label: string; pass: boolean }[]; error?: string }; const total = (result.total_score as number) ?? 0; const results = (result.results as QResult[]) ?? []; const errors = (result.errors as Array>) ?? []; const emoji = (s: number) => s >= 75 ? "🟢" : s >= 50 ? "🟡" : "🔴"; if (errors.length && !results.length) { const errMsg = errors[0]?.global ?? JSON.stringify(errors[0]).slice(0, 150); updateLast(`⚠️ **Benchmark non completato**\n\nErrore: \`${errMsg}\``); return true; } const lines: string[] = [`${emoji(total)} **Score qualità: ${total}/100**\n`]; const allGaps: string[] = []; for (const r of results) { lines.push(`${emoji(r.score)} **${r.label}** — ${r.score}/100`); for (const c of r.criteria ?? []) { if (!c.pass) { lines.push(` ↳ gap: ${(c.label ?? c.id).slice(0, 60)}`); allGaps.push(c.label ?? c.id); } } } if (allGaps.length) lines.push(`\n📌 **${allGaps.length} gap rilevati** — usa \`/fix benchmark gaps\` per correggerli.`); else if (results.length) lines.push("\n✅ Nessun gap — tutte le categorie al verde."); if (errors.length) lines.push(`\n⚠️ ${errors.length} categ. con errore LLM — riprova se persistono.`); updateLast(lines.join("\n")); } catch (e) { updateLast(`❌ Benchmark errore: ${(e as Error).message}`); } return true; } // ── Template: /salva, /template, /elimina-template ─────────────────────── if (lower.startsWith("/salva ")) { const rest = text.slice(7).trim(); const spaceIdx = rest.indexOf(" "); const name = spaceIdx > 0 ? rest.slice(0, spaceIdx).trim() : rest.trim(); const tmpl = spaceIdx > 0 ? rest.slice(spaceIdx + 1).trim() : ""; if (!name || !tmpl) { addMsg("Sintassi: `/salva `"); return true; } try { await promptLibrary.save(name, tmpl); addMsg(`💾 Template **${name}** salvato.`); } catch (e) { addMsg("Errore: " + (e as Error).message); } return true; } if (lower === "/template") { const all = await promptLibrary.list(); if (!all.length) { addMsg("Nessun template salvato. Usa `/salva ` per crearne uno."); return true; } addMsg("## 📋 Template salvati (" + all.length + ")\n\n" + all.map((t: { name: string; text: string }) => `**${t.name}** — ${t.text.slice(0, 80)}${t.text.length > 80 ? "…" : ""}`).join("\n")); return true; } if (lower.startsWith("/template ")) { const name = text.slice(10).trim(); const tmplText = await promptLibrary.get(name); if (!tmplText) { addMsg(`Template **${name}** non trovato. Usa \`/template\` per l'elenco.`); return true; } setTimeout(() => void sendMsgRef.current?.(tmplText), 50); return true; } if (lower.startsWith("/elimina-template ")) { const name = text.slice(18).trim(); const deleted = await promptLibrary.delete(name); addMsg(deleted ? `🗑️ Template **${name}** eliminato.` : `Template **${name}** non trovato.`); return true; } // ── /telemetry — mostra contatori verdict qualità agente ──────────────── if (lower === "/telemetry" || lower.startsWith("/telemetry")) { try { const telem = await _loadTelemetry(); const report = telem.getReport(); if (!report.length) { addMsg("📊 **Telemetria agente**\n\nNessun dato ancora — i contatori si riempiono con l'uso."); return true; } // Raggruppa per sistema const bySystem: Record> = {}; for (const e of report) { if (!bySystem[e.system]) bySystem[e.system] = []; bySystem[e.system].push(e); } const now = Date.now(); const age = (ms: number) => { const s = Math.floor((now - ms) / 1000); if (s < 60) return s + "s fa"; if (s < 3600) return Math.floor(s / 60) + "min fa"; if (s < 86400) return Math.floor(s / 3600) + "h fa"; return Math.floor(s / 86400) + "g fa"; }; const lines: string[] = ["## 📊 Telemetria qualità agente\n"]; // Totale eventi const total = report.reduce((s, e) => s + e.count, 0); lines.push(`**Totale eventi:** ${total}\n`); // Fail rate per sistema const failSystems: string[] = []; for (const [sys] of Object.entries(bySystem)) { const fr = telem.getFailRate(sys); if (fr !== null) failSystems.push(`${sys}: ${Math.round(fr * 100)}%`); } if (failSystems.length) { lines.push("**Fail rate:** " + failSystems.join(" · ") + "\n"); } // Tabella per sistema for (const [sys, entries] of Object.entries(bySystem)) { lines.push(`\n### ${sys}`); lines.push("| Verdict | Count | Ultimo |"); lines.push("|---|---|---|"); for (const e of entries.sort((a, b) => b.count - a.count)) { lines.push(`| ${e.verdict} | **${e.count}** | ${age(e.lastSeenMs)} |`); } } lines.push("\n*Calibrazione: fail rate alto → soglia trigger troppo permissiva; basso → troppo restrittiva.*"); addMsg(lines.join("\n")); } catch (e) { addMsg("❌ Telemetria non disponibile: " + (e as Error).message); } return true; } return false; // sendMsgRef è una ref stabile — non nei deps per design }, [setAgentMode, persistMsgs, sendMsgRef]); // eslint-disable-line react-hooks/exhaustive-deps }