Spaces:
Sleeping
Sleeping
| /** | |
| * useSlashCommands.ts — Hook che gestisce tutti i comandi /slash della chat | |
| * | |
| * v4 (P-COMMANDS): redesign chirurgico. | |
| * AGGIUNTI (dati diretti, output strutturato): | |
| * /meteo <città> → get_weather | |
| * /news [topic] → get_news | |
| * /stock <TICKER> → get_stock | |
| * /cambio <F> <T> [n] → get_currency | |
| * /wiki <query> → search_wikipedia | |
| * /cerca <query> → web search + sintesi (alias /search) | |
| * /ricerca <topic> → deep_research | |
| * /leggi <path> → read_file (alias /read) | |
| * /traduci <t> to <l> → 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<typeof import("@/lib/agentLoop")> | null = null; | |
| let _slashTelemPromise: Promise<typeof import("@/lib/agentTelemetry")> | null = null; | |
| const _loadTelemetry = () => | |
| (_slashTelemPromise ??= import("@/lib/agentTelemetry")); | |
| const _loadAgentLoop = (): Promise<typeof import("@/lib/agentLoop")> => | |
| (_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<void>) | null>; | |
| } | |
| export function useSlashCommands({ | |
| setAgentMode, | |
| persistMsgs, | |
| sendMsgRef, | |
| }: UseSlashCommandsProps): (text: string) => Promise<boolean> { | |
| return useCallback(async (text: string): Promise<boolean> => { | |
| 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<string, unknown>, signal?: AbortSignal): Promise<string> => | |
| (await _loadAgentLoop()).executeTool(name, args, signal); | |
| // ── /commit <msg> — 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 <città>\` | Temperatura, umidità, vento, condizioni |\n` + | |
| `| \`/news [topic]\` | Ultime notizie (topic opzionale) |\n` + | |
| `| \`/stock <TICKER>\` | Quotazione borsa + variazione + storico 5gg |\n` + | |
| `| \`/cambio <FROM> <TO> [importo]\` | Tasso di cambio valute |\n` + | |
| `| \`/wiki <query>\` | Estratto Wikipedia |\n\n` + | |
| `### 🤖 Agente specializzato\n` + | |
| `| Comando | Modalità |\n|---|---|\n` + | |
| `| \`/code <task>\` | Coder — scrivi, modifica, refactoring |\n` + | |
| `| \`/fix <errore>\` | Debugger — analizza e correggi bug |\n` + | |
| `| \`/cerca <query>\` | Web search + sintesi AI |\n` + | |
| `| \`/ricerca <topic>\` | 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 <msg>\` | Push di tutti i file VFS su GitHub |\n` + | |
| `| \`/leggi <path>\` | 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 <code|file>\` | Esegui JS/Python o file VFS |\n` + | |
| `| \`/traduci <testo> to <lingua>\` | Traduzione (en/it/fr/de/es/ja…) |\n` + | |
| `| \`/bench\` | Benchmark qualità AI — score /100 |\n\n` + | |
| `### ⚙️ Utility\n` + | |
| `| Comando | Azione |\n|---|---|\n` + | |
| `| \`/salva <nome> <testo>\` | Salva prompt come template |\n` + | |
| `| \`/template [nome]\` | Lista template o espandi uno |\n` + | |
| `| \`/elimina-template <nome>\` | Elimina template salvato |\n` + | |
| `| \`/clear\` | Cancella conversazione |\n` | |
| ); | |
| return true; | |
| } | |
| // ── /meteo <città> — 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 <TICKER> — 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 <FROM> <TO> [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 <query> — 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 <path> — 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 <query> — 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<string, unknown>).__taskHint = "web"; | |
| setTimeout(() => void sendMsgRef.current?.(query), 50); | |
| return true; | |
| } | |
| // ── /ricerca <topic> — deep research ───────────────────────────────────── | |
| if (lower.startsWith("/ricerca ")) { | |
| const topic = text.slice(9).trim(); | |
| setAgentMode(true); | |
| if (typeof window !== "undefined") (window as unknown as Record<string, unknown>).__taskHint = "researcher"; | |
| setTimeout(() => void sendMsgRef.current?.(topic), 50); | |
| return true; | |
| } | |
| // ── /run <code|file> — 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<string, string> = { 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 <testo> to <lingua> — 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 <task> — 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 <errore> — 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<string, unknown> | 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<string, unknown> | 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<Record<string, string>>) ?? []; | |
| 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 <nome> <testo del prompt>`"); 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 <nome> <testo>` 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<string, Array<{ verdict: string; count: number; lastSeenMs: number }>> = {}; | |
| 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 | |
| } | |