/** * CodeRunner.tsx — FASE 3.5 * * Architettura runtime: * - Codice "rich" (usa vfs / memory / download / fetch) → main thread sandbox * - Codice "pure" JS (nessuna API DOM custom) → jsRunner.worker.ts (off-main-thread) * * Il runtime effettivo è mostrato nella UI (badge). * Cancellazione supportata in entrambi i path. */ import { useState, useCallback, useRef, useEffect, memo } from "react"; import { vfsAsync } from "@/lib/vfsDb"; import { agentMemory } from "@/lib/agentMemory"; import type { JSRunnerMsg, JSRunnerResult } from "@/workers/jsRunner.worker"; interface CodeRunnerProps { code: string; language?: string; onResult?: (result: string) => void; } type RuntimeLabel = "JS Worker" | "JS Main" | "Python"; type Status = "idle" | "running" | "done" | "error"; const MAX_OUTPUT = 8_000; const JS_TIMEOUT = 15_000; // ─── Heuristic: codice usa API DOM custom? → main thread ───────────────────── const MAIN_THREAD_APIS = /\bvfs\b|\bmemory\b|\bdownload\b|\bgeneratePDF\b|\bimportModule\b/; function needsMainThread(code: string): boolean { return MAIN_THREAD_APIS.test(code); } function truncate(s: string) { if (s.length <= MAX_OUTPUT) return s; return s.slice(0, MAX_OUTPUT) + `\n…(troncato: ${s.length} caratteri)`; } // ─── Main-thread rich sandbox (retrocompatibile con API DOM custom) ─────────── async function runInMainThread( code: string, onChunk: (s: string) => void, signal?: AbortSignal, ): Promise { const logs: string[] = []; const logFn = (...args: unknown[]) => { const line = args.map(a => typeof a === "string" ? a : JSON.stringify(a, null, 2)).join(" "); logs.push(line); onChunk(logs.join("\n")); }; const downloadFn = (content: string, filename = "file.txt", mimeType = "text/plain") => { const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); setTimeout(() => URL.revokeObjectURL(url), 5000); logs.push(`DL: ${filename}`); onChunk(logs.join("\n")); }; // Gap 1 (S127): window.print() nativo iOS Safari — apre dialog "Salva come PDF". // Zero librerie esterne, funziona offline, rispetta il font di sistema del dispositivo. const generatePDF = (html: string, filename = "documento.pdf") => { const title = filename.replace(/\.pdf$/i, ""); const newWin = window.open("", "_blank"); if (!newWin) { logs.push(`Err generatePDF: popup bloccato — abilita i popup per questo sito`); onChunk(logs.join("\n")); return; } newWin.document.write( `${title}` + `` + `${html}` ); newWin.document.close(); newWin.focus(); newWin.print(); // Su iOS Safari print() è sincrono — aspetta un tick prima di close setTimeout(() => { try { newWin.close(); } catch { /* non-blocking */ } }, 500); logs.push(`PDF: ${filename} — dialog di stampa aperto`); onChunk(logs.join("\n")); }; const globals: Record = { console: { log: logFn, warn: logFn, error: logFn, info: logFn }, fetch, vfs: vfsAsync, memory: agentMemory, download: downloadFn, generatePDF, sleep: (ms: number) => new Promise(r => setTimeout(r, ms)), importModule: (spec: string) => import(/* @vite-ignore */ `https://esm.sh/${spec}`), uuid: () => crypto.randomUUID(), hash: async (str: string, algo = "SHA-256") => { const buf = await crypto.subtle.digest(algo, new TextEncoder().encode(str)); return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, "0")).join(""); }, b64encode: (s: string) => btoa(unescape(encodeURIComponent(s))), b64decode: (s: string) => decodeURIComponent(escape(atob(s))), atob, btoa, URL, Promise, JSON, Math, Date, Object, Array, String, Number, Boolean, parseInt, parseFloat, isNaN, isFinite, encodeURIComponent, decodeURIComponent, setTimeout, clearTimeout, setInterval, clearInterval, TextEncoder, TextDecoder, crypto, Blob, File, FormData, Headers, Request, Response, ReadableStream, ArrayBuffer, Uint8Array, Int32Array, Float64Array, }; if (signal?.aborted) throw new DOMException("Annullato", "AbortError"); const AsyncFn = Object.getPrototypeOf(async function () {}).constructor as new ( ...args: string[] ) => (...args: unknown[]) => Promise; const fn = new AsyncFn(...Object.keys(globals), `"use strict";\n${code}`); await fn(...Object.values(globals)); return logs.join("\n"); } // ─── Module-level factory — Vite analizza new URL() staticamente solo qui ───── // S248-Fix: spostato fuori dalla closure new Promise() — stessa ragione di // workerManager.ts: Vite non bundla worker creati dentro closure/callbacks. function _mkJsRunnerWorker(): Worker { return new Worker( new URL("../workers/jsRunner.worker.ts", import.meta.url), { type: "module" }, ); } // ─── Worker sandbox (off-main-thread, non-blocking, timeout built-in) ───────── function runInWorker( code: string, onChunk: (s: string) => void, signal?: AbortSignal, ): Promise { return new Promise((resolve, reject) => { const worker = _mkJsRunnerWorker(); const runId = crypto.randomUUID(); const lines: string[] = []; worker.onmessage = (e: MessageEvent) => { const msg = e.data; if (msg.id !== runId) return; if (msg.type === "stdout") { lines.push(msg.text.trimEnd()); onChunk(lines.join("\n")); } else if (msg.type === "stderr") { lines.push(`[err] ${msg.text.trimEnd()}`); onChunk(lines.join("\n")); } else if (msg.type === "done") { worker.terminate(); if (msg.exitCode === 0) { resolve(lines.join("\n") || "(nessun output)"); } else if (msg.exitCode === 124) { reject(new Error(`Timeout: esecuzione oltre ${JS_TIMEOUT / 1000}s`)); } else { reject(new Error(lines.join("\n") || "Errore runtime")); } } else if (msg.type === "error") { worker.terminate(); reject(new Error(msg.message)); } }; worker.onerror = (e) => { worker.terminate(); reject(new Error(e.message ?? "Worker error")); }; const abortHandler = () => { worker.postMessage({ type: "cancel", id: runId } satisfies JSRunnerMsg); setTimeout(() => worker.terminate(), 200); reject(new DOMException("Annullato", "AbortError")); }; signal?.addEventListener("abort", abortHandler, { once: true }); worker.postMessage({ type: "run", id: runId, code, timeout: JS_TIMEOUT } satisfies JSRunnerMsg); }); } // ─── Component ──────────────────────────────────────────────────────────────── const CodeRunner = memo(function CodeRunner({ code, language, onResult }: CodeRunnerProps) { const [status, setStatus] = useState("idle"); const [output, setOutput] = useState(""); const [runtime, setRuntime] = useState(null); const [elapsed, setElapsed] = useState(0); const abortRef = useRef(null); const timerRef = useRef | null>(null); const startRef = useRef(0); useEffect(() => () => { abortRef.current?.abort(); if (timerRef.current) clearInterval(timerRef.current); }, []); const stopTimer = () => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } }; const run = useCallback(async () => { abortRef.current?.abort(); const ctrl = new AbortController(); abortRef.current = ctrl; setStatus("running"); setOutput(""); setElapsed(0); const lang = language?.toLowerCase() ?? ""; const isPython = lang === "python" || lang === "py"; const useWorker = !isPython && !needsMainThread(code); const rtLabel: RuntimeLabel = isPython ? "Python" : useWorker ? "JS Worker" : "JS Main"; setRuntime(rtLabel); startRef.current = Date.now(); timerRef.current = setInterval(() => setElapsed(Math.floor((Date.now() - startRef.current) / 1000)), 1000 ); try { let result: string; if (isPython) { result = "[!] Esecuzione Python: usa il backend (HF Spaces) per Pyodide."; } else if (useWorker) { result = await runInWorker(code, s => setOutput(truncate(s)), ctrl.signal); } else { result = await runInMainThread(code, s => setOutput(truncate(s)), ctrl.signal); } stopTimer(); const final = truncate(result || "(nessun output)"); setOutput(final); setStatus("done"); onResult?.(final); } catch (err: unknown) { stopTimer(); const msg = err instanceof Error ? `${err.name}: ${err.message}` : String(err); setOutput(`Err: ${msg}`); setStatus(err instanceof DOMException && err.name === "AbortError" ? "idle" : "error"); onResult?.(`Errore: ${msg}`); } }, [code, language, onResult]); const cancel = useCallback(() => { abortRef.current?.abort(); stopTimer(); setStatus("idle"); setOutput(""); }, []); const reset = useCallback(() => { abortRef.current?.abort(); stopTimer(); setStatus("idle"); setOutput(""); setRuntime(null); setElapsed(0); }, []); return (
{/* Header */}
Esecuzione codice {/* Runtime badge */} {runtime && ( {runtime} )} {status === "running" && ( {elapsed > 0 && `${elapsed}s`} )} {status === "done" && ok} {status === "error" && err}
{status === "idle" && ( )} {status === "running" && ( )} {(status === "done" || status === "error") && ( )}
{/* Body */} {status === "running" && !output && (
Esecuzione in corso…
)} {output && (
          {output}
        
)}
); }); export default CodeRunner;