// frontend/components/SandboxCanvas.jsx // // File-aware Canvas: a modal that adapts to the file type it was // opened with. Three modes: // // code → split editor + sandbox output. Prepare-Run flow. // Used for .py / .js / .sh / inline snippets. // markdown → split source + rendered preview. No Run button. // Used for .md / .markdown. // text → source-only viewer (read-only by default). // Used for everything else: .json, .yml, .csv, // .html, .txt, ... // // Mode is computed from the ``filename`` prop's extension. When // ``filename`` is null (an anonymous inline snippet from a chat code // block) we fall back to code mode using the ``initialLanguage`` tag. // // The sandbox approval contract is unchanged: Prepare Run dispatches // /api/sandbox/plan, the user approves a green ExecutionPlanCard, // only then /api/sandbox/run actually executes. import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { apiUrl } from "../utils/api.js"; import { fetchExecutionPlan } from "./ExecutionPlanCard.jsx"; const BACKEND_LABELS = { subprocess: "Local", matrixlab: "MatrixLab", off: "Pass-through", }; const LANG_DISPLAY = { py: "python", js: "javascript", node: "javascript", sh: "bash", shell: "bash", }; // Extensions that route through the sandbox. Everything else gets // a viewer-only canvas — README.md must NOT show a Python execution // plan, JSON must NOT pretend it can run. const RUNNABLE_EXTS = new Set(["py", "js", "mjs", "cjs", "sh", "bash"]); const MARKDOWN_EXTS = new Set(["md", "markdown", "mdx"]); // Map extension → canonical sandbox language tag used by the planner. const EXT_TO_LANG = { py: "python", js: "javascript", mjs: "javascript", cjs: "javascript", sh: "bash", bash: "bash", }; // Display label per file type — keeps the header honest about what // the Canvas is showing. const TYPE_LABEL = { python: "Python", javascript: "JavaScript", bash: "Shell", markdown: "Markdown", json: "JSON", yaml: "YAML", csv: "CSV", html: "HTML", text: "Text", }; function extOf(name) { if (!name || !name.includes(".")) return ""; return name.split(".").pop().toLowerCase(); } // Decide which Canvas branch to render based on the opening context. // Pure function so tests can pin it. function resolveCanvasMode({ filename, initialLanguage }) { const ext = extOf(filename || ""); if (filename && MARKDOWN_EXTS.has(ext)) { return { kind: "markdown", typeLabel: "Markdown", language: "markdown" }; } if (filename && RUNNABLE_EXTS.has(ext)) { const lang = EXT_TO_LANG[ext]; return { kind: "code", typeLabel: TYPE_LABEL[lang], language: lang }; } if (filename) { // Known-but-non-runnable: json / yaml / csv / html / txt / etc. const known = { json: "json", yml: "yaml", yaml: "yaml", csv: "csv", html: "html", txt: "text" }[ext]; if (known) { return { kind: "text", typeLabel: TYPE_LABEL[known], language: known }; } return { kind: "text", typeLabel: ext.toUpperCase() || "Text", language: "text" }; } // No filename → inline snippet from a chat code block. Honor the // language tag the chat fence carried. const tag = (initialLanguage || "python").toLowerCase(); if (tag === "py" || tag === "python") return { kind: "code", typeLabel: "Python snippet", language: "python" }; if (tag === "js" || tag === "javascript" || tag === "node") return { kind: "code", typeLabel: "JavaScript snippet", language: "javascript" }; if (tag === "sh" || tag === "bash" || tag === "shell") return { kind: "code", typeLabel: "Shell snippet", language: "bash" }; return { kind: "text", typeLabel: "Text", language: "text" }; } // Minimal, dependency-free Markdown → HTML renderer. Covers the // patterns that matter for README files: headings, bold, italic, // inline code, fenced code blocks, lists, links. Escapes HTML to // avoid XSS when README contains