| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import React, { useCallback, useEffect, useRef, useState } from "react"; |
| import { apiUrl } from "../utils/api.js"; |
|
|
| const POLL_MS = 30_000; |
|
|
| const BACKEND_LABEL = { |
| subprocess: "Local", |
| matrixlab: "MatrixLab", |
| off: "Pass-through", |
| }; |
|
|
| export default function SandboxStatusWidget({ onOpenSettings }) { |
| const [status, setStatus] = useState(null); |
| const [error, setError] = useState(null); |
| const [switching, setSwitching] = useState(false); |
| const timerRef = useRef(null); |
|
|
| const refresh = useCallback(async () => { |
| try { |
| const res = await fetch(apiUrl("/api/sandbox/status")); |
| const data = await res.json(); |
| if (!res.ok) { |
| setError(data.detail || `HTTP ${res.status}`); |
| return; |
| } |
| setStatus(data); |
| setError(null); |
| } catch (err) { |
| setError(err.message || "Unable to reach sandbox status"); |
| } |
| }, []); |
|
|
| useEffect(() => { |
| refresh(); |
| timerRef.current = setInterval(refresh, POLL_MS); |
| return () => clearInterval(timerRef.current); |
| }, [refresh]); |
|
|
| const switchToLocal = async () => { |
| setSwitching(true); |
| try { |
| await fetch(apiUrl("/api/sandbox/config"), { |
| method: "PUT", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ backend: "subprocess" }), |
| }); |
| await refresh(); |
| } finally { |
| setSwitching(false); |
| } |
| }; |
|
|
| if (!status && !error) { |
| return ( |
| <div style={s.shell}> |
| <div style={s.title}>Sandbox</div> |
| <div style={s.dim}>Checking…</div> |
| </div> |
| ); |
| } |
|
|
| if (error) { |
| return ( |
| <div style={s.shell}> |
| <div style={s.title}>Sandbox</div> |
| <div style={{ ...s.statusRow, color: "#fca5a5" }}> |
| <span>⚠</span> Status check failed |
| </div> |
| <div style={s.dim}>{error}</div> |
| </div> |
| ); |
| } |
|
|
| const backend = status.backend || "subprocess"; |
| const ok = !!status.ok; |
| const label = BACKEND_LABEL[backend] || backend; |
| const dot = ok ? "●" : "⚠"; |
| const dotColor = ok ? "#10B981" : "#f59e0b"; |
| const stateText = ok ? "Ready" : "Unavailable"; |
|
|
| return ( |
| <div style={s.shell}> |
| <div style={s.titleRow}> |
| <span style={s.title}>Sandbox</span> |
| <button type="button" onClick={refresh} style={s.refresh} |
| title="Refresh sandbox status">↻</button> |
| </div> |
| <div style={s.statusRow}> |
| <span style={{ color: dotColor, marginRight: 6 }}>{dot}</span> |
| <strong>{label}</strong> |
| <span style={s.dim}> · {stateText}</span> |
| </div> |
| {status.matrixlab_url && backend === "matrixlab" && ( |
| <div style={s.metaRow}> |
| <span style={s.metaKey}>URL</span> |
| <span style={s.metaVal}>{status.matrixlab_url}</span> |
| </div> |
| )} |
| <div style={s.metaRow}> |
| <span style={s.metaKey}>Timeout</span> |
| <span style={s.metaVal}>{status.timeout_sec}s</span> |
| </div> |
| <div style={s.metaRow}> |
| <span style={s.metaKey}>Network</span> |
| <span style={s.metaVal}> |
| {status.allow_network ? "Enabled" : "Disabled"} |
| </span> |
| </div> |
| |
| <div style={s.actions}> |
| {onOpenSettings && ( |
| <button type="button" onClick={onOpenSettings} style={s.action}> |
| {ok ? "Change" : "Repair"} |
| </button> |
| )} |
| {!ok && backend === "matrixlab" && ( |
| <button |
| type="button" |
| onClick={switchToLocal} |
| disabled={switching} |
| style={{ ...s.action, opacity: switching ? 0.6 : 1 }} |
| title="Switch the sandbox backend to Local subprocess" |
| > |
| {switching ? "Switching…" : "Use Local"} |
| </button> |
| )} |
| </div> |
| </div> |
| ); |
| } |
|
|
| const s = { |
| shell: { |
| padding: "10px 12px", |
| background: "#0d0e17", |
| border: "1px solid #1f2937", |
| borderRadius: 8, |
| fontFamily: "system-ui, sans-serif", |
| fontSize: 12, |
| color: "#e4e4e7", |
| }, |
| titleRow: { display: "flex", justifyContent: "space-between", alignItems: "center" }, |
| title: { |
| fontSize: 10, fontWeight: 700, letterSpacing: "0.06em", |
| color: "#9092b5", textTransform: "uppercase", |
| }, |
| refresh: { |
| background: "transparent", color: "#71717a", border: "none", |
| cursor: "pointer", fontSize: 14, padding: 0, |
| }, |
| statusRow: { marginTop: 6, display: "flex", alignItems: "center" }, |
| metaRow: { marginTop: 4, display: "flex", justifyContent: "space-between" }, |
| metaKey: { color: "#9092b5", fontSize: 11 }, |
| metaVal: { color: "#d4d4d8", fontSize: 11, fontFamily: "ui-monospace, monospace" }, |
| actions: { display: "flex", gap: 6, marginTop: 8 }, |
| action: { |
| flex: 1, padding: "4px 8px", fontSize: 11, |
| background: "transparent", color: "#a1a1aa", |
| border: "1px solid #3F3F46", borderRadius: 4, cursor: "pointer", |
| }, |
| dim: { color: "#71717a", fontSize: 11 }, |
| }; |
|
|