AUDIT / src /hooks /useSlashCommands.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 2)
cc11e77 verified
Raw
History Blame Contribute Delete
20.8 kB
/**
* 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
}