import React, { useState } from "react"; import SandboxCanvas from "./SandboxCanvas.jsx"; import ExecutionPlanCard, { fetchExecutionPlan } from "./ExecutionPlanCard.jsx"; // Languages the Run button supports. Anything not in this set still // renders as a normal code block (no button) — keeps the visual contract // honest: if there's a button, the snippet really is executable. const RUNNABLE = new Set([ "python", "py", "javascript", "js", "node", "bash", "sh", "shell", ]); // Friendly badge text per backend, surfaced so the user always knows // which sandbox actually ran their code. Mirrors the labels in // SettingsModal so the two views agree. const BACKEND_LABELS = { subprocess: "Local", matrixlab: "MatrixLab", off: "Pass-through", }; // Map "py" → "python" etc. so the badge always shows the canonical // language name rather than whatever alias the LLM tagged the fence // with. const LANG_DISPLAY = { py: "python", js: "javascript", node: "javascript", sh: "bash", shell: "bash", }; // Default file extension per language — used when the user clicks // "Save to repo" and we need to seed a plausible filename. const LANG_EXT = { python: "py", py: "py", javascript: "js", js: "js", node: "js", bash: "sh", sh: "sh", shell: "sh", }; // Mirror executor's _looks_like_matplotlib heuristic so plt.show() // snippets don't hang the headless sandbox. False positives are // harmless (Agg is a valid backend for any Python script). function looksLikeMatplotlib(code) { if (!code) return false; const lower = code.toLowerCase(); return /import\s+matplotlib|from\s+matplotlib|plt\.show|pyplot/.test(lower); } function applyMatplotlibShim(language, code) { if (language !== "python" && language !== "py") return code; if (!looksLikeMatplotlib(code)) return code; return ( "import os as _gp_os\n" + '_gp_os.environ.setdefault("MPLBACKEND", "Agg")\n' + code ); } /** A single fenced code block with a per-block Run button. * * Optional ``owner``/``repo`` props enable the "Save to repo" button by * giving the save call a real target. When absent the button is * hidden — keeps the contract honest: no button without somewhere to * save. */ export default function RunnableCodeBlock({ language, code, owner, repo }) { const lang = (language || "").trim().toLowerCase(); const canRun = RUNNABLE.has(lang); const [busy, setBusy] = useState(false); const [result, setResult] = useState(null); const [error, setError] = useState(null); const [canvasOpen, setCanvasOpen] = useState(false); const [saving, setSaving] = useState(false); const [saveMsg, setSaveMsg] = useState(null); // Approval-first: clicking ▶ Run first fetches a deterministic // ExecutionPlan and surfaces it inline. The actual sandbox call // is gated on the user clicking "Run in Sandbox" inside the plan. const [pendingPlan, setPendingPlan] = useState(null); const [planError, setPlanError] = useState(null); const display = LANG_DISPLAY[lang] || lang || "text"; const onRunClick = async () => { setPlanError(null); setResult(null); setError(null); setBusy(true); try { const shipped = applyMatplotlibShim(lang, code); const plan = await fetchExecutionPlan({ code: shipped, language: lang, source: "code_block", }); setPendingPlan(plan); } catch (err) { setPlanError(err.message || "Could not build execution plan"); } finally { setBusy(false); } }; const onApprovePlan = async (plan) => { setBusy(true); setResult(null); setError(null); try { const res = await fetch("/api/sandbox/run", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ language: plan.language, code: plan.inline_code, timeout_sec: plan.timeout_sec, }), }); const data = await res.json(); if (!res.ok) { setError(data.detail || `HTTP ${res.status}`); return; } setResult(data); setPendingPlan(null); } catch (err) { setError(err.message || "Run failed"); } finally { setBusy(false); } }; const onCancelPlan = () => { setPendingPlan(null); setPlanError(null); }; const copy = () => { if (navigator?.clipboard) navigator.clipboard.writeText(code).catch(() => {}); }; // POST the snippet to /api/repos/{owner}/{repo}/file with a path // chosen by the user. Pure client-side prompt — no new backend // wiring needed because the endpoint already exists. const saveToRepo = async (snippet, snippetLang) => { if (!owner || !repo) { setSaveMsg("No repository context — open this chat inside a repo to save."); return; } const ext = LANG_EXT[(snippetLang || lang).toLowerCase()] || "txt"; const suggested = `snippets/inline.${ext}`; const path = window.prompt("Save snippet to path (inside repo):", suggested); if (!path) return; setSaving(true); setSaveMsg(null); try { const res = await fetch(`/api/repos/${owner}/${repo}/file`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path, content: snippet, message: `Save snippet from chat (${path})`, }), }); const data = await res.json().catch(() => ({})); if (!res.ok) { setSaveMsg(data.detail || `Save failed (HTTP ${res.status})`); return; } setSaveMsg(`Saved to ${path}`); } catch (err) { setSaveMsg(err.message || "Save failed"); } finally { setSaving(false); } }; return (
{code}
{pendingPlan && (
{error}}
{result?.stdout && {result.stdout}}
{result?.stderr && {result.stderr}}
{Array.isArray(result?.artifacts) && result.artifacts.length > 0 && (
{a.name || a.id}
{a.size && · {a.size} bytes}
{a.mime && · {a.mime}}