Spaces:
Runtime error
Runtime error
| /* ============================================================ | |
| Matrix Cloud — Enterprise Console | |
| Production port of the Claude Design prototype, wired to the | |
| real Matrix Runtime API (/v1/*). Live views: Overview, Runtimes, | |
| Sandboxes, Jobs, Models. The rest render the design's reference | |
| data so the console is complete and navigable. | |
| React + ReactDOM are provided as globals by vendored UMD builds. | |
| ============================================================ */ | |
| /* --------------------------------------------------------------- | |
| API client — same-origin calls to the runtime control surface. | |
| --------------------------------------------------------------- */ | |
| const API_BASE = window.MATRIX_API_BASE || ""; | |
| const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); | |
| const TOKEN_KEY = "matrixcloud_token"; | |
| function getToken() { try { return localStorage.getItem(TOKEN_KEY) || ""; } catch (e) { return ""; } } | |
| function setToken(t) { try { t ? localStorage.setItem(TOKEN_KEY, t) : localStorage.removeItem(TOKEN_KEY); } catch (e) {} } | |
| // urlIntent inspects the address bar for email-link flows. Resend links point at | |
| // {APP_URL}/reset?token=mxpr_… and {APP_URL}/verify?token=mxev_…; the SPA serves | |
| // index.html for those paths, so we read the intent here and clear the query. | |
| function urlIntent() { | |
| try { | |
| const u = new URL(window.location.href); | |
| const token = u.searchParams.get("token") || ""; | |
| const path = u.pathname.replace(/\/+$/, ""); | |
| if (path.endsWith("/reset") || token.startsWith("mxpr_")) return { kind: "reset", token }; | |
| if (path.endsWith("/verify") || token.startsWith("mxev_")) return { kind: "verify", token }; | |
| } catch (e) {} | |
| return { kind: "", token: "" }; | |
| } | |
| function clearURLToken() { | |
| try { window.history.replaceState({}, "", window.location.pathname.replace(/\/(reset|verify)$/, "/") || "/"); } catch (e) {} | |
| } | |
| const api = { | |
| async req(method, path, body) { | |
| const opts = { method, headers: {} }; | |
| if (body !== undefined) { | |
| opts.headers["Content-Type"] = "application/json"; | |
| opts.body = JSON.stringify(body); | |
| } | |
| const tok = getToken() || window.MATRIX_API_TOKEN; | |
| if (tok) opts.headers["Authorization"] = "Bearer " + tok; | |
| const res = await fetch(API_BASE + path, opts); | |
| const txt = await res.text(); | |
| let data = null; | |
| try { data = txt ? JSON.parse(txt) : null; } catch (e) { data = txt; } | |
| if (!res.ok) { | |
| const err = new Error((data && data.error) || ("HTTP " + res.status)); | |
| err.status = res.status; err.data = data; | |
| throw err; | |
| } | |
| return data; | |
| }, | |
| get(p) { return this.req("GET", p); }, | |
| post(p, b) { return this.req("POST", p, b); }, | |
| del(p) { return this.req("DELETE", p); }, | |
| eventsURL(p) { return API_BASE + p; }, | |
| }; | |
| // Auth helpers backed by the runtime's user store (SQLite or Postgres/Neon). | |
| const auth = { | |
| async me() { const r = await api.get("/v1/auth/me"); return r.user; }, | |
| async login(email, password) { const r = await api.post("/v1/auth/login", { email, password }); setToken(r.token); return r.user; }, | |
| async signup(name, email, password, workspace) { const r = await api.post("/v1/auth/signup", { name, email, password, workspace }); setToken(r.token); return r.user; }, | |
| async logout(all) { try { await api.post("/v1/auth/logout" + (all ? "?all=true" : ""), {}); } catch (e) {} setToken(null); }, | |
| async forgot(email) { const r = await api.post("/v1/auth/forgot", { email }); return r.message || "Check your email."; }, | |
| async reset(token, password) { const r = await api.post("/v1/auth/reset", { token, password }); return r.message || "Password updated."; }, | |
| async verify(token) { const r = await api.post("/v1/auth/verify", { token }); return r.message || "Email verified."; }, | |
| }; | |
| // Hosted control-plane helpers (workspace-scoped via the session bearer token). | |
| const cloud = { | |
| async listRuntimes() { const r = await api.get("/v1/cloud/runtimes"); return r.runtimes || []; }, | |
| async mintJoinToken(label, maxUses, ttlMinutes) { return api.post("/v1/cloud/join-tokens", { label, max_uses: maxUses, ttl_minutes: ttlMinutes }); }, | |
| async listProviders() { const r = await api.get("/v1/cloud/providers"); return r.providers || []; }, | |
| async setProvider(provider, label, secret, meta) { const r = await api.post("/v1/cloud/providers", { provider, label, secret, meta }); return r.provider; }, | |
| async listAudit() { const r = await api.get("/v1/cloud/audit"); return r.events || []; }, | |
| }; | |
| async function runJobToCompletion(type, payload, timeoutMs = 25000) { | |
| const c = await api.post("/v1/jobs", { type, payload }); | |
| const start = Date.now(); | |
| while (Date.now() - start < timeoutMs) { | |
| const s = await api.get("/v1/jobs/" + c.job_id); | |
| if (["complete", "error", "expired", "cancelled"].includes(s.status)) { | |
| if (s.status !== "complete") throw new Error(s.error || s.status); | |
| return s.result; | |
| } | |
| await sleep(500); | |
| } | |
| throw new Error("timed out"); | |
| } | |
| function fmtParams(n) { | |
| if (!n) return "—"; | |
| if (n >= 1e9) return (n / 1e9).toFixed(1) + "B"; | |
| if (n >= 1e6) return (n / 1e6).toFixed(0) + "M"; | |
| return String(n); | |
| } | |
| /* --------------------------------------------------------------- | |
| Live runtime context — polls /v1/health + /v1/capabilities. | |
| --------------------------------------------------------------- */ | |
| const RuntimeCtx = React.createContext(null); | |
| function useRuntimeState() { | |
| const [s, setS] = React.useState({ loading: true, online: false, health: null, caps: null }); | |
| React.useEffect(() => { | |
| let alive = true; | |
| async function tick() { | |
| try { | |
| const [h, c] = await Promise.all([api.get("/v1/health"), api.get("/v1/capabilities")]); | |
| if (alive) setS({ loading: false, online: true, health: h, caps: c }); | |
| } catch (e) { | |
| if (alive) setS((p) => ({ ...p, loading: false, online: false })); | |
| } | |
| } | |
| tick(); | |
| const t = setInterval(tick, 5000); | |
| return () => { alive = false; clearInterval(t); }; | |
| }, []); | |
| return s; | |
| } | |
| const useRuntime = () => React.useContext(RuntimeCtx); | |
| function useJobs(ms) { | |
| const [jobs, setJobs] = React.useState(null); | |
| const load = React.useCallback(async () => { | |
| try { const r = await api.get("/v1/jobs"); setJobs(r.jobs || []); } | |
| catch (e) { setJobs(null); } | |
| }, []); | |
| React.useEffect(() => { | |
| load(); | |
| if (ms) { const t = setInterval(load, ms); return () => clearInterval(t); } | |
| }, [load, ms]); | |
| return { jobs, refresh: load }; | |
| } | |
| /* --------------------------------------------------------------- | |
| Icons (1.7px stroke). | |
| --------------------------------------------------------------- */ | |
| function GI({ d, size = 18, sw = 1.7, style, className }) { | |
| return ( | |
| <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" | |
| strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round" style={style} className={className} aria-hidden="true">{d}</svg> | |
| ); | |
| } | |
| const G = { | |
| grid: <><rect x="3" y="3" width="7" height="7" rx="1.5" /><rect x="14" y="3" width="7" height="7" rx="1.5" /><rect x="3" y="14" width="7" height="7" rx="1.5" /><rect x="14" y="14" width="7" height="7" rx="1.5" /></>, | |
| server: <><rect x="3" y="4" width="18" height="7" rx="2" /><rect x="3" y="13" width="18" height="7" rx="2" /><path d="M7 7.5h.01M7 16.5h.01" /></>, | |
| download: <><path d="M12 3v12" /><path d="m7 10 5 5 5-5" /><path d="M5 21h14" /></>, | |
| cpu: <><rect x="6" y="6" width="12" height="12" rx="2" /><path d="M9 2v2M15 2v2M9 20v2M15 20v2M2 9h2M2 15h2M20 9h2M20 15h2" /><rect x="9.5" y="9.5" width="5" height="5" rx="1" /></>, | |
| bot: <><rect x="4" y="8" width="16" height="11" rx="3" /><path d="M12 8V4M9 4h6" /><circle cx="9" cy="13.5" r="1.2" fill="currentColor" stroke="none" /><circle cx="15" cy="13.5" r="1.2" fill="currentColor" stroke="none" /></>, | |
| activity: <path d="M22 12h-4l-3 9L9 3l-3 9H2" />, | |
| logs: <><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><path d="M14 2v6h6M9 13h6M9 17h4" /></>, | |
| shield: <><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10" /><path d="m9 12 2 2 4-4" /></>, | |
| audit: <><path d="M9 11l3 3 7-7" /><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" /></>, | |
| gear: <><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.6 1.6 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.6 1.6 0 0 0-1.8-.3 1.6 1.6 0 0 0-1 1.5V21a2 2 0 0 1-4 0v-.1A1.6 1.6 0 0 0 9 19.4a1.6 1.6 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.6 1.6 0 0 0 .3-1.8 1.6 1.6 0 0 0-1.5-1H3a2 2 0 0 1 0-4h.1A1.6 1.6 0 0 0 4.6 9a1.6 1.6 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.6 1.6 0 0 0 1.8.3H9a1.6 1.6 0 0 0 1-1.5V3a2 2 0 0 1 4 0v.1a1.6 1.6 0 0 0 1 1.5 1.6 1.6 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.6 1.6 0 0 0-.3 1.8V9a1.6 1.6 0 0 0 1.5 1H21a2 2 0 0 1 0 4h-.1a1.6 1.6 0 0 0-1.5 1z" /></>, | |
| bell: <><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9" /><path d="M13.7 21a2 2 0 0 1-3.4 0" /></>, | |
| search: <><circle cx="11" cy="11" r="7" /><path d="m21 21-4.3-4.3" /></>, | |
| chev: <path d="m6 9 6 6 6-6" />, | |
| chevr: <path d="m9 18 6-6-6-6" />, | |
| plus: <path d="M12 5v14M5 12h14" />, | |
| arrowup: <><path d="M12 19V5" /><path d="m5 12 7-7 7 7" /></>, | |
| arrowdn: <><path d="M12 5v14" /><path d="m19 12-7 7-7-7" /></>, | |
| check: <path d="M20 6 9 17l-5-5" />, | |
| x: <><path d="M18 6 6 18M6 6l12 12" /></>, | |
| play: <path d="M6 4l14 8-14 8z" />, | |
| refresh: <><path d="M3 12a9 9 0 0 1 15-6.7L21 8" /><path d="M21 3v5h-5" /><path d="M21 12a9 9 0 0 1-15 6.7L3 16" /><path d="M3 21v-5h5" /></>, | |
| menu: <><path d="M4 6h16M4 12h16M4 18h16" /></>, | |
| globe: <><circle cx="12" cy="12" r="9" /><path d="M3 12h18" /><path d="M12 3a14 14 0 0 1 0 18 14 14 0 0 1 0-18" /></>, | |
| key: <><circle cx="8" cy="15" r="4" /><path d="m10.8 12.2 8.2-8.2M17 5l2 2M14 8l2 2" /></>, | |
| zap: <path d="M13 2 4 14h7l-1 8 9-12h-7z" />, | |
| clock: <><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3 2" /></>, | |
| layers: <><path d="m12 2 9 5-9 5-9-5z" /><path d="m3 12 9 5 9-5" /><path d="m3 17 9 5 9-5" /></>, | |
| user: <><circle cx="12" cy="8" r="4" /><path d="M4 21a8 8 0 0 1 16 0" /></>, | |
| terminal: <><path d="m4 17 6-6-6-6" /><path d="M12 19h8" /></>, | |
| }; | |
| /* --------------------------------------------------------------- | |
| Reference data (fallback + demo fleet/governance context). | |
| --------------------------------------------------------------- */ | |
| const FILESYSTEM_CMD = "npx -y @modelcontextprotocol/server-filesystem /tmp"; | |
| const STATUS_CLASS = { complete: "green", running: "blue", queued: "gray", error: "red", expired: "amber", cancelled: "amber" }; | |
| // Real-data hooks — the console renders only what the backend reports. | |
| function useFetch(path, pollMs) { | |
| const [data, setData] = React.useState(null); | |
| const load = React.useCallback(async () => { | |
| try { setData(await api.get(path)); } catch (e) { setData(null); } | |
| }, [path]); | |
| React.useEffect(() => { | |
| load(); | |
| if (pollMs) { const t = setInterval(load, pollMs); return () => clearInterval(t); } | |
| }, [load, pollMs]); | |
| return { data, refresh: load }; | |
| } | |
| function useRuntimes(pollMs) { | |
| const { data, refresh } = useFetch("/v1/runtimes", pollMs); | |
| return { runtimes: data ? data.runtimes || [] : null, refresh }; | |
| } | |
| function useCatalog() { | |
| const { data } = useFetch("/v1/catalog"); | |
| if (!data) return null; | |
| return (data.items || []).map((it) => ({ ...it, kindClass: it.kind_class, startCommand: it.start_command })); | |
| } | |
| // useCloudRuntimes lists runtimes registered to this workspace (HF Spaces and | |
| // self-hosted runtimes that joined via a join token). Distinct from /v1/runtimes | |
| // which describes this local node. | |
| function useCloudRuntimes(pollMs) { | |
| const { data, refresh } = useFetch("/v1/cloud/runtimes", pollMs); | |
| return { runtimes: data ? data.runtimes || [] : null, refresh }; | |
| } | |
| function timeAgo(iso) { | |
| if (!iso) return "—"; | |
| const t = new Date(iso).getTime(); | |
| if (!t) return "—"; | |
| const s = Math.max(0, Math.round((Date.now() - t) / 1000)); | |
| if (s < 60) return s + "s ago"; | |
| if (s < 3600) return Math.round(s / 60) + "m ago"; | |
| if (s < 86400) return Math.round(s / 3600) + "h ago"; | |
| return Math.round(s / 86400) + "d ago"; | |
| } | |
| const RT_STATUS_CLASS = { online: "green", idle: "amber", pending: "violet", offline: "red" }; | |
| /* --------------------------------------------------------------- | |
| Shared primitives. | |
| --------------------------------------------------------------- */ | |
| function Spark({ data, color }) { | |
| const max = Math.max(...data); | |
| return <div className="spark">{data.map((v, i) => <span key={i} style={{ height: (v / max) * 100 + "%", background: color }} />)}</div>; | |
| } | |
| function ItemMono({ initials, size = 44 }) { | |
| return ( | |
| <div style={{ width: size, height: size, borderRadius: 11, flexShrink: 0, display: "flex", alignItems: "center", justifyContent: "center", | |
| border: "1px solid var(--line-2)", background: "var(--raised)", color: "var(--acc)", fontFamily: "var(--mono)", fontWeight: 700, fontSize: size * 0.3 }}>{initials}</div> | |
| ); | |
| } | |
| function CmdBlock({ label, children }) { | |
| const [copied, setCopied] = React.useState(false); | |
| function copy() { try { navigator.clipboard.writeText(children); } catch (e) {} setCopied(true); setTimeout(() => setCopied(false), 1300); } | |
| return ( | |
| <div className="card" style={{ overflow: "hidden", background: "var(--inset)" }}> | |
| <div className="card-h" style={{ padding: "9px 14px" }}> | |
| <span className="mono" style={{ fontSize: 11, color: "var(--ink-3)", textTransform: "uppercase", letterSpacing: "0.1em" }}>{label}</span> | |
| <button className="chip" style={{ cursor: "pointer" }} onClick={copy}>{copied ? <><GI d={G.check} size={11} /> copied</> : <><GI d={G.logs} size={11} /> copy</>}</button> | |
| </div> | |
| <pre className="mono" style={{ margin: 0, padding: 15, fontSize: 12.5, lineHeight: 1.7, color: "var(--ink)", overflowX: "auto" }}> | |
| <span style={{ color: "var(--ink-4)", userSelect: "none" }}>$ </span>{children} | |
| </pre> | |
| </div> | |
| ); | |
| } | |
| function LiveTag() { | |
| return <span className="chip green" title="Wired to this Matrix Runtime"><span className="dot green pulse" /> live</span>; | |
| } | |
| // WARN_META maps readiness warning codes to a human title, severity and the | |
| // console route that fixes them. | |
| const WARN_META = { | |
| api_token_missing: { sev: "high", title: "API token not set", route: "settings" }, | |
| matrixshell_enabled: { sev: "med", title: "MatrixShell enabled", route: "settings" }, | |
| sqlite_in_use: { sev: "med", title: "Using SQLite (single-node)", route: "settings" }, | |
| store_unavailable: { sev: "high", title: "User store unavailable", route: "settings" }, | |
| local_dev_mode: { sev: "low", title: "Local-dev mode", route: null }, | |
| }; | |
| // ReadinessBanner surfaces production-safety warnings from GET /v1/ready. It is | |
| // dismissible per-session and only renders when there is something to show. | |
| function ReadinessBanner({ go }) { | |
| const { data } = useFetch("/v1/ready", 30000); | |
| const [dismissed, setDismissed] = React.useState(() => { | |
| try { return sessionStorage.getItem("mc-ready-dismissed") === "1"; } catch (e) { return false; } | |
| }); | |
| if (!data || dismissed) return null; | |
| // Hide the low-severity local-dev-only notice unless something else is wrong. | |
| const warnings = (data.warnings || []).filter((w) => (WARN_META[w.code]?.sev || "low") !== "low"); | |
| if (warnings.length === 0) return null; | |
| const worst = warnings.some((w) => WARN_META[w.code]?.sev === "high") ? "high" : "med"; | |
| const color = worst === "high" ? "var(--red)" : "var(--amber, #f5a623)"; | |
| const bg = worst === "high" ? "var(--red-soft)" : "rgba(245,166,35,0.10)"; | |
| function dismiss() { try { sessionStorage.setItem("mc-ready-dismissed", "1"); } catch (e) {} setDismissed(true); } | |
| return ( | |
| <div style={{ margin: "0 0 16px", padding: "12px 14px", borderRadius: "var(--r-md)", border: "1px solid " + color, background: bg }}> | |
| <div style={{ display: "flex", alignItems: "center", gap: 10 }}> | |
| <span style={{ color, display: "inline-flex", flexShrink: 0 }}><GI d={G.shield} size={17} /></span> | |
| <span style={{ fontWeight: 700, fontSize: 13.5 }}>Production readiness</span> | |
| <span className="chip" style={{ color }}>{warnings.length} warning{warnings.length > 1 ? "s" : ""}</span> | |
| <div style={{ flex: 1 }} /> | |
| <button className="iconbtn" onClick={dismiss} title="Dismiss for this session"><GI d={G.x} size={15} /></button> | |
| </div> | |
| <div style={{ display: "grid", gap: 6, marginTop: 10 }}> | |
| {warnings.map((w) => { | |
| const meta = WARN_META[w.code] || { title: w.code, route: null }; | |
| return ( | |
| <div key={w.code} style={{ display: "flex", alignItems: "center", gap: 10, fontSize: 12.5, color: "var(--ink-2)" }}> | |
| <span className="dot" style={{ background: WARN_META[w.code]?.sev === "high" ? "var(--red)" : color }} /> | |
| <b style={{ color: "var(--ink)" }}>{meta.title}.</b> <span style={{ color: "var(--ink-3)" }}>{w.message}</span> | |
| {meta.route && go && <button onClick={() => go(meta.route)} style={{ color: "var(--acc-2)", fontWeight: 600, marginLeft: "auto" }}>Fix →</button>} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| /* --------------------------------------------------------------- | |
| Overview (live header + recent jobs). | |
| --------------------------------------------------------------- */ | |
| function OverviewView({ go }) { | |
| const rt = useRuntime(); | |
| const { jobs } = useJobs(5000); | |
| const liveJobs = jobs || []; | |
| const running = liveJobs.filter((j) => j.status === "running" || j.status === "queued").length; | |
| const caps = (rt.caps && rt.caps.capabilities) || []; | |
| const limits = (rt.caps && rt.caps.limits) || {}; | |
| const metrics = [ | |
| { lab: "Runtime", ic: G.server, val: rt.online ? "Online" : (rt.loading ? "…" : "Offline"), delta: rt.health ? rt.health.mode : "—", dir: rt.online ? "up" : "down", note: "this node" }, | |
| { lab: "Capabilities", ic: G.zap, val: String(caps.length || "—"), delta: rt.health ? "v" + rt.health.version : "—", dir: "flat", note: "advertised" }, | |
| { lab: "Active jobs", ic: G.activity, val: String(running), delta: String(liveJobs.length) + " total", dir: "up", note: "live" }, | |
| { lab: "Max concurrency", ic: G.layers, val: String(limits.max_concurrent_jobs || "—"), delta: (limits.max_ttl_seconds || 600) + "s TTL", dir: "flat", note: "limit" }, | |
| ]; | |
| const { runtimes } = useRuntimes(5000); | |
| const recent = liveJobs.slice(0, 6); | |
| const rtRuntimes = (rt.caps && rt.caps.runtimes) || {}; | |
| // Real job-status breakdown from live jobs (no fabricated throughput). | |
| const byStatus = {}; | |
| liveJobs.forEach((j) => { byStatus[j.status] = (byStatus[j.status] || 0) + 1; }); | |
| const statusRows = ["complete", "running", "queued", "error", "expired", "cancelled"].filter((s) => byStatus[s]).map((s) => [s, byStatus[s]]); | |
| return ( | |
| <div className="wrap rise"> | |
| <div className="phead"> | |
| <div> | |
| <p className="eyebrow">{rt.health ? rt.health.mode + " · " + rt.health.runtime_id : "matrix runtime"}</p> | |
| <h1>Overview</h1> | |
| <p>Real-time health of this Matrix Runtime, its capabilities, and jobs.</p> | |
| </div> | |
| <div style={{ display: "flex", gap: 10 }}> | |
| <button className="btn btn-ghost" onClick={() => go("logs")}><GI d={G.logs} size={15} /> Live logs</button> | |
| <button className="btn btn-primary" onClick={() => go("catalog")}><GI d={G.play} size={15} /> New sandbox</button> | |
| </div> | |
| </div> | |
| <div className="metrics"> | |
| {metrics.map((m) => ( | |
| <div className="metric" key={m.lab}> | |
| <div className="lab"><span style={{ color: "var(--acc)", display: "inline-flex" }}><GI d={m.ic} size={15} /></span> {m.lab}</div> | |
| <div className="val">{m.val}</div> | |
| <div className={"delta " + m.dir}> | |
| {m.dir !== "flat" && <GI d={m.dir === "up" ? G.arrowup : G.arrowdn} size={12} />} | |
| {m.delta} <span style={{ color: "var(--ink-4)" }}>· {m.note}</span> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| <div style={{ display: "grid", gridTemplateColumns: "1.5fr 1fr", gap: 16, marginTop: 16 }} className="ov-grid"> | |
| <div className="card"> | |
| <div className="card-h"><span className="t">Runtimes {runtimes && <LiveTag />}</span><button className="chip" onClick={() => go("runtimes")}>manage <GI d={G.chevr} size={12} /></button></div> | |
| <table className="tbl"> | |
| <thead><tr><th>Runtime</th><th className="hide-sm">Mode</th><th>Region</th><th>Jobs</th><th>Status</th></tr></thead> | |
| <tbody> | |
| {(runtimes || []).length === 0 && <tr><td colSpan={5} style={{ color: "var(--ink-4)", textAlign: "center", padding: 20 }}>{runtimes ? "No runtimes." : "…"}</td></tr>} | |
| {(runtimes || []).map((c) => ( | |
| <tr className="row" key={c.id}> | |
| <td className="nm mono">{c.name}</td> | |
| <td className="hide-sm"><span className="chip">{c.mode}</span></td> | |
| <td className="mono" style={{ fontSize: 12 }}>{c.region}</td> | |
| <td className="mono">{c.jobs}</td> | |
| <td><span className={"chip " + c.statusClass}><span className={"dot " + c.statusClass + (c.statusClass === "green" ? " pulse" : "")} /> {c.status}</span></td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| <div style={{ display: "grid", gap: 16, alignContent: "start" }}> | |
| <div className="card card-pad"> | |
| <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}> | |
| <span style={{ fontSize: 13, fontWeight: 700 }}>Jobs by status</span><span className="chip">{liveJobs.length} total</span> | |
| </div> | |
| <div style={{ display: "grid", gap: 9, marginTop: 13 }}> | |
| {statusRows.length === 0 && <div style={{ fontSize: 12.5, color: "var(--ink-4)" }}>No jobs yet — start a sandbox or inspect a model.</div>} | |
| {statusRows.map(([s, n]) => ( | |
| <div key={s} style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 12.5 }}> | |
| <span className={"chip " + (STATUS_CLASS[s] || "")} style={{ height: 20, minWidth: 78 }}>{s}</span> | |
| <div className="bar" style={{ flex: 1 }}><span className={STATUS_CLASS[s] === "red" ? "amber" : "green"} style={{ width: Math.round((n / liveJobs.length) * 100) + "%" }} /></div> | |
| <span className="mono" style={{ fontSize: 11, color: "var(--ink-3)" }}>{n}</span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="card card-pad"> | |
| <span style={{ fontSize: 13, fontWeight: 700 }}>Runtime health</span> | |
| <div style={{ display: "grid", gap: 11, marginTop: 13 }}> | |
| {[["Control plane", rt.online ? "green" : "red", rt.online ? "operational" : "unreachable"], | |
| ["Node runner", rtRuntimes.node ? "green" : "amber", rtRuntimes.node ? "ready" : "not found"], | |
| ["Python runner", rtRuntimes.python ? "green" : "amber", rtRuntimes.python ? "ready" : "not found"], | |
| ["Ollama", rtRuntimes.ollama ? "green" : "gray", rtRuntimes.ollama ? "ready" : "not detected"], | |
| ["vLLM", rtRuntimes.vllm ? "green" : "gray", rtRuntimes.vllm ? "ready" : "not detected"]].map(([k, s, v]) => ( | |
| <div key={k} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", fontSize: 12.5 }}> | |
| <span style={{ display: "flex", alignItems: "center", gap: 8, color: "var(--ink-2)" }}><span className={"dot " + s + (s === "green" ? " pulse" : "")} /> {k}</span> | |
| <span style={{ color: "var(--ink-4)" }}>{v}</span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="card" style={{ marginTop: 16 }}> | |
| <div className="card-h"><span className="t">Recent jobs {jobs && <LiveTag />}</span><button className="chip" onClick={() => go("agents")}>view all <GI d={G.chevr} size={12} /></button></div> | |
| <table className="tbl"> | |
| <thead><tr><th>Job ID</th><th>Type</th><th>Status</th><th className="hide-sm">Created</th></tr></thead> | |
| <tbody> | |
| {recent.length === 0 && <tr><td colSpan={4} style={{ color: "var(--ink-4)", textAlign: "center", padding: 24 }}>No jobs yet — start a sandbox or inspect a model.</td></tr>} | |
| {recent.map((j) => ( | |
| <tr className="row" key={j.job_id}> | |
| <td className="nm mono">{j.job_id}</td> | |
| <td><span className="chip">{j.type}</span></td> | |
| <td><span className={"chip " + (STATUS_CLASS[j.status] || "")}>{j.status === "running" && <span className="dot blue" />}{j.status}</span></td> | |
| <td className="hide-sm mono" style={{ fontSize: 12 }}>{(j.created_at || "").replace("T", " ").replace("Z", "")}</td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| /* --------------------------------------------------------------- | |
| Catalog + detail. | |
| --------------------------------------------------------------- */ | |
| function CatalogView({ openItem }) { | |
| const [tab, setTab] = React.useState("All"); | |
| const [q, setQ] = React.useState(""); | |
| const catalog = useCatalog(); | |
| const tabs = ["All", "MCP Servers", "Agents", "Tools", "Models", "Verified", "Sandbox Enabled"]; | |
| const items = (catalog || []).filter((it) => { | |
| if (q && !(it.name + it.desc + it.id).toLowerCase().includes(q.toLowerCase())) return false; | |
| if (tab === "All") return true; | |
| if (tab === "Verified") return it.verified; | |
| if (tab === "Sandbox Enabled") return it.sandbox; | |
| return it.kind === tab.replace(/s$/, "") || it.kind + "s" === tab; | |
| }); | |
| return ( | |
| <div className="wrap rise"> | |
| <div className="phead"> | |
| <div><p className="eyebrow">Registry {catalog && <LiveTag />}</p><h1>Catalog</h1><p>Curated MCP servers, agents, tools, and models served by this runtime. Test sandbox-enabled servers in a 10-minute session before you install.</p></div> | |
| </div> | |
| <div className="topsearch" style={{ width: "100%", maxWidth: 480, height: 40, marginBottom: 16 }}> | |
| <GI d={G.search} size={16} /><input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search MCP servers, tools, agents, models…" /> | |
| </div> | |
| <div style={{ display: "flex", gap: 7, flexWrap: "wrap", marginBottom: 18 }}> | |
| {tabs.map((t) => <button key={t} onClick={() => setTab(t)} className={"chip" + (tab === t ? " green" : "")} style={{ height: 30, cursor: "pointer", fontFamily: "var(--font)", fontSize: 12.5 }}>{t}</button>)} | |
| </div> | |
| <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }} className="ov-grid"> | |
| {catalog === null && <div style={{ color: "var(--ink-4)", padding: 24 }}>Loading catalog…</div>} | |
| {items.map((it) => ( | |
| <div className="card card-pad" key={it.id} style={{ display: "flex", flexDirection: "column", gap: 13 }}> | |
| <div style={{ display: "flex", gap: 13 }}> | |
| <ItemMono initials={it.initials} /> | |
| <div style={{ minWidth: 0, flex: 1 }}> | |
| <div style={{ display: "flex", alignItems: "center", gap: 7, flexWrap: "wrap" }}> | |
| <span className={"chip " + it.kindClass}>{it.kind}</span> | |
| {it.verified && <span className="chip green"><GI d={G.check} size={11} /> Verified</span>} | |
| {it.sandbox && it.startCommand && <span className="chip blue">Sandbox · live</span>} | |
| {it.sandbox && !it.startCommand && <span className="chip blue">Sandbox</span>} | |
| </div> | |
| <button onClick={() => openItem(it)} style={{ display: "block", marginTop: 9, fontSize: 16, fontWeight: 700, color: "var(--ink)", textAlign: "left" }}>{it.name}</button> | |
| </div> | |
| </div> | |
| <p style={{ margin: 0, fontSize: 12.5, lineHeight: 1.55, color: "var(--ink-3)" }}>{it.desc}</p> | |
| <div style={{ display: "flex", gap: 14, fontSize: 11.5, color: "var(--ink-3)", fontFamily: "var(--mono)", flexWrap: "wrap" }}> | |
| <span>{it.runtime}</span><span>· {it.source}</span><span>· {it.license}</span><span>· {it.secrets ? "needs secrets" : "no secrets"}</span> | |
| </div> | |
| <div style={{ display: "flex", gap: 9, marginTop: 2 }}> | |
| {it.sandbox | |
| ? <button className="btn btn-primary btn-sm" onClick={() => openItem(it, true)}><GI d={G.play} size={13} /> Test Sandbox</button> | |
| : <button className="btn btn-ghost btn-sm" onClick={() => openItem(it)}>View</button>} | |
| <button className="btn btn-ghost btn-sm" onClick={() => openItem(it)}>Details</button> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function DetailView({ item, back, startSandbox }) { | |
| const manifest = { | |
| id: item.id, kind: item.kind.toLowerCase().replace(" ", "_"), | |
| runtime: { type: item.runtime.split(" / ")[0].toLowerCase(), transport: item.runtime.includes("stdio") ? "stdio" : "remote" }, | |
| sandbox: { enabled: item.sandbox, ttl_seconds: 600, requires_secrets: item.secrets }, | |
| source: item.source, license: item.license, version: item.version, | |
| }; | |
| const rows = [["Runtime", item.runtime], ["Source", item.source], ["License", item.license], ["Secrets", item.secrets ? "Required" : "None"], ["Network", item.network], ["Start command", item.startCommand || "—"]]; | |
| return ( | |
| <div className="wrap rise"> | |
| <button className="chip" onClick={back} style={{ marginBottom: 16, cursor: "pointer" }}><GI d={G.chev} size={13} style={{ transform: "rotate(90deg)" }} /> Catalog</button> | |
| <div style={{ display: "flex", gap: 18, flexWrap: "wrap", alignItems: "flex-start", justifyContent: "space-between" }}> | |
| <div style={{ display: "flex", gap: 16, minWidth: 0 }}> | |
| <ItemMono initials={item.initials} size={64} /> | |
| <div style={{ minWidth: 0 }}> | |
| <div style={{ display: "flex", gap: 7, flexWrap: "wrap" }}> | |
| <span className={"chip " + item.kindClass}>{item.kind}</span> | |
| {item.verified && <span className="chip green"><GI d={G.check} size={11} /> Verified</span>} | |
| {item.sandbox && <span className="chip blue">Sandbox Enabled</span>} | |
| {!item.secrets && <span className="chip">No Secrets</span>} | |
| </div> | |
| <h1 style={{ margin: "13px 0 0", fontSize: 26, fontWeight: 700, letterSpacing: "-0.02em" }}>{item.name}</h1> | |
| <p style={{ margin: "9px 0 0", fontSize: 13.5, color: "var(--ink-3)", maxWidth: 540, lineHeight: 1.6 }}>{item.desc}</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div style={{ display: "flex", gap: 10, marginTop: 22, flexWrap: "wrap" }}> | |
| <button className="btn btn-primary" disabled={!item.sandbox} onClick={() => startSandbox(item)} title={item.sandbox ? "" : "Sandbox not available for this item"}> | |
| <GI d={G.play} size={15} /> Test in 10-minute Sandbox | |
| </button> | |
| <button className="btn btn-ghost"><GI d={G.download} size={15} /> Install</button> | |
| <button className="btn btn-ghost"><GI d={G.logs} size={15} /> Copy CLI Command</button> | |
| <button className="btn btn-ghost"><GI d={G.bot} size={15} /> Add to Agent</button> | |
| </div> | |
| <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16, marginTop: 22 }} className="ov-grid"> | |
| <div style={{ display: "grid", gap: 16, alignContent: "start" }}> | |
| <div className="card"> | |
| <div className="card-h"><span className="t">Overview</span></div> | |
| <div style={{ padding: "4px 18px" }}> | |
| {rows.map(([k, v]) => ( | |
| <div key={k} style={{ display: "flex", justifyContent: "space-between", padding: "11px 0", borderBottom: "1px solid var(--line)", fontSize: 13 }}> | |
| <span style={{ color: "var(--ink-3)" }}>{k}</span><span className="mono" style={{ color: "var(--ink)" }}>{v}</span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="card card-pad"> | |
| <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}> | |
| <span style={{ fontWeight: 700, fontSize: 14 }}>Sandbox</span> | |
| <span className={"chip " + (item.sandbox ? "green" : "")}>{item.sandbox ? "Available" : "Unavailable"}</span> | |
| </div> | |
| <div style={{ display: "grid", gap: 9, marginTop: 13, fontSize: 12.5 }}> | |
| {[["TTL", "10 minutes"], ["Requires secrets", item.secrets ? "Yes" : "No"], ["Network", item.network], ["Runtime", item.runtime.split(" / ")[0]]].map(([k, v]) => ( | |
| <div key={k} style={{ display: "flex", justifyContent: "space-between" }}><span style={{ color: "var(--ink-3)" }}>{k}</span><span className="mono">{v}</span></div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| <div className="card" style={{ overflow: "hidden" }}> | |
| <div className="card-h"><span className="t">Manifest</span><span className="chip mono">manifest.json</span></div> | |
| <pre className="mono" style={{ margin: 0, padding: 16, fontSize: 12, lineHeight: 1.7, color: "var(--ink-2)", overflowX: "auto" }}>{JSON.stringify(manifest, null, 2)}</pre> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| /* --------------------------------------------------------------- | |
| Sandbox modal + LIVE session (wired to /v1/sandbox/sessions). | |
| --------------------------------------------------------------- */ | |
| function SandboxModal({ item, onClose, onStart }) { | |
| const real = !!item.startCommand; | |
| return ReactDOM.createPortal( | |
| <div className="scrim" style={{ position: "fixed", inset: 0, zIndex: 70, display: "flex", alignItems: "center", justifyContent: "center", padding: 24, background: "rgba(5,8,12,0.7)", backdropFilter: "blur(6px)" }} | |
| onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}> | |
| <div className="rise card" style={{ width: "100%", maxWidth: 460, background: "var(--panel-2)" }}> | |
| <div className="card-h"><span className="t">Test {item.name}</span><button className="iconbtn" onClick={onClose}><GI d={G.x} size={16} /></button></div> | |
| <div style={{ padding: 18 }}> | |
| <p style={{ margin: 0, fontSize: 13, lineHeight: 1.6, color: "var(--ink-2)" }}> | |
| {real | |
| ? "This starts a real sandbox on this Matrix Runtime over stdio. No production credentials are used. The session expires automatically after 10 minutes." | |
| : "This item has no runnable start command in this demo. The session will play a simulated lifecycle."} | |
| </p> | |
| <div style={{ display: "grid", gap: 1, marginTop: 16, borderRadius: "var(--r-sm)", overflow: "hidden", border: "1px solid var(--line)" }}> | |
| {[["Runtime", "Matrix Runtime · local"], ["TTL", "10 minutes"], ["Network", item.network], ["Secrets", "None"], ["Command", real ? item.startCommand : "—"]].map(([k, v]) => ( | |
| <div key={k} style={{ display: "flex", gap: 12, padding: "11px 13px", background: "var(--inset)" }}> | |
| <span style={{ width: 84, flexShrink: 0, fontSize: 12, color: "var(--ink-3)", textTransform: "uppercase", letterSpacing: "0.05em" }}>{k}</span> | |
| <span className="mono" style={{ fontSize: 12.5, color: "var(--ink)", wordBreak: "break-all" }}>{v}</span> | |
| </div> | |
| ))} | |
| </div> | |
| <div style={{ display: "flex", gap: 10, marginTop: 18 }}> | |
| <button className="btn btn-primary" style={{ flex: 1 }} onClick={onStart}><GI d={G.play} size={15} /> Start Sandbox</button> | |
| <button className="btn btn-ghost" onClick={onClose}>Cancel</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div>, | |
| document.body | |
| ); | |
| } | |
| function sampleArgs(tool) { | |
| const s = tool.input_schema || tool.inputSchema || {}; | |
| const props = s.properties || {}; | |
| const req = s.required || Object.keys(props); | |
| const out = {}; | |
| for (const k of req) { | |
| const p = props[k] || {}; | |
| if (k === "path") out[k] = "/tmp"; | |
| else if (k === "pattern") out[k] = "*"; | |
| else if (p.type === "number" || p.type === "integer") out[k] = 0; | |
| else if (p.type === "boolean") out[k] = false; | |
| else if (p.type === "array") out[k] = []; | |
| else out[k] = ""; | |
| } | |
| return out; | |
| } | |
| const SIM_LIFECYCLE = [ | |
| { step: "validate", status: "ok", message: "Command accepted" }, | |
| { step: "sandbox", status: "start", message: "Temporary directory created" }, | |
| { step: "mcp_start", status: "start", message: "MCP server started" }, | |
| { step: "mcp_initialize", status: "ok", message: "MCP initialize succeeded" }, | |
| { step: "tools_list", status: "ok", message: "Found 4 tools" }, | |
| { step: "ready", status: "ok", message: "Sandbox ready" }, | |
| ]; | |
| const SIM_TOOLS = [ | |
| { name: "list_directory", description: "Read directory contents.", input_schema: { properties: { path: { type: "string" } }, required: ["path"] } }, | |
| { name: "read_file", description: "Read the contents of a file.", input_schema: { properties: { path: { type: "string" } }, required: ["path"] } }, | |
| { name: "write_file", description: "Write content to a file.", input_schema: { properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"] } }, | |
| ]; | |
| function SandboxSession({ item, back }) { | |
| const [events, setEvents] = React.useState([]); | |
| const [phase, setPhase] = React.useState("starting"); // starting | ready | expired | error | |
| const [tools, setTools] = React.useState([]); | |
| const [secs, setSecs] = React.useState(600); | |
| const [activeTool, setActiveTool] = React.useState(0); | |
| const [toolOut, setToolOut] = React.useState(null); | |
| const [running, setRunning] = React.useState(false); | |
| const [live, setLive] = React.useState(true); | |
| const sessRef = React.useRef(null); | |
| const logRef = React.useRef(null); | |
| React.useEffect(() => { | |
| let es = null, timer = null, cancelled = false; | |
| const cmd = item.startCommand; | |
| async function loadTools(sid) { | |
| try { const r = await api.get("/v1/sandbox/sessions/" + sid + "/tools"); if (!cancelled) setTools(r.tools || []); } catch (e) {} | |
| } | |
| function simulate() { | |
| setLive(false); | |
| let i = 0; | |
| timer = setInterval(() => { | |
| if (i >= SIM_LIFECYCLE.length) { clearInterval(timer); setPhase("ready"); setTools(SIM_TOOLS); startCountdown(); return; } | |
| setEvents((e) => [...e, SIM_LIFECYCLE[i]]); i++; | |
| }, 600); | |
| } | |
| function startCountdown(expiresAt) { | |
| timer = setInterval(() => { | |
| setSecs((s) => { | |
| if (expiresAt) return Math.max(0, Math.round((expiresAt - Date.now()) / 1000)); | |
| return Math.max(0, s - 1); | |
| }); | |
| }, 1000); | |
| } | |
| if (!cmd) { simulate(); return () => { cancelled = true; if (timer) clearInterval(timer); }; } | |
| (async () => { | |
| try { | |
| const r = await api.post("/v1/sandbox/sessions", { entity_id: item.id, ttl_seconds: 600, runtime: "node", transport: "stdio", start_command: cmd }); | |
| if (cancelled) { api.del("/v1/sandbox/sessions/" + r.session_id).catch(() => {}); return; } | |
| sessRef.current = r.session_id; | |
| const expiresAt = r.expires_at ? new Date(r.expires_at).getTime() : null; | |
| es = new EventSource(api.eventsURL("/v1/sandbox/sessions/" + r.session_id + "/events")); | |
| es.onmessage = (ev) => { | |
| let d; try { d = JSON.parse(ev.data); } catch (e) { return; } | |
| setEvents((e) => [...e, d]); | |
| if (d.step === "ready") { setPhase("ready"); loadTools(r.session_id); } | |
| else if (d.status === "expired") setPhase("expired"); | |
| else if (d.status === "error") setPhase("error"); | |
| }; | |
| es.onerror = () => { /* stream closes when the job ends; status already captured */ }; | |
| startCountdown(expiresAt); | |
| } catch (e) { | |
| setEvents((ev) => [...ev, { step: "error", status: "error", message: e.message }]); | |
| simulate(); | |
| } | |
| })(); | |
| return () => { | |
| cancelled = true; | |
| if (es) es.close(); | |
| if (timer) clearInterval(timer); | |
| if (sessRef.current) api.del("/v1/sandbox/sessions/" + sessRef.current).catch(() => {}); | |
| }; | |
| }, [item.id]); | |
| React.useEffect(() => { if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight; }, [events]); | |
| const mm = String(Math.floor(secs / 60)).padStart(2, "0"); | |
| const ss = String(secs % 60).padStart(2, "0"); | |
| async function runTool() { | |
| const tool = tools[activeTool]; if (!tool) return; | |
| setRunning(true); setToolOut(null); | |
| const args = sampleArgs(tool); | |
| try { | |
| if (live && sessRef.current) { | |
| const r = await api.post("/v1/sandbox/sessions/" + sessRef.current + "/tools/call", { name: tool.name, arguments: args }); | |
| setToolOut(r.result !== undefined ? r.result : r); | |
| } else { | |
| await sleep(700); | |
| setToolOut(tool.name === "list_directory" ? { entries: ["notes.txt", "out.txt", "project/", "readme.md"], count: 4 } : { ok: true, tool: tool.name }); | |
| } | |
| } catch (e) { setToolOut({ error: e.message }); } | |
| finally { setRunning(false); } | |
| } | |
| const toolCount = tools.length; | |
| return ( | |
| <div className="wrap rise"> | |
| <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 14, flexWrap: "wrap", marginBottom: 18 }}> | |
| <div style={{ display: "flex", alignItems: "center", gap: 12 }}> | |
| <button className="chip" onClick={back} style={{ cursor: "pointer" }}><GI d={G.chev} size={13} style={{ transform: "rotate(90deg)" }} /> Back</button> | |
| <div> | |
| <h1 style={{ margin: 0, fontSize: 20, fontWeight: 700 }}>{item.name} · Sandbox</h1> | |
| <p style={{ margin: "3px 0 0", fontSize: 12, color: "var(--ink-3)", fontFamily: "var(--mono)" }}>session {sessRef.current || "sbx_…"} · {live ? "live runtime" : "simulated"}</p> | |
| </div> | |
| </div> | |
| <div style={{ display: "flex", alignItems: "center", gap: 10 }}> | |
| {live && <LiveTag />} | |
| <span className={"chip " + (phase === "ready" ? "green" : phase === "error" ? "red" : phase === "expired" ? "amber" : "amber")}> | |
| <span className={"dot " + (phase === "ready" ? "green pulse" : phase === "error" ? "red" : "amber")} /> {phase === "ready" ? "Running" : phase === "error" ? "Error" : phase === "expired" ? "Expired" : "Starting"} | |
| </span> | |
| <span className="chip mono" style={{ fontSize: 13 }}><GI d={G.clock} size={13} /> {mm}:{ss}</span> | |
| </div> | |
| </div> | |
| <div style={{ display: "grid", gridTemplateColumns: "260px 1fr 300px", gap: 16 }} className="sbx-grid"> | |
| <div className="card" style={{ alignSelf: "start" }}> | |
| <div className="card-h"><span className="t">Lifecycle</span></div> | |
| <div style={{ padding: "8px 16px 14px" }}> | |
| {events.map((e, i) => ( | |
| <div key={i} className="rise" style={{ display: "flex", gap: 10, padding: "9px 0", borderBottom: "1px solid var(--line)" }}> | |
| <span style={{ color: e.status === "error" ? "var(--red)" : "var(--acc)", display: "inline-flex", marginTop: 1 }}> | |
| <GI d={e.status === "error" ? G.x : G.check} size={15} sw={2.4} /> | |
| </span> | |
| <div><div className="mono" style={{ fontSize: 12.5, color: "var(--ink)" }}>{e.step}</div> | |
| <div style={{ fontSize: 11.5, color: "var(--ink-3)", marginTop: 1 }}>{e.message}</div></div> | |
| </div> | |
| ))} | |
| {phase === "starting" && <div style={{ padding: "9px 0", color: "var(--ink-4)", fontSize: 12 }}><span className="cur" /></div>} | |
| </div> | |
| </div> | |
| <div className="card" style={{ alignSelf: "start" }}> | |
| <div className="card-h"><span className="t">Tool explorer</span><span className="chip">{phase === "ready" ? toolCount + " tools" : "—"}</span></div> | |
| {phase !== "ready" ? ( | |
| <div style={{ padding: 40, textAlign: "center", color: "var(--ink-3)", fontSize: 13 }}>Waiting for <span className="mono">tools/list</span>…</div> | |
| ) : ( | |
| <div style={{ padding: 16 }}> | |
| <div style={{ display: "flex", gap: 7, flexWrap: "wrap", marginBottom: 14 }}> | |
| {tools.map((t, i) => ( | |
| <button key={t.name} onClick={() => { setActiveTool(i); setToolOut(null); }} className={"chip" + (activeTool === i ? " green" : "")} style={{ cursor: "pointer", height: 28 }}>{t.name}</button> | |
| ))} | |
| </div> | |
| {tools[activeTool] && <> | |
| <p style={{ margin: "0 0 10px", fontSize: 12.5, color: "var(--ink-3)" }}>{tools[activeTool].description}</p> | |
| <div style={{ fontSize: 11, color: "var(--ink-4)", marginBottom: 6, textTransform: "uppercase", letterSpacing: "0.06em" }}>Input</div> | |
| <pre className="mono" style={{ margin: 0, padding: 12, background: "var(--inset)", border: "1px solid var(--line)", borderRadius: "var(--r-sm)", fontSize: 12, color: "var(--ink-2)", overflowX: "auto" }}>{JSON.stringify(sampleArgs(tools[activeTool]), null, 2)}</pre> | |
| <button className="btn btn-primary btn-sm" style={{ marginTop: 12 }} onClick={runTool} disabled={running}> | |
| {running ? "Running…" : <><GI d={G.play} size={13} /> Run Tool</>} | |
| </button> | |
| </>} | |
| {toolOut && ( | |
| <div className="rise" style={{ marginTop: 12 }}> | |
| <div style={{ fontSize: 11, color: "var(--ink-4)", marginBottom: 6, textTransform: "uppercase", letterSpacing: "0.06em" }}>Output</div> | |
| <pre className="mono" style={{ margin: 0, padding: 12, background: "var(--inset)", border: "1px solid var(--acc-line)", borderRadius: "var(--r-sm)", fontSize: 12, color: "var(--acc-2)", overflowX: "auto", maxHeight: 240 }}>{JSON.stringify(toolOut, null, 2)}</pre> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| <div style={{ display: "grid", gap: 16, alignContent: "start" }}> | |
| <div className="card"> | |
| <div className="card-h"><span className="t">Logs</span>{live && <span className="chip"><span className="dot green pulse" /> SSE</span>}</div> | |
| <div className="logwin" ref={logRef} style={{ height: 180, borderRadius: 0, border: "none" }}> | |
| {events.map((e, i) => (<div key={i} style={{ color: e.status === "error" ? "var(--red)" : "var(--ink-3)" }}><span style={{ color: "var(--ink-4)" }}>[{e.step}]</span> {e.message}</div>))} | |
| {phase === "ready" && <div style={{ color: "var(--acc-2)" }}>stdout: server listening on stdio</div>} | |
| <span className="cur" /> | |
| </div> | |
| </div> | |
| <div className="card card-pad" style={{ borderColor: phase === "ready" ? "var(--acc-line)" : "var(--line)" }}> | |
| <div style={{ display: "flex", alignItems: "center", gap: 8 }}> | |
| <span style={{ fontWeight: 700, fontSize: 14 }}>Verdict</span> | |
| <span className={"chip " + (phase === "ready" ? "green" : phase === "error" ? "red" : "amber")}>{phase === "ready" ? "Passed" : phase === "error" ? "Failed" : "Pending"}</span> | |
| </div> | |
| {phase === "ready" ? ( | |
| <> | |
| <p style={{ margin: "11px 0 0", fontSize: 12.5, lineHeight: 1.6, color: "var(--ink-2)" }}> | |
| The MCP server initialized and exposed {toolCount} tools. No secrets were required. No blocked commands detected. | |
| </p> | |
| <div style={{ marginTop: 12, fontSize: 11, color: "var(--ink-4)", textTransform: "uppercase", letterSpacing: "0.06em" }}>Recommended next step</div> | |
| <pre className="mono" style={{ margin: "6px 0 0", padding: 11, background: "var(--inset)", border: "1px solid var(--line)", borderRadius: "var(--r-sm)", fontSize: 11.5, color: "var(--acc-2)", overflowX: "auto" }}>matrix install {item.id.split(":")[1]} --alias {item.id.split(":")[1]}</pre> | |
| </> | |
| ) : <p style={{ margin: "11px 0 0", fontSize: 12.5, color: "var(--ink-3)" }}>{phase === "error" ? "Sandbox failed — see lifecycle." : "Running safety checks…"}</p>} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| /* --------------------------------------------------------------- | |
| Runtimes (live self runtime + demo fleet). | |
| --------------------------------------------------------------- */ | |
| function WorkspaceRuntimes() { | |
| const { runtimes, refresh } = useCloudRuntimes(5000); | |
| if (runtimes === null) return null; | |
| return ( | |
| <div className="card" style={{ overflow: "hidden", marginBottom: 16 }}> | |
| <div className="card-h"> | |
| <span className="t">Workspace runtimes <span style={{ color: "var(--ink-4)", fontWeight: 500 }}>· joined sandboxes & self-hosted</span></span> | |
| <button className="chip" style={{ cursor: "pointer" }} onClick={refresh}><GI d={G.refresh} size={11} /> refresh</button> | |
| </div> | |
| <table className="tbl"> | |
| <thead><tr><th>Runtime</th><th>Status</th><th className="hide-sm">Kind</th><th className="hide-sm">Capabilities</th><th className="hide-sm">Heartbeat</th></tr></thead> | |
| <tbody> | |
| {runtimes.length === 0 && <tr><td colSpan={5} style={{ color: "var(--ink-4)", textAlign: "center", padding: 22 }}>No runtimes have joined yet. Use <b style={{ color: "var(--ink-2)" }}>Add a runtime</b> to mint a join token or duplicate the HF Space.</td></tr>} | |
| {runtimes.map((r) => { | |
| const sc = RT_STATUS_CLASS[r.status] || "violet"; | |
| return ( | |
| <tr className="row" key={r.id}> | |
| <td> | |
| <div className="nm mono" style={{ display: "flex", alignItems: "center", gap: 8 }}>{r.name || r.id}</div> | |
| {r.hf_space ? <div style={{ fontSize: 11, color: "var(--ink-4)" }} className="hide-sm">🤗 {r.hf_space}</div> : (r.url ? <div style={{ fontSize: 11, color: "var(--ink-4)" }} className="hide-sm">{r.url}</div> : null)} | |
| </td> | |
| <td><span className={"chip " + sc}><span className={"dot " + sc + (sc === "green" ? " pulse" : "")} /> {r.status}</span></td> | |
| <td className="hide-sm"><span className="chip">{r.kind || "self-hosted"}</span></td> | |
| <td className="hide-sm" style={{ fontSize: 11.5, color: "var(--ink-3)" }}>{(r.caps || []).join(" · ") || "—"}</td> | |
| <td className="hide-sm mono" style={{ fontSize: 12, color: "var(--ink-3)" }}>{timeAgo(r.last_seen_at)}</td> | |
| </tr> | |
| ); | |
| })} | |
| </tbody> | |
| </table> | |
| </div> | |
| ); | |
| } | |
| function RuntimesView({ go }) { | |
| const { runtimes, refresh } = useRuntimes(5000); | |
| const rows = runtimes || []; | |
| return ( | |
| <div className="wrap rise"> | |
| <div className="phead"> | |
| <div><p className="eyebrow">Execution plane {runtimes && <LiveTag />}</p><h1>Runtimes</h1><p>Matrix Runtime installations connected to this control surface. Hybrid runtimes connect outbound — no inbound ports.</p></div> | |
| <div style={{ display: "flex", gap: 9 }}> | |
| <button className="btn btn-ghost" onClick={refresh}><GI d={G.refresh} size={15} /> Refresh</button> | |
| <button className="btn btn-primary" onClick={() => go("install")}><GI d={G.download} size={15} /> Add a runtime</button> | |
| </div> | |
| </div> | |
| <WorkspaceRuntimes /> | |
| <div className="card" style={{ overflow: "hidden" }}> | |
| <div className="card-h"><span className="t">This control node</span></div> | |
| <table className="tbl"> | |
| <thead><tr><th>Runtime</th><th>Status</th><th className="hide-sm">Mode</th><th className="hide-sm">Region</th><th>Jobs</th><th className="hide-sm">Heartbeat</th><th></th></tr></thead> | |
| <tbody> | |
| {rows.length === 0 && <tr><td colSpan={7} style={{ color: "var(--ink-4)", textAlign: "center", padding: 24 }}>{runtimes ? "No runtimes connected." : "…"}</td></tr>} | |
| {rows.map((r) => ( | |
| <tr className="row" key={r.id}> | |
| <td><div className="nm mono" style={{ display: "flex", alignItems: "center", gap: 8 }}>{r.name} {r.live && <LiveTag />}</div><div style={{ fontSize: 11, color: "var(--ink-4)" }} className="hide-sm">{r.caps.join(" · ")}</div></td> | |
| <td><span className={"chip " + r.statusClass}><span className={"dot " + r.statusClass + (r.statusClass === "green" ? " pulse" : "")} /> {r.status}</span></td> | |
| <td className="hide-sm"><span className="chip">{r.mode}</span></td> | |
| <td className="hide-sm mono" style={{ fontSize: 12 }}>{r.region}</td> | |
| <td className="mono">{r.jobs}</td> | |
| <td className="hide-sm mono" style={{ fontSize: 12, color: "var(--ink-3)" }}>{r.heartbeat}</td> | |
| <td><button className="btn btn-ghost btn-sm" onClick={() => go("agents")}>View</button></td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // HF_DUPLICATE_URL is the one-click "duplicate this Space" target. The owner's | |
| // published Space is at agent-matrix/matrixcloud; override via window global. | |
| const HF_DUPLICATE_URL = (window.MATRIX_HF_SPACE_URL || "https://huggingface.co/spaces/agent-matrix/matrixcloud") + "?duplicate=true"; | |
| const CLOUD_URL = window.MATRIX_CLOUD_URL || "https://api.matrixhub.io"; | |
| function InstallRuntimeView() { | |
| const [tab, setTab] = React.useState("Hugging Face Space"); | |
| const [token, setToken] = React.useState(""); // real minted secret (shown once) | |
| const [minting, setMinting] = React.useState(false); | |
| const [mintErr, setMintErr] = React.useState(""); | |
| const [copied, setCopied] = React.useState(false); | |
| const tabs = ["Hugging Face Space", "Docker", "Kubernetes", "Helm", "Local Dev", "On-Prem"]; | |
| const tk = token || "mxrt_xxxxx_mint_a_token"; | |
| async function mint() { | |
| setMintErr(""); setMinting(true); | |
| try { const r = await cloud.mintJoinToken("console", 1, 60); setToken(r.secret); } | |
| catch (e) { setMintErr(e.message || "Could not mint a join token."); } | |
| finally { setMinting(false); } | |
| } | |
| function copyToken() { try { navigator.clipboard.writeText(token); } catch (e) {} setCopied(true); setTimeout(() => setCopied(false), 1300); } | |
| const cmds = { | |
| "Helm": `helm install matrix-runtime ./deploy/helm/matrix-runtime \\\n --namespace matrix-runtime --create-namespace \\\n --set cloud.url=${CLOUD_URL} \\\n --set runtime.joinToken=${tk}`, | |
| "Docker": `docker run -d --name matrix-runtime \\\n -e MATRIX_CLOUD_URL=${CLOUD_URL} \\\n -e MATRIX_RUNTIME_JOIN_TOKEN=${tk} \\\n -v matrix-runtime-data:/var/lib/matrix-runtime \\\n ghcr.io/agent-matrix/matrix-runtime:latest`, | |
| "Kubernetes": `kubectl apply -f deploy/k8s/namespace.yaml\nkubectl -n matrix-runtime create secret generic join-token \\\n --from-literal=token=${tk}`, | |
| "Hugging Face Space": `# 1) Duplicate the Space (button on the right), then\n# 2) set these as Space secrets:\nMATRIX_RUNTIME_MODE=hf-space\nMATRIX_CLOUD_URL=${CLOUD_URL}\nMATRIX_RUNTIME_JOIN_TOKEN=${tk}\n# 3) (optional) bring your own HF inference:\nHF_TOKEN=hf_your_token`, | |
| "Local Dev": `make build\n./bin/matrix-runtime --mode local-dev\n# join the control plane:\nmatrix-runtime join --cloud-url ${CLOUD_URL} --token ${tk}`, | |
| "On-Prem": `sudo make install INSTALL_SYSTEMD=1\nsudo systemctl enable --now matrix-runtime\n# join-token: ${tk}`, | |
| }; | |
| return ( | |
| <div className="wrap rise"> | |
| <div className="phead"> | |
| <div><p className="eyebrow">Onboarding</p><h1>Add a runtime</h1><p>MatrixCloud manages the control plane. A runtime executes jobs inside your environment — duplicate the Hugging Face Space for a managed sandbox, or self-host. Runtimes connect outbound only.</p></div> | |
| <span className="chip violet">Hybrid · recommended</span> | |
| </div> | |
| <div className="card card-pad" style={{ display: "flex", gap: 16, alignItems: "center", marginBottom: 18, borderColor: "rgba(157,123,255,0.3)" }}> | |
| {[["Control plane", "MatrixHub Cloud", "SaaS"], ["→", "", ""], ["Execution plane", "Your infrastructure", "Runtime"]].map(([a, b, c], i) => ( | |
| a === "→" | |
| ? <GI key={i} d={G.chevr} size={20} style={{ color: "var(--ink-4)", flexShrink: 0 }} /> | |
| : <div key={i} style={{ flex: 1, padding: "12px 14px", borderRadius: "var(--r-md)", border: "1px solid var(--line-2)", background: "var(--inset)" }}> | |
| <div style={{ fontSize: 11, color: "var(--ink-4)", textTransform: "uppercase", letterSpacing: "0.06em" }}>{a}</div> | |
| <div style={{ fontWeight: 700, fontSize: 14, marginTop: 4 }}>{b}</div> | |
| <span className="chip" style={{ marginTop: 8 }}>{c}</span> | |
| </div> | |
| ))} | |
| </div> | |
| <div style={{ display: "grid", gridTemplateColumns: "1fr 300px", gap: 16 }} className="ov-grid"> | |
| <div> | |
| <div style={{ display: "flex", gap: 7, flexWrap: "wrap", marginBottom: 14 }}> | |
| {tabs.map((t) => <button key={t} onClick={() => setTab(t)} className={"chip" + (tab === t ? " green" : "")} style={{ height: 30, cursor: "pointer", fontFamily: "var(--font)", fontSize: 12.5 }}>{t}</button>)} | |
| </div> | |
| <CmdBlock label={tab + " install"}>{cmds[tab]}</CmdBlock> | |
| <div className="card card-pad" style={{ marginTop: 14 }}> | |
| <span style={{ fontWeight: 700, fontSize: 13.5 }}>Expected result</span> | |
| <p style={{ margin: "8px 0 0", fontSize: 12.5, color: "var(--ink-3)", lineHeight: 1.6 }}>After installation, your runtime appears under <b style={{ color: "var(--ink-2)" }}>Runtimes</b> with status <span className="chip green" style={{ height: 20 }}><span className="dot green" /> Online</span> and these capabilities:</p> | |
| <div style={{ display: "flex", gap: 7, flexWrap: "wrap", marginTop: 11 }}> | |
| {["mcp.test", "mcp.run", "agent.run", "model.pull", "model.preload"].map((c) => <span key={c} className="chip green">{c}</span>)} | |
| </div> | |
| </div> | |
| </div> | |
| <div style={{ display: "grid", gap: 16, alignContent: "start" }}> | |
| {tab === "Hugging Face Space" && ( | |
| <a className="btn btn-primary" href={HF_DUPLICATE_URL} target="_blank" rel="noopener noreferrer" style={{ height: 46, justifyContent: "center", textDecoration: "none" }}> | |
| <GI d={G.download} size={16} /> Duplicate the Space | |
| </a> | |
| )} | |
| <div className="card card-pad"> | |
| <div style={{ display: "flex", alignItems: "center", gap: 8 }}><GI d={G.key} size={16} style={{ color: "var(--acc)" }} /><span style={{ fontWeight: 700, fontSize: 13.5 }}>Runtime join token</span></div> | |
| {token ? ( | |
| <> | |
| <div className="mono" style={{ marginTop: 12, padding: "11px 13px", borderRadius: "var(--r-sm)", background: "var(--inset)", border: "1px solid var(--line-2)", fontSize: 13, color: "var(--ink)", letterSpacing: "0.04em", wordBreak: "break-all" }}>{token}</div> | |
| <div style={{ display: "flex", gap: 8, marginTop: 12 }}> | |
| <button className="btn btn-ghost btn-sm" onClick={copyToken} style={{ flex: 1 }}>{copied ? <><GI d={G.check} size={14} /> Copied</> : <><GI d={G.logs} size={14} /> Copy</>}</button> | |
| <button className="btn btn-ghost btn-sm" onClick={mint} title="Mint another"><GI d={G.refresh} size={14} /></button> | |
| </div> | |
| <p style={{ margin: "11px 0 0", fontSize: 11.5, color: "var(--acc-2)", lineHeight: 1.5 }}>Shown once — copy it now. Single-use, expires in 60 minutes.</p> | |
| </> | |
| ) : ( | |
| <> | |
| <p style={{ margin: "11px 0 0", fontSize: 12, color: "var(--ink-3)", lineHeight: 1.5 }}>Mint a single-use token scoped to your workspace, then paste it into the runtime's secrets.</p> | |
| <button className="btn btn-primary btn-sm" onClick={mint} disabled={minting} style={{ marginTop: 12, width: "100%" }}>{minting ? "Minting…" : <><GI d={G.key} size={14} /> Mint join token</>}</button> | |
| {mintErr && <p style={{ margin: "10px 0 0", fontSize: 11.5, color: "var(--red)" }}>{mintErr}</p>} | |
| </> | |
| )} | |
| </div> | |
| <div className="card card-pad"> | |
| <span style={{ fontWeight: 700, fontSize: 13.5 }}>Security posture</span> | |
| <div style={{ display: "grid", gap: 9, marginTop: 12 }}> | |
| {["Outbound-only tunnel", "Secrets stay on-prem", "Signed runtime image", "TLS + audited control channel"].map((s) => ( | |
| <div key={s} style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 12.5, color: "var(--ink-2)" }}> | |
| <span style={{ color: "var(--acc)", display: "inline-flex" }}><GI d={G.check} size={14} sw={2.4} /></span> {s} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| /* --------------------------------------------------------------- | |
| Models — generic multi-source importer. | |
| Lifecycle: Profile only → Queued → Downloading → Installed → | |
| Attached → Ready. Import (resolve a profile) is separate from | |
| Attach (install onto a runtime) and Runtime Cache (physical state). | |
| --------------------------------------------------------------- */ | |
| const HF_FALLBACK = [ | |
| { id: "deepseek-ai/DeepSeek-V3", pipeline_tag: "text-generation", downloads: 1520000, likes: 8100, tags: ["transformers", "moe"], library_name: "transformers" }, | |
| { id: "deepseek-ai/DeepSeek-R1", pipeline_tag: "text-generation", downloads: 2100000, likes: 11200, tags: ["transformers", "reasoning"], library_name: "transformers" }, | |
| { id: "Qwen/Qwen2.5-7B-Instruct", pipeline_tag: "text-generation", downloads: 2300000, likes: 12000, tags: ["transformers"], library_name: "transformers" }, | |
| { id: "meta-llama/Llama-3.1-8B-Instruct", pipeline_tag: "text-generation", downloads: 3100000, likes: 15000, tags: ["transformers", "gated"], library_name: "transformers" }, | |
| { id: "mistralai/Mistral-7B-Instruct-v0.3", pipeline_tag: "text-generation", downloads: 1900000, likes: 7600, tags: ["transformers"], library_name: "transformers" }, | |
| { id: "BAAI/bge-large-en-v1.5", pipeline_tag: "feature-extraction", downloads: 4200000, likes: 2200, tags: ["sentence-transformers"], library_name: "sentence-transformers" }, | |
| ]; | |
| function fmtNum(n) { if (n == null) return "—"; if (n >= 1e6) return (n / 1e6).toFixed(1) + "M"; if (n >= 1e3) return (n / 1e3).toFixed(0) + "K"; return String(n); } | |
| function gpuLikely(m) { return /(\b|-|\/)(7b|8b|13b|70b|v3|v4|r1|large|moe)/i.test((m && m.id) || "") || ((m && m.tags) || []).includes("moe"); } | |
| function recRuntime(m) { return (m.library_name === "sentence-transformers" || m.pipeline_tag === "feature-extraction") ? "Matrix Runtime" : (gpuLikely(m) ? "vLLM / SGLang" : "Ollama / vLLM"); } | |
| // Search HF: same-origin backend proxy first (real, no CORS), then direct HF, | |
| // then offline sample data — so the demo never breaks. | |
| async function hfSearch(q, task) { | |
| try { | |
| const p = new URLSearchParams({ q: q || "deepseek", limit: "16" }); | |
| if (task && task !== "any") p.set("task", task); | |
| const r = await api.get("/v1/model-sources/huggingface/search?" + p.toString()); | |
| if (r && r.live && Array.isArray(r.items) && r.items.length) return { live: true, items: r.items }; | |
| } catch (e) { /* fall through */ } | |
| try { | |
| const params = new URLSearchParams({ search: q || "deepseek", sort: "downloads", direction: "-1", limit: "16" }); | |
| if (task && task !== "any") params.set("pipeline_tag", task); | |
| const r = await fetch("https://huggingface.co/api/models?" + params.toString(), { headers: { Accept: "application/json" } }); | |
| if (r.ok) { | |
| const data = await r.json(); | |
| if (Array.isArray(data) && data.length) return { live: true, items: data.map((m) => ({ id: m.id || m.modelId, pipeline_tag: m.pipeline_tag, downloads: m.downloads, likes: m.likes, tags: m.tags || [], library_name: m.library_name })) }; | |
| } | |
| } catch (e) { /* fall through */ } | |
| const ql = (q || "").toLowerCase(); | |
| return { live: false, items: HF_FALLBACK.filter((m) => !ql || m.id.toLowerCase().includes(ql)) }; | |
| } | |
| const IMPORT_SOURCES = [ | |
| { id: "huggingface", name: "Hugging Face", glyph: "🤗", desc: "Search public & private models", search: true }, | |
| { id: "github", name: "GitHub", mono: "GH", desc: "Import from a repository", repo: true }, | |
| { id: "gitlab", name: "GitLab", mono: "GL", desc: "Import from a repository", repo: true }, | |
| { id: "s3", name: "Amazon S3", mono: "S3", desc: "Model artifacts in a bucket", bucket: true }, | |
| { id: "r2", name: "Cloudflare R2", mono: "R2", desc: "S3-compatible object storage", bucket: true }, | |
| { id: "ollama", name: "Ollama", mono: "OL", desc: "Pull a local Ollama model", ollama: true }, | |
| { id: "url", name: "Custom URL", mono: "URL", desc: "Direct manifest or weights URL", url: true }, | |
| ]; | |
| function Inp({ label, value, onChange, placeholder, mono }) { | |
| return ( | |
| <label style={{ display: "block" }}> | |
| <span style={{ display: "block", fontSize: 10.5, fontFamily: "var(--mono)", letterSpacing: "0.14em", textTransform: "uppercase", color: "var(--ink-4)", marginBottom: 6 }}>{label}</span> | |
| <input value={value} onChange={(e) => onChange(e.target.value)} placeholder={placeholder} spellCheck={false} | |
| style={{ width: "100%", height: 42, padding: "0 12px", borderRadius: "var(--r-sm)", border: "1px solid var(--line-2)", background: "rgba(0,0,0,0.35)", color: "var(--ink)", fontSize: 13, fontFamily: mono ? "var(--mono)" : "var(--font)", outline: "none" }} /> | |
| </label> | |
| ); | |
| } | |
| function ImportModelModal({ onClose, onImport, initialSource }) { | |
| const [source, setSource] = React.useState(null); | |
| const [step, setStep] = React.useState(0); // 0 source/search · 1 preview · 2 attach | |
| const [q, setQ] = React.useState("deepseek"); | |
| const [task, setTask] = React.useState("any"); | |
| const [items, setItems] = React.useState([]); | |
| const [live, setLive] = React.useState(true); | |
| const [loading, setLoading] = React.useState(false); | |
| const [priv, setPriv] = React.useState(false); | |
| const [token, setToken] = React.useState(""); | |
| const [form, setForm] = React.useState({ repo: "", branch: "main", bucket: "", endpoint: "", path: "", model: "", url: "" }); | |
| const [sel, setSel] = React.useState(null); | |
| const setF = (k, v) => setForm((s) => ({ ...s, [k]: v })); | |
| async function doSearch() { setLoading(true); const res = await hfSearch(q, task); setItems(res.items); setLive(res.live); setLoading(false); } | |
| function pickSource(s) { setSource(s); if (s.search) { setItems([]); doSearch(); } } | |
| React.useEffect(() => { | |
| if (initialSource) { const s = IMPORT_SOURCES.find((x) => x.id === initialSource); if (s) pickSource(s); } | |
| }, []); // eslint-disable-line | |
| function resolveForm() { | |
| let id = ""; | |
| if (source.repo) id = (form.repo || "owner/repo").replace(/^https?:\/\/(github|gitlab)\.com\//, ""); | |
| else if (source.bucket) id = source.id + "://" + (form.bucket || "bucket") + "/" + (form.path || "model"); | |
| else if (source.ollama) id = "ollama/" + (form.model || "llama3.1"); | |
| else if (source.url) id = form.url || "https://example.com/model.gguf"; | |
| setSel({ id, pipeline_tag: "text-generation", library_name: source.ollama ? "ollama" : "custom", tags: priv ? ["private"] : [], _source: source.name }); | |
| setStep(1); | |
| } | |
| const tasks = ["any", "text-generation", "feature-extraction", "text-classification", "automatic-speech-recognition"]; | |
| const heads = ["Source", "Preview", "Attach"]; | |
| return ReactDOM.createPortal( | |
| <div className="scrim" style={{ position: "fixed", inset: 0, zIndex: 70, display: "flex", alignItems: "center", justifyContent: "center", padding: 24, background: "rgba(5,8,12,0.72)", backdropFilter: "blur(7px)" }} | |
| onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}> | |
| <div className="rise card" style={{ width: "100%", maxWidth: 760, maxHeight: "88vh", display: "flex", flexDirection: "column", background: "var(--panel-2)" }}> | |
| <div className="card-h"> | |
| <div style={{ display: "flex", alignItems: "center", gap: 10 }}> | |
| <span style={{ color: "var(--acc)", display: "inline-flex" }}><GI d={G.download} size={17} /></span> | |
| <span className="t" style={{ letterSpacing: "0.04em" }}>Import Model{source ? " · " + source.name : ""}</span> | |
| </div> | |
| <button className="iconbtn" onClick={onClose}><GI d={G.x} size={16} /></button> | |
| </div> | |
| {source && ( | |
| <div style={{ display: "flex", gap: 8, padding: "12px 20px 0" }}> | |
| {heads.map((s, i) => ( | |
| <div key={s} style={{ display: "flex", alignItems: "center", gap: 7, fontSize: 11.5, color: i === step ? "var(--acc-2)" : i < step ? "var(--ink-2)" : "var(--ink-4)" }}> | |
| <span style={{ width: 18, height: 18, borderRadius: 99, display: "grid", placeItems: "center", fontSize: 10, fontFamily: "var(--mono)", border: "1px solid " + (i <= step ? "var(--acc)" : "var(--line-3)"), background: i < step ? "var(--acc)" : "transparent", color: i < step ? "#03140c" : "inherit" }}>{i < step ? "✓" : i + 1}</span> | |
| {s}{i < 2 && <span style={{ color: "var(--ink-4)" }}>›</span>} | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| <div style={{ padding: 20, overflowY: "auto", flex: 1 }}> | |
| {!source && ( | |
| <div> | |
| <p style={{ margin: "0 0 14px", fontSize: 13, color: "var(--ink-3)" }}>Choose where to import the model from. Private sources accept a token.</p> | |
| <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }} className="ov-grid"> | |
| {IMPORT_SOURCES.map((s) => ( | |
| <button key={s.id} onClick={() => pickSource(s)} style={{ display: "flex", alignItems: "center", gap: 13, padding: "13px 14px", borderRadius: "var(--r-md)", textAlign: "left", border: "1px solid var(--line-2)", background: "rgba(0,0,0,0.3)" }}> | |
| <span style={{ width: 38, height: 38, borderRadius: 10, flexShrink: 0, display: "grid", placeItems: "center", border: "1px solid var(--line-2)", background: "var(--raised)", color: "var(--ink)", fontFamily: "var(--mono)", fontWeight: 700, fontSize: 12 }}>{s.glyph || s.mono}</span> | |
| <span style={{ minWidth: 0 }}><span style={{ display: "block", fontSize: 13.5, fontWeight: 600 }}>{s.name}</span><span style={{ display: "block", fontSize: 11.5, color: "var(--ink-3)", marginTop: 1 }}>{s.desc}</span></span> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {source && step === 0 && source.search && ( | |
| <div> | |
| <form onSubmit={(e) => { e.preventDefault(); doSearch(); }} style={{ display: "flex", gap: 9, flexWrap: "wrap" }}> | |
| <div className="topsearch" style={{ flex: "1 1 260px", height: 42, width: "auto", maxWidth: "none", display: "flex" }}> | |
| <GI d={G.search} size={16} /><input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search models… e.g. deepseek" autoFocus /> | |
| </div> | |
| <select value={task} onChange={(e) => setTask(e.target.value)} style={{ height: 42, padding: "0 12px", borderRadius: "var(--r-sm)", border: "1px solid var(--line-2)", background: "rgba(0,0,0,0.35)", color: "var(--ink)", fontSize: 13 }}> | |
| {tasks.map((t) => <option key={t} value={t}>{t === "any" ? "Any task" : t}</option>)} | |
| </select> | |
| <button className="btn btn-primary" type="submit">Search</button> | |
| </form> | |
| <label style={{ display: "flex", alignItems: "center", gap: 8, margin: "12px 0 0", fontSize: 12.5, color: "var(--ink-2)", cursor: "pointer" }}> | |
| <input type="checkbox" checked={priv} onChange={(e) => setPriv(e.target.checked)} style={{ accentColor: "var(--acc)" }} /> Private repository (use access token) | |
| </label> | |
| {priv && <div style={{ marginTop: 10 }}><Inp label="HF access token" value={token} onChange={setToken} placeholder="hf_•••••••••••" mono /></div>} | |
| <div style={{ display: "flex", alignItems: "center", gap: 8, margin: "12px 0 4px" }}> | |
| <span className={"chip " + (live ? "green" : "amber")} style={{ height: 20 }}><span className={"dot " + (live ? "green" : "amber")} /> {live ? "live · huggingface.co/api" : "offline · sample results"}</span> | |
| {loading && <span style={{ fontSize: 11.5, color: "var(--ink-4)" }}>searching…</span>} | |
| </div> | |
| <div style={{ display: "grid", gap: 9, marginTop: 8 }}> | |
| {loading ? <div style={{ padding: 28, textAlign: "center", color: "var(--ink-3)", fontSize: 13 }}>Querying Hugging Face…</div> | |
| : items.map((m) => ( | |
| <div key={m.id} style={{ display: "flex", alignItems: "center", gap: 13, padding: "11px 14px", borderRadius: "var(--r-md)", border: "1px solid var(--line-2)", background: "rgba(0,0,0,0.3)" }}> | |
| <div style={{ minWidth: 0, flex: 1 }}> | |
| <div className="mono" style={{ fontSize: 13, fontWeight: 600, color: "var(--ink)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{m.id}</div> | |
| <div style={{ display: "flex", gap: 12, marginTop: 5, fontSize: 11.5, color: "var(--ink-3)", flexWrap: "wrap" }}> | |
| <span>{m.pipeline_tag || "—"}</span><span>↓ {fmtNum(m.downloads)}</span><span>♥ {fmtNum(m.likes)}</span> | |
| {gpuLikely(m) && <span className="chip amber" style={{ height: 18 }}>GPU</span>} | |
| {((m.tags || []).includes("gated") || m.gated) && <span className="chip amber" style={{ height: 18 }}>Gated</span>} | |
| </div> | |
| </div> | |
| <button className="btn btn-ghost btn-sm" onClick={() => { setSel({ ...m, _source: "Hugging Face" }); setStep(1); }}>Import Profile</button> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {source && step === 0 && !source.search && ( | |
| <div style={{ display: "grid", gap: 14 }}> | |
| <p style={{ margin: 0, fontSize: 13, color: "var(--ink-3)" }}>Provide the {source.name} location to resolve a model profile.</p> | |
| {source.repo && <><Inp label="Repository" value={form.repo} onChange={(v) => setF("repo", v)} placeholder={source.id + ".com/owner/model-repo"} mono /><Inp label="Branch / tag" value={form.branch} onChange={(v) => setF("branch", v)} placeholder="main" mono /></>} | |
| {source.bucket && <><Inp label="Bucket" value={form.bucket} onChange={(v) => setF("bucket", v)} placeholder="my-models" mono /><Inp label="Endpoint" value={form.endpoint} onChange={(v) => setF("endpoint", v)} placeholder={source.id === "r2" ? "https://<acct>.r2.cloudflarestorage.com" : "s3.us-east-1.amazonaws.com"} mono /><Inp label="Object path" value={form.path} onChange={(v) => setF("path", v)} placeholder="models/deepseek-v4/" mono /></>} | |
| {source.ollama && <Inp label="Ollama model" value={form.model} onChange={(v) => setF("model", v)} placeholder="llama3.1 / qwen2.5 / mistral" mono />} | |
| {source.url && <Inp label="Manifest / weights URL" value={form.url} onChange={(v) => setF("url", v)} placeholder="https://…/model.gguf" mono />} | |
| <label style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 12.5, color: "var(--ink-2)", cursor: "pointer" }}> | |
| <input type="checkbox" checked={priv} onChange={(e) => setPriv(e.target.checked)} style={{ accentColor: "var(--acc)" }} /> Private — requires credentials | |
| </label> | |
| {priv && <Inp label={source.bucket ? "Access key / secret" : "Access token"} value={token} onChange={setToken} placeholder={source.bucket ? "AKIA… / ••••" : "token ••••"} mono />} | |
| <div style={{ display: "flex", justifyContent: "space-between" }}> | |
| <button className="btn btn-ghost" onClick={() => setSource(null)}>Back</button> | |
| <button className="btn btn-primary" onClick={resolveForm}>Resolve profile</button> | |
| </div> | |
| </div> | |
| )} | |
| {source && step === 1 && sel && ( | |
| <div> | |
| <div className="mono" style={{ fontSize: 15, fontWeight: 700, color: "var(--ink)", wordBreak: "break-all" }}>{sel.id}</div> | |
| <div style={{ fontSize: 12, color: "var(--ink-3)", marginTop: 4 }}>Model Profile preview · resolved from {sel._source}{priv ? " · private" : ""}</div> | |
| <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1px", marginTop: 16, borderRadius: "var(--r-sm)", overflow: "hidden", border: "1px solid var(--line)" }}> | |
| {[["Source", sel._source], ["Model ID", sel.id], ["Task", sel.pipeline_tag || "text-generation"], ["Library", sel.library_name || "transformers"], ["Requires GPU", gpuLikely(sel) ? "Likely" : "No"], ["Recommended runtime", recRuntime(sel)], ["Access", priv ? "Private · token" : "Public"], ["License", (sel.tags || []).includes("gated") ? "Gated · review" : "review required"]].map(([k, v]) => ( | |
| <div key={k} style={{ padding: "11px 13px", background: "var(--inset)" }}><div style={{ fontSize: 10.5, color: "var(--ink-4)", textTransform: "uppercase", letterSpacing: "0.05em" }}>{k}</div><div className="mono" style={{ fontSize: 12.5, color: "var(--ink)", marginTop: 4, overflow: "hidden", textOverflow: "ellipsis" }}>{v}</div></div> | |
| ))} | |
| </div> | |
| <div style={{ marginTop: 16, padding: 14, borderRadius: "var(--r-md)", border: "1px solid var(--line)", background: "rgba(0,0,0,0.25)" }}> | |
| <div style={{ fontSize: 12, fontWeight: 700, color: "var(--ink-2)", marginBottom: 10 }}>Security</div> | |
| <div style={{ display: "grid", gap: 8, fontSize: 12.5 }}> | |
| <div style={{ display: "flex", alignItems: "center", gap: 8, color: "var(--acc-2)" }}><GI d={G.check} size={14} /> safetensors preferred</div> | |
| <div style={{ display: "flex", alignItems: "center", gap: 8, color: "var(--ink-2)" }}><GI d={G.shield} size={14} /> remote code disabled by default — requires explicit approval</div> | |
| </div> | |
| </div> | |
| <div style={{ display: "flex", justifyContent: "space-between", marginTop: 18 }}> | |
| <button className="btn btn-ghost" onClick={() => setStep(0)}>Back</button> | |
| <button className="btn btn-primary" onClick={() => setStep(2)}><GI d={G.download} size={15} /> Import Profile</button> | |
| </div> | |
| </div> | |
| )} | |
| {source && step === 2 && sel && <AttachStep model={sel} onImport={onImport} />} | |
| </div> | |
| </div> | |
| </div>, document.body); | |
| } | |
| function AttachStep({ model, onImport }) { | |
| const { runtimes: all, refresh: refetch } = useRuntimes(); | |
| const [refreshing, setRefreshing] = React.useState(false); | |
| const runtimes = (all || []).filter((r) => r.statusClass !== "red"); | |
| const [runtime, setRuntime] = React.useState(""); | |
| React.useEffect(() => { if (!runtime && runtimes[0]) setRuntime(runtimes[0].name); }, [runtimes, runtime]); | |
| const [mode, setMode] = React.useState("pull"); | |
| const [engine, setEngine] = React.useState(gpuLikely(model) ? "vLLM" : "Ollama"); | |
| const engines = ["vLLM", "SGLang", "TGI", "Ollama", "External endpoint"]; | |
| function refresh() { setRefreshing(true); refetch(); setTimeout(() => setRefreshing(false), 700); } | |
| return ( | |
| <div> | |
| <div style={{ fontSize: 13.5, fontWeight: 700 }}>Attach model to runtime</div> | |
| <div className="mono" style={{ fontSize: 12.5, color: "var(--ink-3)", marginTop: 4, wordBreak: "break-all" }}>{model.id}</div> | |
| <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", margin: "16px 0 8px" }}> | |
| <span style={{ fontSize: 11, color: "var(--ink-4)", textTransform: "uppercase", letterSpacing: "0.06em" }}>Choose runtime</span> | |
| <button className="btn btn-ghost btn-sm" onClick={refresh} disabled={refreshing}><GI d={G.refresh} size={13} style={refreshing ? { animation: "spin 0.7s linear infinite" } : null} /> {refreshing ? "Refreshing…" : "Refresh"}</button> | |
| </div> | |
| <div style={{ display: "grid", gap: 8 }}> | |
| {runtimes.map((r) => { | |
| const lvl = gpuLikely(model) ? (r.caps.includes("model.preload") ? "compatible" : r.mode === "local-dev" ? "not recommended" : "limited") : "compatible"; | |
| return ( | |
| <button key={r.id} onClick={() => setRuntime(r.name)} style={{ display: "flex", alignItems: "center", gap: 11, padding: "11px 13px", borderRadius: "var(--r-md)", textAlign: "left", border: "1px solid " + (runtime === r.name ? "var(--acc-line)" : "var(--line-2)"), background: runtime === r.name ? "var(--acc-soft)" : "rgba(0,0,0,0.3)" }}> | |
| <span style={{ flex: 1 }}><span className="mono" style={{ fontSize: 13, color: "var(--ink)" }}>{r.name}</span>{r.live && <LiveTag />}<span style={{ fontSize: 11.5, color: "var(--ink-4)", marginLeft: 8 }}>{r.mode} · {r.region}</span></span> | |
| <span className={"chip " + (lvl === "compatible" ? "green" : "amber")} style={{ height: 20 }}>{lvl}</span> | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| {gpuLikely(model) && runtime && runtimes.find((r) => r.name === runtime && r.mode === "local-dev") && ( | |
| <div style={{ marginTop: 10, padding: "10px 12px", borderRadius: "var(--r-sm)", border: "1px solid var(--amber-soft)", background: "var(--amber-soft)", fontSize: 12, color: "var(--amber)" }}> | |
| This model is likely too large for the selected runtime. Recommended: a GPU runtime, a quantized variant, or an external endpoint. | |
| </div> | |
| )} | |
| <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16, marginTop: 16 }} className="ov-grid"> | |
| <div> | |
| <div style={{ fontSize: 11, color: "var(--ink-4)", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 8 }}>Install mode</div> | |
| <div style={{ display: "grid", gap: 7 }}> | |
| {[["pull", "Pull from source"], ["mount", "Mount existing volume"], ["endpoint", "Use external endpoint"]].map(([v, l]) => ( | |
| <label key={v} style={{ display: "flex", alignItems: "center", gap: 9, fontSize: 12.5, color: "var(--ink-2)", cursor: "pointer", padding: "8px 11px", borderRadius: 8, border: "1px solid " + (mode === v ? "var(--acc-line)" : "var(--line)"), background: mode === v ? "var(--acc-soft)" : "transparent" }}> | |
| <input type="radio" name="imode" checked={mode === v} onChange={() => setMode(v)} style={{ accentColor: "var(--acc)" }} /> {l} | |
| </label> | |
| ))} | |
| </div> | |
| </div> | |
| <div> | |
| <div style={{ fontSize: 11, color: "var(--ink-4)", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 8 }}>Serving engine</div> | |
| <div style={{ display: "flex", gap: 7, flexWrap: "wrap" }}> | |
| {engines.map((e) => <button key={e} onClick={() => setEngine(e)} className={"chip" + (engine === e ? " green" : "")} style={{ height: 30, cursor: "pointer", fontFamily: "var(--font)" }}>{e}</button>)} | |
| </div> | |
| </div> | |
| </div> | |
| <div style={{ display: "flex", justifyContent: "flex-end", gap: 10, marginTop: 20 }}> | |
| <button className="btn btn-ghost" onClick={() => onImport({ model, runtime, engine, mode })}>Import profile only</button> | |
| <button className="btn btn-primary" onClick={() => onImport({ model, runtime, engine, mode, install: true })}><GI d={G.download} size={15} /> Attach & install</button> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // Map a source display name / install mode to the backend's enum values. | |
| const SOURCE_TYPE_OF = { "Hugging Face": "huggingface", "GitHub": "github", "GitLab": "gitlab", "Amazon S3": "s3", "Cloudflare R2": "r2", "Ollama": "ollama", "Custom URL": "url" }; | |
| const INSTALL_MODE_OF = { pull: "pull_from_source", mount: "mount_volume", endpoint: "external_endpoint" }; | |
| const PROFILE_STATUS_CLASS = { ready: "green", attached: "green", installed: "green", downloading: "amber", queued: "blue", profile_only: "", failed: "red", gated: "amber", incompatible: "red" }; | |
| function useModelData(pollMs) { | |
| const [profiles, setProfiles] = React.useState(null); | |
| const [installs, setInstalls] = React.useState(null); | |
| const load = React.useCallback(async () => { | |
| try { const r = await api.get("/v1/model-profiles"); setProfiles(r.profiles || []); } catch (e) { setProfiles(null); } | |
| try { const r = await api.get("/v1/model-installations"); setInstalls(r.installations || []); } catch (e) { setInstalls(null); } | |
| }, []); | |
| React.useEffect(() => { | |
| load(); | |
| if (pollMs) { const t = setInterval(load, pollMs); return () => clearInterval(t); } | |
| }, [load, pollMs]); | |
| return { profiles, installs, refresh: load }; | |
| } | |
| function ModelsView({ onAttach }) { | |
| const [tab, setTab] = React.useState("Model Profiles"); | |
| const [importOpen, setImportOpen] = React.useState(false); | |
| const [importSource, setImportSource] = React.useState(null); | |
| // Poll faster while a download is in flight, else slow. | |
| const [active, setActive] = React.useState(false); | |
| const { profiles, installs, refresh } = useModelData(active ? 1500 : 6000); | |
| const tabs = ["Available Models", "Connected Providers", "Model Profiles", "Runtime Cache"]; | |
| const providers = [["Hugging Face", "HF", "connected", "live search · resolvable", true], ["OpenAI-compatible", "AI", "connected", "gpt-4o · gpt-4o-mini", false], | |
| ["Ollama", "OL", "connected", "local models", false], ["vLLM", "VL", "available", "GPU endpoint", false], ["GitHub", "GH", "available", "repo-hosted models", false], ["Amazon S3", "S3", "available", "bucket artifacts", false]]; | |
| // Available Models = installations that are ready/attached (real). | |
| const readyInstalls = (installs || []).filter((i) => i.status === "ready" || i.status === "attached"); | |
| React.useEffect(() => { | |
| const downloading = (installs || []).some((i) => i.status === "downloading" || i.status === "checking" || i.status === "queued"); | |
| setActive(downloading); | |
| }, [installs]); | |
| async function handleImport(p) { | |
| setImportOpen(false); setImportSource(null); | |
| const m = p.model || {}; | |
| const provider = m._source || "Custom URL"; | |
| const body = { | |
| source_type: SOURCE_TYPE_OF[provider] || "url", | |
| provider, | |
| external_id: m.id, | |
| display_name: m.id, | |
| source_uri: provider === "Hugging Face" ? ("hf:" + m.id) : m.id, | |
| task: m.pipeline_tag || "text-generation", | |
| library: m.library_name || "transformers", | |
| license: (m.tags || []).includes("gated") ? "gated" : "review required", | |
| tags: m.tags || [], | |
| metadata: { downloads: m.downloads, likes: m.likes }, | |
| }; | |
| let profile; | |
| try { const r = await api.post("/v1/model-profiles", body); profile = r.profile; } | |
| catch (e) { return; } | |
| if (p.install && profile) { | |
| setTab("Runtime Cache"); setActive(true); | |
| try { | |
| await api.post("/v1/model-profiles/" + profile.id + "/attach", { | |
| runtimeId: p.runtime, installMode: INSTALL_MODE_OF[p.mode] || "pull_from_source", servingEngine: p.engine, | |
| }); | |
| } catch (e) { /* surfaced via list */ } | |
| refresh(); | |
| } else { | |
| setTab("Model Profiles"); refresh(); | |
| } | |
| } | |
| return ( | |
| <div className="wrap rise"> | |
| <div className="phead"> | |
| <div><p className="eyebrow">Model gateway · matrix-llm</p><h1>Models</h1><p>Connect providers, resolve metadata into profiles, then attach and install models into runtimes — from Hugging Face, GitHub, S3, R2, Ollama, or a custom URL.</p></div> | |
| <div style={{ display: "flex", gap: 9, flexWrap: "wrap" }}> | |
| <button className="btn btn-ghost" onClick={onAttach}><GI d={G.plus} size={15} /> Add Model</button> | |
| <button className="btn btn-primary" onClick={() => { setImportSource(null); setImportOpen(true); }}><GI d={G.download} size={15} /> Import Model</button> | |
| </div> | |
| </div> | |
| <div style={{ display: "flex", gap: 7, flexWrap: "wrap", marginBottom: 18 }}> | |
| {tabs.map((t) => <button key={t} onClick={() => setTab(t)} className={"chip" + (tab === t ? " green" : "")} style={{ height: 30, cursor: "pointer", fontFamily: "var(--font)", fontSize: 12.5 }}>{t}</button>)} | |
| </div> | |
| {tab === "Available Models" && ( | |
| <div className="card" style={{ overflow: "hidden" }}> | |
| <div className="card-h"><span className="t">Ready to use {installs && <LiveTag />}</span></div> | |
| <table className="tbl"> | |
| <thead><tr><th>Model</th><th className="hide-sm">Provider</th><th>Runtime</th><th className="hide-sm">Engine</th><th></th></tr></thead> | |
| <tbody> | |
| {readyInstalls.length === 0 && <tr><td colSpan={5} style={{ color: "var(--ink-4)", textAlign: "center", padding: 24 }}>No models ready yet — import one and attach it to a runtime.</td></tr>} | |
| {readyInstalls.map((m) => ( | |
| <tr className="row" key={m.id}> | |
| <td className="nm mono" style={{ fontSize: 12.5 }}>{m.model_name}</td> | |
| <td className="hide-sm">{m.provider}</td><td><span className="chip">{m.runtime_id}</span></td><td className="hide-sm">{m.serving_engine || "—"}</td> | |
| <td style={{ textAlign: "right" }}><button className="btn btn-ghost btn-sm">Use</button></td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| )} | |
| {tab === "Connected Providers" && ( | |
| <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }} className="ov-grid"> | |
| {providers.map(([nm, tag, st, ds, isHf]) => ( | |
| <div className="card card-pad" key={nm} style={{ display: "flex", alignItems: "center", gap: 14 }}> | |
| <div style={{ width: 40, height: 40, borderRadius: 10, flexShrink: 0, display: "grid", placeItems: "center", border: "1px solid var(--line-2)", background: "var(--raised)", color: "var(--ink)", fontFamily: "var(--mono)", fontWeight: 700, fontSize: 11 }}>{isHf ? "🤗" : tag}</div> | |
| <div style={{ flex: 1, minWidth: 0 }}><div style={{ fontWeight: 700, fontSize: 14 }}>{nm}</div><div style={{ fontSize: 12, color: "var(--ink-3)", marginTop: 2 }}>{ds}</div></div> | |
| {isHf ? <button className="btn btn-ghost btn-sm" onClick={() => { setImportSource("huggingface"); setImportOpen(true); }}><GI d={G.search} size={13} /> Search Models</button> | |
| : st === "connected" ? <span className="chip green"><span className="dot green" /> connected</span> : <button className="btn btn-ghost btn-sm" onClick={() => { setImportSource(tag === "GH" ? "github" : tag === "S3" ? "s3" : null); setImportOpen(true); }}>Connect</button>} | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| {tab === "Model Profiles" && ( | |
| <div className="card" style={{ overflow: "hidden" }}> | |
| <div className="card-h"><span className="t">Model Profiles {profiles && <LiveTag />}</span><button className="chip" onClick={refresh} style={{ cursor: "pointer" }}><GI d={G.refresh} size={11} /> refresh</button></div> | |
| <table className="tbl"> | |
| <thead><tr><th>Model Profile</th><th className="hide-sm">Provider</th><th>Status</th><th className="hide-sm">Runtime</th><th></th></tr></thead> | |
| <tbody> | |
| {(profiles || []).length === 0 && <tr><td colSpan={5} style={{ color: "var(--ink-4)", textAlign: "center", padding: 24 }}>{profiles ? "No profiles yet — click Import Model to resolve one." : "Runtime unreachable."}</td></tr>} | |
| {(profiles || []).map((p) => { | |
| const inst = (installs || []).find((i) => i.model_profile_id === p.id); | |
| const rt = inst ? inst.runtime_id : "Not attached"; | |
| const sc = PROFILE_STATUS_CLASS[p.status] || ""; | |
| const label = p.status === "profile_only" ? "Profile only" : p.status.charAt(0).toUpperCase() + p.status.slice(1); | |
| return ( | |
| <tr className="row" key={p.id}> | |
| <td className="nm mono" style={{ fontSize: 12.5 }}>{p.display_name}</td> | |
| <td className="hide-sm">{p.provider}</td> | |
| <td><span className={"chip " + sc}>{p.status === "downloading" && <span className="dot amber" />}{label}</span></td> | |
| <td className="hide-sm mono" style={{ fontSize: 12 }}>{rt}</td> | |
| <td style={{ textAlign: "right", whiteSpace: "nowrap" }}> | |
| {!inst | |
| ? <button className="btn btn-primary btn-sm" onClick={() => { setImportSource(null); setImportOpen(true); }}>Attach Runtime</button> | |
| : <button className="btn btn-ghost btn-sm" onClick={() => setTab("Runtime Cache")}>View</button>} | |
| </td> | |
| </tr> | |
| ); | |
| })} | |
| </tbody> | |
| </table> | |
| </div> | |
| )} | |
| {tab === "Runtime Cache" && ( | |
| <div className="card" style={{ overflow: "hidden" }}> | |
| <div className="card-h"><span className="t">Runtime Cache {installs && <LiveTag />}</span><button className="chip" onClick={refresh} style={{ cursor: "pointer" }}><GI d={G.refresh} size={11} /> refresh</button></div> | |
| <table className="tbl"> | |
| <thead><tr><th>Runtime</th><th>Model</th><th>Status</th><th className="hide-sm">Engine</th><th></th></tr></thead> | |
| <tbody> | |
| {(installs || []).length === 0 && <tr><td colSpan={5} style={{ color: "var(--ink-4)", textAlign: "center", padding: 24 }}>{installs ? "Nothing installed yet — attach a model from Model Profiles." : "Runtime unreachable."}</td></tr>} | |
| {(installs || []).map((c) => { | |
| const downloading = c.status === "downloading" || c.status === "checking" || c.status === "queued"; | |
| const sc = c.status === "ready" || c.status === "attached" ? "green" : c.status === "failed" ? "red" : "amber"; | |
| return ( | |
| <tr className="row" key={c.id}> | |
| <td className="nm mono" style={{ fontSize: 12.5 }}>{c.runtime_id}</td> | |
| <td className="mono" style={{ fontSize: 12.5 }}>{c.model_name}</td> | |
| <td style={{ minWidth: 160 }}> | |
| {downloading | |
| ? <div style={{ display: "flex", alignItems: "center", gap: 9 }}><div className="bar" style={{ flex: 1 }}><span className="amber" style={{ width: (c.progress || 0) + "%" }} /></div><span className="mono" style={{ fontSize: 11, color: "var(--lime)" }}>{c.progress || 0}%</span></div> | |
| : <span className={"chip " + sc}><span className={"dot " + sc} /> {c.status}</span>} | |
| </td> | |
| <td className="hide-sm mono" style={{ fontSize: 12 }}>{c.serving_engine || "—"}</td> | |
| <td style={{ textAlign: "right" }}><button className="btn btn-ghost btn-sm" onClick={() => c.job_id && window.open(API_BASE + "/v1/jobs/" + c.job_id, "_blank")}>{downloading ? "Logs" : "Test"}</button></td> | |
| </tr> | |
| ); | |
| })} | |
| </tbody> | |
| </table> | |
| </div> | |
| )} | |
| {importOpen && <ImportModelModal initialSource={importSource} onClose={() => { setImportOpen(false); setImportSource(null); }} onImport={handleImport} />} | |
| </div> | |
| ); | |
| } | |
| /* --------------------------------------------------------------- | |
| Jobs (live list + quick create + live event timeline). | |
| --------------------------------------------------------------- */ | |
| function JobsView() { | |
| const { jobs, refresh } = useJobs(3000); | |
| const [sel, setSel] = React.useState(null); | |
| const [filter, setFilter] = React.useState("All"); | |
| const [busy, setBusy] = React.useState(false); | |
| const statuses = ["All", "running", "complete", "error", "queued", "expired"]; | |
| const live = jobs || []; | |
| const rows = live.filter((j) => filter === "All" || j.status === filter); | |
| async function quick(type, payload) { | |
| setBusy(true); | |
| try { await api.post("/v1/jobs", { type, ttl_seconds: type === "mcp.test" ? 600 : undefined, payload }); await sleep(300); refresh(); } | |
| catch (e) {} finally { setBusy(false); } | |
| } | |
| if (sel) return <JobDetail job={sel} back={() => { setSel(null); refresh(); }} />; | |
| return ( | |
| <div className="wrap rise"> | |
| <div className="phead"> | |
| <div><p className="eyebrow">Execution</p><h1>Jobs {jobs && <LiveTag />}</h1><p>Every unit of runtime work — sandbox tests, model inspections, pulls, and tool calls — with live status.</p></div> | |
| <button className="btn btn-ghost" onClick={refresh}><GI d={G.refresh} size={15} /> Refresh</button> | |
| </div> | |
| <div className="card card-pad" style={{ display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap", marginBottom: 14 }}> | |
| <span style={{ fontSize: 12.5, color: "var(--ink-3)", fontWeight: 600 }}>Quick run:</span> | |
| <button className="btn btn-ghost btn-sm" disabled={busy} onClick={() => quick("model.inspect", { model: "hf:Qwen/Qwen2.5-7B-Instruct", revision: "main" })}><GI d={G.cpu} size={13} /> model.inspect Qwen2.5-7B</button> | |
| <button className="btn btn-ghost btn-sm" disabled={busy} onClick={() => quick("mcp.test", { runtime: "node", transport: "stdio", start_command: FILESYSTEM_CMD })}><GI d={G.play} size={13} /> mcp.test filesystem</button> | |
| </div> | |
| <div style={{ display: "flex", gap: 7, flexWrap: "wrap", marginBottom: 16 }}> | |
| {statuses.map((s) => <button key={s} onClick={() => setFilter(s)} className={"chip" + (filter === s ? " green" : "")} style={{ height: 28, cursor: "pointer", fontFamily: "var(--font)" }}>{s}</button>)} | |
| </div> | |
| <div className="card" style={{ overflow: "hidden" }}> | |
| <table className="tbl"> | |
| <thead><tr><th>Job ID</th><th>Type</th><th>Status</th><th className="hide-sm">Created</th><th className="hide-sm">Expires</th></tr></thead> | |
| <tbody> | |
| {rows.length === 0 && <tr><td colSpan={5} style={{ color: "var(--ink-4)", textAlign: "center", padding: 24 }}>{jobs ? "No jobs match." : "Runtime unreachable."}</td></tr>} | |
| {rows.map((j) => ( | |
| <tr className="row" key={j.job_id} style={{ cursor: "pointer" }} onClick={() => setSel(j)}> | |
| <td className="nm mono">{j.job_id}</td> | |
| <td><span className="chip">{j.type}</span></td> | |
| <td><span className={"chip " + (STATUS_CLASS[j.status] || "")}>{j.status === "running" && <span className="dot blue" />}{j.status}</span></td> | |
| <td className="hide-sm mono" style={{ fontSize: 12 }}>{(j.created_at || "").replace("T", " ").replace("Z", "")}</td> | |
| <td className="hide-sm mono" style={{ fontSize: 12 }}>{(j.expires_at || "").replace("T", " ").replace("Z", "")}</td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function JobDetail({ job, back }) { | |
| const [events, setEvents] = React.useState([]); | |
| const [snap, setSnap] = React.useState(job); | |
| React.useEffect(() => { | |
| let es = null, poll = null, alive = true; | |
| es = new EventSource(api.eventsURL("/v1/jobs/" + job.job_id + "/events")); | |
| es.onmessage = (ev) => { try { const d = JSON.parse(ev.data); setEvents((e) => [...e, d]); } catch (e) {} }; | |
| es.onerror = () => { if (es) es.close(); }; | |
| async function tick() { try { const s = await api.get("/v1/jobs/" + job.job_id); if (alive) setSnap(s); } catch (e) {} } | |
| tick(); poll = setInterval(tick, 2000); | |
| return () => { alive = false; if (es) es.close(); if (poll) clearInterval(poll); }; | |
| }, [job.job_id]); | |
| return ( | |
| <div className="wrap rise"> | |
| <button className="chip" onClick={back} style={{ marginBottom: 16, cursor: "pointer" }}><GI d={G.chev} size={13} style={{ transform: "rotate(90deg)" }} /> Jobs</button> | |
| <div style={{ display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }}> | |
| <h1 style={{ margin: 0, fontSize: 22, fontWeight: 700 }} className="mono">{snap.job_id}</h1> | |
| <span className="chip">{snap.type}</span> | |
| <span className={"chip " + (STATUS_CLASS[snap.status] || "")}>{snap.status}</span> | |
| </div> | |
| <div style={{ display: "grid", gridTemplateColumns: "1fr 320px", gap: 16, marginTop: 20 }} className="ov-grid"> | |
| <div className="card"> | |
| <div className="card-h"><span className="t">Event timeline</span><span className="chip"><span className="dot green pulse" /> SSE</span></div> | |
| <div style={{ padding: "10px 18px 16px" }}> | |
| {events.map((e, i) => ( | |
| <div key={i} className="rise" style={{ display: "flex", alignItems: "center", gap: 12, padding: "10px 0", borderBottom: "1px solid var(--line)" }}> | |
| <span className="mono" style={{ fontSize: 13, color: "var(--ink)", flex: 1 }}>{e.step}</span> | |
| <span style={{ fontSize: 12, color: "var(--ink-3)", flex: 2, minWidth: 0 }}>{e.message}</span> | |
| <span className={"chip " + (e.status === "ok" || e.status === "complete" ? "green" : e.status === "error" ? "red" : e.status === "expired" || e.status === "cancelled" ? "amber" : "")}>{e.status}</span> | |
| </div> | |
| ))} | |
| {events.length === 0 && <div style={{ padding: "10px 0", color: "var(--ink-4)" }}><span className="cur" /></div>} | |
| </div> | |
| </div> | |
| <div style={{ display: "grid", gap: 16, alignContent: "start" }}> | |
| <div className="card card-pad"> | |
| <span style={{ fontWeight: 700, fontSize: 13.5 }}>Details</span> | |
| <div style={{ display: "grid", gap: 9, marginTop: 12, fontSize: 12.5 }}> | |
| {[["Type", snap.type], ["Status", snap.status], ["Created", (snap.created_at || "").replace("T", " ").replace("Z", "")], ["Expires", (snap.expires_at || "").replace("T", " ").replace("Z", "")]].map(([k, v]) => ( | |
| <div key={k} style={{ display: "flex", justifyContent: "space-between", gap: 10 }}><span style={{ color: "var(--ink-3)" }}>{k}</span><span className="mono" style={{ color: "var(--ink)", textAlign: "right" }}>{v}</span></div> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="card"> | |
| <div className="card-h" style={{ padding: "10px 14px" }}><span className="mono" style={{ fontSize: 11, color: "var(--ink-3)" }}>RESULT</span></div> | |
| <pre className="mono" style={{ margin: 0, padding: 14, fontSize: 11.5, color: snap.error ? "var(--red)" : "var(--acc-2)", overflowX: "auto", maxHeight: 280 }}>{snap.error ? snap.error : JSON.stringify(snap.result, null, 2)}</pre> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| /* --------------------------------------------------------------- | |
| Logs (live tail of the newest job's SSE, with demo baseline). | |
| --------------------------------------------------------------- */ | |
| function LogsView() { | |
| const { jobs } = useJobs(4000); | |
| const [tail, setTail] = React.useState([]); | |
| const newest = jobs && jobs[0]; | |
| React.useEffect(() => { | |
| if (!newest) return; | |
| const es = new EventSource(api.eventsURL("/v1/jobs/" + newest.job_id + "/events")); | |
| es.onmessage = (ev) => { try { const d = JSON.parse(ev.data); setTail((t) => [...t.slice(-200), { job: newest.job_id, ...d }]); } catch (e) {} }; | |
| es.onerror = () => es.close(); | |
| return () => es.close(); | |
| }, [newest && newest.job_id]); | |
| function evColor(e) { return e.status === "error" ? "var(--red)" : e.status === "ok" || e.status === "complete" ? "var(--acc-2)" : e.status === "expired" ? "var(--amber)" : "var(--ink-2)"; } | |
| return ( | |
| <div className="wrap rise"> | |
| <div className="phead"><div><p className="eyebrow">Observability</p><h1>Logs {jobs && <LiveTag />}</h1><p>Live event stream from the newest job on this runtime.</p></div></div> | |
| <div className="logwin" style={{ height: 440 }}> | |
| {tail.length === 0 && <div style={{ color: "var(--ink-4)" }}>{newest ? "waiting for events from " + newest.job_id + "…" : "no jobs yet — start a sandbox or inspect a model to see live events"}</div>} | |
| {tail.map((e, i) => ( | |
| <div key={"t" + i} style={{ color: evColor(e) }}> | |
| <span style={{ color: "var(--ink-4)" }}>[{e.job}]</span> <span style={{ color: "var(--ink-4)" }}>{e.step}</span> {e.message} | |
| </div> | |
| ))} | |
| <span className="cur" /> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| /* --------------------------------------------------------------- | |
| Governance + settings + profile (reference data). | |
| --------------------------------------------------------------- */ | |
| const POLICY_ICON = { "Sandbox Policy": G.play, "Command Policy": G.shield, "Runtime Policy": G.server }; | |
| function PoliciesView() { | |
| const { data } = useFetch("/v1/policies"); | |
| const policies = data ? data.policies || [] : null; | |
| return ( | |
| <div className="wrap rise"> | |
| <div className="phead"><div><p className="eyebrow">Governance {policies && <LiveTag />}</p><h1>Policies</h1><p>The guardrails this runtime actually enforces — derived from its live configuration and the command allow/deny lists.</p></div></div> | |
| <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }} className="ov-grid"> | |
| {policies === null && <div style={{ color: "var(--ink-4)", padding: 24 }}>Loading…</div>} | |
| {(policies || []).map((p) => ( | |
| <div className="card" key={p.name}> | |
| <div className="card-h"><span className="t" style={{ display: "flex", alignItems: "center", gap: 9 }}><span style={{ color: "var(--acc)", display: "inline-flex" }}><GI d={POLICY_ICON[p.name] || G.shield} size={16} /></span> {p.name}</span><span className={"chip " + (p.active ? "green" : "")}>{p.active && <span className="dot green" />} {p.active ? "enforced" : "off"}</span></div> | |
| <div style={{ padding: "6px 16px 14px" }}> | |
| {Object.entries(p.body).map(([k, v]) => ( | |
| <div key={k} style={{ padding: "10px 0", borderBottom: "1px solid var(--line)" }}> | |
| <div style={{ fontSize: 12, color: "var(--ink-3)", marginBottom: Array.isArray(v) ? 7 : 0, display: "flex", justifyContent: "space-between", alignItems: "center", gap: 10 }}> | |
| <span style={{ fontWeight: 600, color: "var(--ink-2)" }}>{k.replace(/_/g, " ")}</span> | |
| {!Array.isArray(v) && (typeof v === "boolean" | |
| ? <span className={"chip " + (v ? "green" : "")}>{v ? "yes" : "no"}</span> | |
| : <span className="mono" style={{ fontSize: 12, color: "var(--ink)" }}>{String(v)}</span>)} | |
| </div> | |
| {Array.isArray(v) && (v.length | |
| ? <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>{v.map((x) => <span key={x} className={"chip" + (k.startsWith("blocked") ? " red" : "")} style={{ fontFamily: "var(--mono)", fontSize: 11 }}>{x}</span>)}</div> | |
| : <span style={{ fontSize: 12, color: "var(--ink-4)" }}>none</span>)} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function AuditView() { | |
| const [events, setEvents] = React.useState(null); | |
| const load = React.useCallback(() => { cloud.listAudit().then(setEvents).catch(() => setEvents([])); }, []); | |
| React.useEffect(() => { load(); const t = setInterval(load, 10000); return () => clearInterval(t); }, [load]); | |
| const rows = events || []; | |
| return ( | |
| <div className="wrap rise"> | |
| <div className="phead"><div><p className="eyebrow">Compliance {events && <LiveTag />}</p><h1>Audit</h1><p>Append-only record of sensitive actions in your workspace — logins, runtime & token activity, credentials, model imports and attaches, and MatrixShell commands.</p></div><button className="btn btn-ghost" onClick={load}><GI d={G.refresh} size={15} /> Refresh</button></div> | |
| <div className="card" style={{ overflow: "hidden" }}> | |
| <table className="tbl"> | |
| <thead><tr><th>Timestamp</th><th>Action</th><th>Target</th><th className="hide-sm">Actor</th><th className="hide-sm">IP</th><th>Status</th></tr></thead> | |
| <tbody> | |
| {rows.length === 0 && <tr><td colSpan={6} style={{ color: "var(--ink-4)", textAlign: "center", padding: 24 }}>{events ? "No audited activity yet." : "…"}</td></tr>} | |
| {rows.map((e) => ( | |
| <tr className="row" key={e.id}> | |
| <td className="mono" style={{ fontSize: 12, color: "var(--ink-3)" }}>{(e.created_at || "").replace("T", " ").replace("Z", "").replace(/\..*/, "")}</td> | |
| <td className="nm"><span className="chip">{e.action}</span></td> | |
| <td style={{ fontSize: 12.5, maxWidth: 260, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{e.target || "—"}</td> | |
| <td className="hide-sm mono" style={{ fontSize: 11.5, color: "var(--ink-3)" }}>{e.actor || "—"}</td> | |
| <td className="hide-sm mono" style={{ fontSize: 11.5, color: "var(--ink-4)" }}>{e.ip || "—"}</td> | |
| <td><span className={"chip " + (e.status === "success" ? "green" : "red")}><span className={"dot " + (e.status === "success" ? "green" : "red")} /> {e.status}</span></td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // PROVIDERS — the model providers a workspace can bring its own credentials for. | |
| const BYO_PROVIDERS = [ | |
| { id: "huggingface", name: "Hugging Face", hint: "hf_…", ph: "hf_xxxxxxxxxxxxxxxxxxxx", note: "Use your own HF account & inference quota for HF LLMs inside your runtimes.", meta: "default_model" }, | |
| { id: "openai", name: "OpenAI-compatible", hint: "sk_…", ph: "sk-…", note: "Any OpenAI-compatible endpoint (OpenAI, Together, Groq, …).", meta: "base_url" }, | |
| { id: "anthropic", name: "Anthropic", hint: "sk-ant-…", ph: "sk-ant-…", note: "Claude models via the Anthropic API.", meta: "default_model" }, | |
| ]; | |
| function ProvidersCard() { | |
| const [list, setList] = React.useState(null); | |
| const [open, setOpen] = React.useState(null); // provider id being edited | |
| const [secret, setSecret] = React.useState(""); | |
| const [metaVal, setMetaVal] = React.useState(""); | |
| const [busy, setBusy] = React.useState(false); | |
| const [err, setErr] = React.useState(""); | |
| const [okMsg, setOkMsg] = React.useState(""); | |
| const load = React.useCallback(() => { | |
| cloud.listProviders().then(setList).catch(() => setList([])); | |
| }, []); | |
| React.useEffect(load, [load]); | |
| function startEdit(pid) { setOpen(pid); setSecret(""); setMetaVal(""); setErr(""); setOkMsg(""); } | |
| async function save(p) { | |
| setErr(""); setOkMsg(""); | |
| if (!secret.trim()) return setErr("Paste your token first."); | |
| setBusy(true); | |
| try { | |
| const meta = metaVal.trim() ? { [p.meta]: metaVal.trim() } : undefined; | |
| await cloud.setProvider(p.id, "default", secret.trim(), meta); | |
| setOpen(null); setSecret(""); setMetaVal(""); setOkMsg(p.name + " connected."); | |
| load(); | |
| } catch (e) { setErr(e.message || "Could not save credential."); } | |
| finally { setBusy(false); } | |
| } | |
| const byId = {}; | |
| (list || []).forEach((c) => { byId[c.provider] = c; }); | |
| return ( | |
| <div className="card" style={{ gridColumn: "1 / -1" }}> | |
| <div className="card-h"> | |
| <span className="t">Model providers · bring your own key</span> | |
| {list && <span className="chip"><span className="dot green" /> {list.length} connected</span>} | |
| </div> | |
| <div style={{ padding: "4px 18px 14px" }}> | |
| <p style={{ fontSize: 12.5, color: "var(--ink-3)", lineHeight: 1.6, margin: "10px 0 6px" }}> | |
| Plug in your own provider tokens. They're encrypted at rest (AES-256-GCM) and used server-side only — the console never shows the secret back, only a <span className="mono">••••1234</span> hint. | |
| </p> | |
| {okMsg && <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "9px 11px", margin: "6px 0", borderRadius: "var(--r-sm)", border: "1px solid var(--acc-line)", background: "var(--acc-soft)", color: "var(--acc-2)", fontSize: 12.5 }}><GI d={G.check} size={14} sw={2.4} /> {okMsg}</div>} | |
| {BYO_PROVIDERS.map((p) => { | |
| const cur = byId[p.id]; | |
| const editing = open === p.id; | |
| return ( | |
| <div key={p.id} style={{ padding: "13px 0", borderBottom: "1px solid var(--line)" }}> | |
| <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}> | |
| <div style={{ minWidth: 0 }}> | |
| <div style={{ fontSize: 13.5, fontWeight: 600, display: "flex", alignItems: "center", gap: 8 }}> | |
| {p.name} | |
| {cur ? <span className="chip green"><span className="dot green" /> {cur.hint}</span> : <span className="chip" style={{ color: "var(--ink-4)" }}>not set</span>} | |
| </div> | |
| <div style={{ fontSize: 12, color: "var(--ink-4)", marginTop: 3 }}>{p.note}</div> | |
| </div> | |
| <button className="btn btn-ghost btn-sm" onClick={() => (editing ? setOpen(null) : startEdit(p.id))}> | |
| {editing ? "Cancel" : cur ? "Replace" : "Connect"} | |
| </button> | |
| </div> | |
| {editing && ( | |
| <div style={{ display: "grid", gap: 10, marginTop: 12 }}> | |
| <AField label={p.name + " token"} type="password" value={secret} onChange={setSecret} placeholder={p.ph} icon="key" autoFocus /> | |
| <AField label={p.meta === "base_url" ? "Base URL (optional)" : "Default model (optional)"} value={metaVal} onChange={setMetaVal} placeholder={p.meta === "base_url" ? "https://api.example.com/v1" : "Qwen/Qwen2.5-7B-Instruct"} icon="cpu" /> | |
| {err && <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "9px 11px", borderRadius: "var(--r-sm)", border: "1px solid rgba(251,113,133,0.3)", background: "var(--red-soft)", color: "var(--red)", fontSize: 12.5 }}><GI d={G.x} size={14} /> {err}</div>} | |
| <div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}> | |
| <button className="btn btn-primary btn-sm" disabled={busy} onClick={() => save(p)}>{busy ? "Saving…" : "Save token"}</button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function CloudSettingsView() { | |
| const rt = useRuntime(); | |
| const { data: meData } = useFetch("/v1/auth/me"); | |
| const me = meData && meData.user; | |
| const det = (rt.caps && rt.caps.runtimes) || {}; | |
| const sections = [ | |
| ["Workspace", me ? me.workspace : "—"], | |
| ["Owner", me ? me.email : "—"], | |
| ["Role", me ? me.role : "—"], | |
| ["Runtime", rt.health ? rt.health.runtime_id : "—"], | |
| ["Mode", rt.health ? rt.health.mode : "—"], | |
| ["Version", rt.health ? "v" + rt.health.version : "—"], | |
| ]; | |
| const registries = [ | |
| ["Hugging Face", "connected"], | |
| ["Node runner", det.node ? "connected" : "available"], | |
| ["Python runner", det.python ? "connected" : "available"], | |
| ["Ollama", det.ollama ? "connected" : "available"], | |
| ["vLLM", det.vllm ? "connected" : "available"], | |
| ]; | |
| return ( | |
| <div className="wrap rise"> | |
| <div className="phead"><div><p className="eyebrow">Workspace</p><h1>Settings</h1><p>Your workspace, this runtime, and the detected model runtimes.</p></div></div> | |
| <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }} className="ov-grid"> | |
| <div className="card"> | |
| <div className="card-h"><span className="t">Workspace</span></div> | |
| <div style={{ padding: "4px 18px" }}> | |
| {sections.map(([k, v]) => ( | |
| <div key={k} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "13px 0", borderBottom: "1px solid var(--line)" }}> | |
| <span style={{ fontSize: 13.5, fontWeight: 600 }}>{k}</span><span style={{ fontSize: 12.5, color: "var(--ink-3)" }} className="mono">{v}</span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="card"> | |
| <div className="card-h"><span className="t">Registries</span><button className="chip" style={{ cursor: "pointer" }}><GI d={G.plus} size={11} /> add</button></div> | |
| <div style={{ padding: "4px 18px" }}> | |
| {registries.map(([k, v]) => ( | |
| <div key={k} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "13px 0", borderBottom: "1px solid var(--line)" }}> | |
| <span style={{ fontSize: 13.5, fontWeight: 600 }}>{k}</span> | |
| {v === "connected" ? <span className="chip green"><span className="dot green" /> connected</span> : <span className="chip" style={{ color: "var(--ink-4)" }}>{v}</span>} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| <StorageCard /> | |
| <ProvidersCard /> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| const PROFILE_DEFAULTS = { name: "Neo Anderson", email: "neo@acme.io", username: "neo", role: "AI Engineer", theme: "Dark", language: "English", timezone: "UTC", dateFormat: "YYYY-MM-DD", defaultWorkspace: "acme-prod", defaultEnv: "Production", notifyEmail: true, notifyJobs: true, notifySecurity: true, notifyDigest: false }; | |
| function loadProfile() { try { return { ...PROFILE_DEFAULTS, ...JSON.parse(localStorage.getItem("mcloud-profile") || "{}") }; } catch (e) { return { ...PROFILE_DEFAULTS }; } } | |
| function PField({ label, hint, children }) { | |
| return <label style={{ display: "block", minWidth: 0 }}><span style={{ display: "block", fontSize: 12.5, fontWeight: 600, color: "var(--ink-2)", marginBottom: 6 }}>{label}</span>{children}{hint && <span style={{ display: "block", fontSize: 11.5, color: "var(--ink-4)", marginTop: 5 }}>{hint}</span>}</label>; | |
| } | |
| const PINPUT = { width: "100%", height: 40, padding: "0 12px", fontSize: 13.5, color: "var(--ink)", background: "var(--inset)", border: "1px solid var(--line-2)", borderRadius: "var(--r-sm)", outline: "none" }; | |
| const PSELECT = { ...PINPUT, cursor: "pointer", appearance: "none", WebkitAppearance: "none", backgroundImage: "linear-gradient(45deg,transparent 50%,var(--ink-3) 50%),linear-gradient(135deg,var(--ink-3) 50%,transparent 50%)", backgroundPosition: "calc(100% - 16px) 17px, calc(100% - 11px) 17px", backgroundSize: "5px 5px, 5px 5px", backgroundRepeat: "no-repeat" }; | |
| function PToggle({ on, onChange }) { | |
| return <button onClick={() => onChange(!on)} role="switch" aria-checked={on} style={{ width: 40, height: 23, borderRadius: 99, padding: 2, flexShrink: 0, background: on ? "var(--acc)" : "var(--line-3)", transition: "background .15s" }}><span style={{ display: "block", width: 19, height: 19, borderRadius: 99, background: "#fff", transform: on ? "translateX(17px)" : "translateX(0)", transition: "transform .15s", boxShadow: "0 1px 3px rgba(0,0,0,0.3)" }} /></button>; | |
| } | |
| function ProfileView() { | |
| const [p, setP] = React.useState(loadProfile); | |
| const [saved, setSaved] = React.useState(false); | |
| const set = (k, v) => { setP((s) => ({ ...s, [k]: v })); setSaved(false); }; | |
| const initials = p.name.split(" ").map((x) => x[0]).slice(0, 2).join("").toUpperCase(); | |
| function save() { try { localStorage.setItem("mcloud-profile", JSON.stringify(p)); } catch (e) {} setSaved(true); setTimeout(() => setSaved(false), 1800); } | |
| function reset() { setP({ ...PROFILE_DEFAULTS }); setSaved(false); } | |
| const sel = (k, opts) => <select style={PSELECT} value={p[k]} onChange={(e) => set(k, e.target.value)}>{opts.map((o) => <option key={o} value={o}>{o}</option>)}</select>; | |
| return ( | |
| <div className="wrap rise" style={{ maxWidth: 880 }}> | |
| <div className="phead"><div><p className="eyebrow">Account</p><h1>Profile & preferences</h1><p>Your personal account details and standard workspace preferences.</p></div></div> | |
| <div className="card card-pad" style={{ display: "flex", alignItems: "center", gap: 16, flexWrap: "wrap" }}> | |
| <div style={{ width: 60, height: 60, borderRadius: 14, flexShrink: 0, display: "flex", alignItems: "center", justifyContent: "center", background: "linear-gradient(135deg, var(--acc), var(--acc-dim))", color: "#042012", fontWeight: 800, fontSize: 22 }}>{initials}</div> | |
| <div style={{ flex: 1, minWidth: 0 }}><div style={{ fontSize: 18, fontWeight: 700 }}>{p.name}</div><div style={{ fontSize: 13, color: "var(--ink-3)", marginTop: 2 }}>{p.email}</div></div> | |
| <span className="chip violet">{p.role}</span> | |
| <button className="btn btn-ghost btn-sm">Change avatar</button> | |
| </div> | |
| <div className="card" style={{ marginTop: 16 }}> | |
| <div className="card-h"><span className="t">Account</span></div> | |
| <div className="card-pad ov-grid" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}> | |
| <PField label="Full name"><input style={PINPUT} value={p.name} onChange={(e) => set("name", e.target.value)} /></PField> | |
| <PField label="Email"><input style={PINPUT} value={p.email} onChange={(e) => set("email", e.target.value)} /></PField> | |
| <PField label="Username"><input style={{ ...PINPUT, fontFamily: "var(--mono)" }} value={p.username} onChange={(e) => set("username", e.target.value)} /></PField> | |
| <PField label="Role" hint="Assigned by your workspace admin.">{sel("role", ["Platform Admin", "AI Engineer", "Security / Compliance", "Developer"])}</PField> | |
| </div> | |
| </div> | |
| <div className="card" style={{ marginTop: 16 }}> | |
| <div className="card-h"><span className="t">Preferences</span></div> | |
| <div className="card-pad ov-grid" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}> | |
| <PField label="Theme">{sel("theme", ["Dark", "System"])}</PField> | |
| <PField label="Language">{sel("language", ["English", "Español", "Deutsch", "Français", "日本語"])}</PField> | |
| <PField label="Timezone">{sel("timezone", ["UTC", "America/New_York", "America/Los_Angeles", "Europe/London", "Europe/Berlin", "Asia/Tokyo"])}</PField> | |
| <PField label="Date format">{sel("dateFormat", ["YYYY-MM-DD", "DD/MM/YYYY", "MM/DD/YYYY"])}</PField> | |
| <PField label="Default workspace">{sel("defaultWorkspace", ["acme-prod", "shared", "personal"])}</PField> | |
| <PField label="Default environment">{sel("defaultEnv", ["Production", "Staging", "Development"])}</PField> | |
| </div> | |
| </div> | |
| <div className="card" style={{ marginTop: 16 }}> | |
| <div className="card-h"><span className="t">Notifications</span></div> | |
| <div style={{ padding: "4px 18px" }}> | |
| {[["notifyEmail", "Email notifications", "Receive account and workspace emails."], ["notifyJobs", "Job alerts", "Notify when your jobs complete or fail."], ["notifySecurity", "Security alerts", "Approvals, policy changes, and token events."], ["notifyDigest", "Weekly digest", "A weekly summary of activity and usage."]].map(([k, nm, ds]) => ( | |
| <div key={k} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 14, padding: "14px 0", borderBottom: "1px solid var(--line)" }}> | |
| <div><div style={{ fontSize: 13.5, fontWeight: 600 }}>{nm}</div><div style={{ fontSize: 12, color: "var(--ink-3)", marginTop: 2 }}>{ds}</div></div> | |
| <PToggle on={p[k]} onChange={(v) => set(k, v)} /> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, marginTop: 20, flexWrap: "wrap" }}> | |
| <button className="btn btn-ghost" onClick={reset}><GI d={G.refresh} size={15} /> Reset to defaults</button> | |
| <div style={{ display: "flex", gap: 10 }}> | |
| <button className="btn btn-ghost" style={{ color: "var(--red)", borderColor: "rgba(255,93,108,0.3)" }}>Sign out</button> | |
| <button className="btn btn-primary" onClick={save}>{saved ? <><GI d={G.check} size={15} sw={2.4} /> Saved</> : "Save changes"}</button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| /* --------------------------------------------------------------- | |
| App shell + routing. | |
| --------------------------------------------------------------- */ | |
| /* --------------------------------------------------------------- | |
| Auth screen (wired to /v1/auth via the SQLite user store). | |
| --------------------------------------------------------------- */ | |
| function AField({ label, type = "text", value, onChange, placeholder, icon, autoFocus }) { | |
| const I = icon ? G[icon] : null; | |
| return ( | |
| <label style={{ display: "block" }}> | |
| <span style={{ display: "block", fontFamily: "var(--mono)", fontSize: 10.5, letterSpacing: "0.18em", textTransform: "uppercase", color: "var(--ink-3)", marginBottom: 7 }}>{label}</span> | |
| <div style={{ display: "flex", alignItems: "center", gap: 10, height: 46, padding: "0 13px", borderRadius: "var(--r-sm)", border: "1px solid var(--line-2)", background: "rgba(0,0,0,0.35)" }}> | |
| {I && <span style={{ color: "var(--acc)", display: "inline-flex", flexShrink: 0 }}><GI d={I} size={16} /></span>} | |
| <input type={type} value={value} onChange={(e) => onChange(e.target.value)} placeholder={placeholder} autoFocus={autoFocus} spellCheck={false} | |
| style={{ flex: 1, minWidth: 0, height: "100%", background: "transparent", border: "none", outline: "none", color: "var(--ink)", fontSize: 14 }} /> | |
| </div> | |
| </label> | |
| ); | |
| } | |
| function AuthScreen({ onAuthed }) { | |
| const intent = React.useMemo(urlIntent, []); | |
| const [mode, setMode] = React.useState(intent.kind === "reset" ? "reset" : intent.kind === "verify" ? "verify" : "login"); | |
| const [name, setName] = React.useState(""); | |
| const [email, setEmail] = React.useState(""); | |
| const [pw, setPw] = React.useState(""); | |
| const [pw2, setPw2] = React.useState(""); | |
| const [resetToken] = React.useState(intent.token); | |
| const [err, setErr] = React.useState(""); | |
| const [msg, setMsg] = React.useState(""); | |
| const [busy, setBusy] = React.useState(false); | |
| // Email verification runs as soon as the screen mounts with a verify link. | |
| React.useEffect(() => { | |
| if (mode !== "verify") return; | |
| if (!resetToken) { setErr("This verification link is missing its token."); return; } | |
| setBusy(true); | |
| auth.verify(resetToken) | |
| .then((m) => { setMsg(m); clearURLToken(); }) | |
| .catch((e) => setErr(e.message || "Verification failed.")) | |
| .finally(() => setBusy(false)); | |
| }, [mode, resetToken]); | |
| function switchMode(m) { setMode(m); setErr(""); setMsg(""); } | |
| async function submit(e) { | |
| e.preventDefault(); setErr(""); setMsg(""); | |
| // Password recovery — request a reset link. | |
| if (mode === "forgot") { | |
| if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return setErr("Enter a valid email."); | |
| setBusy(true); | |
| try { setMsg(await auth.forgot(email.trim())); } | |
| catch (e2) { setErr(e2.message || "Could not send reset email."); } | |
| finally { setBusy(false); } | |
| return; | |
| } | |
| // Choose a new password from a reset link. | |
| if (mode === "reset") { | |
| if (pw.length < 8) return setErr("Password must be at least 8 characters."); | |
| if (pw !== pw2) return setErr("Passwords do not match."); | |
| setBusy(true); | |
| try { setMsg(await auth.reset(resetToken, pw)); clearURLToken(); setTimeout(() => switchMode("login"), 1200); } | |
| catch (e2) { setErr(e2.message || "Could not reset password."); } | |
| finally { setBusy(false); } | |
| return; | |
| } | |
| if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return setErr("Enter a valid work email."); | |
| if (pw.length < 6) return setErr("Password must be at least 6 characters."); | |
| if (mode === "signup") { | |
| if (!name.trim()) return setErr("Enter your full name."); | |
| if (pw !== pw2) return setErr("Passwords do not match."); | |
| } | |
| setBusy(true); | |
| try { | |
| const user = mode === "signup" ? await auth.signup(name.trim(), email.trim(), pw) : await auth.login(email.trim(), pw); | |
| onAuthed(user); | |
| } catch (e2) { setErr(e2.message || "Authentication failed."); } | |
| finally { setBusy(false); } | |
| } | |
| const feats = [ | |
| ["shield", "Secure by default", "Signed manifests, audited installs, approval gates."], | |
| ["server", "Runtime isolation", "Hybrid and customer-agent execution modes."], | |
| ["cpu", "Model gateway", "Hugging Face, OpenAI-compatible, Ollama, vLLM."], | |
| ["audit", "Complete audit trail", "Every install, policy, and access change."], | |
| ]; | |
| const HEAD = { | |
| login: ["Welcome back", "Sign in to MatrixCloud", "Use your work email and password."], | |
| signup: ["Get started", "Create your account", "Set up a workspace owner account to get started."], | |
| forgot: ["Account recovery", "Reset your password", "Enter your email and we'll send you a secure reset link."], | |
| reset: ["Account recovery", "Choose a new password", "Pick a strong password — at least 8 characters."], | |
| verify: ["Email verification", "Verifying your email", "Confirming your address with MatrixCloud."], | |
| }; | |
| const ACTION = { login: "Sign in", signup: "Create account", forgot: "Send reset link", reset: "Update password", verify: "Continue" }; | |
| return ( | |
| <div style={{ position: "relative", zIndex: 1, minHeight: "100vh", display: "grid", gridTemplateColumns: "1.1fr 520px" }} className="auth-grid"> | |
| <section className="auth-hero" style={{ display: "flex", flexDirection: "column", justifyContent: "space-between", padding: "44px 52px" }}> | |
| <div style={{ display: "flex", alignItems: "center", gap: 11 }}> | |
| <span style={{ width: 38, height: 38, borderRadius: 11, display: "grid", placeItems: "center", border: "1px solid var(--acc-line)", background: "var(--acc-soft)", color: "var(--acc)", boxShadow: "0 0 26px rgba(0,255,136,0.14)" }}><GI d={G.terminal} size={19} /></span> | |
| <div> | |
| <p style={{ margin: 0, fontSize: 16, fontWeight: 700, letterSpacing: "-0.01em" }}>MatrixCloud</p> | |
| <p style={{ margin: "2px 0 0", fontFamily: "var(--mono)", fontSize: 9.5, letterSpacing: "0.24em", textTransform: "uppercase", color: "var(--acc-2)", opacity: 0.7 }}>Enterprise Control Plane</p> | |
| </div> | |
| </div> | |
| <div style={{ maxWidth: 620 }}> | |
| <p className="eyebrow">Secure AI infrastructure</p> | |
| <h1 style={{ margin: "18px 0 0", fontSize: "clamp(34px, 4vw, 56px)", fontWeight: 600, letterSpacing: "-0.045em", lineHeight: 1.02 }}>Operate your AI<br />runtime layer.</h1> | |
| <p style={{ margin: "20px 0 0", fontSize: 15, lineHeight: 1.7, color: "var(--ink-3)", maxWidth: 520 }}>Connect runtimes, test MCP servers, manage models, run agents, enforce policies, and audit every action across your workspace.</p> | |
| <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12, marginTop: 28 }} className="auth-feats"> | |
| {feats.map(([ic, t, c]) => ( | |
| <div key={t} style={{ borderRadius: "var(--r-md)", border: "1px solid var(--line)", background: "rgba(0,0,0,0.25)", padding: 16, backdropFilter: "blur(10px)" }}> | |
| <span style={{ color: "var(--acc)", display: "inline-flex" }}><GI d={G[ic]} size={18} /></span> | |
| <p style={{ margin: "11px 0 0", fontSize: 13.5, fontWeight: 600 }}>{t}</p> | |
| <p style={{ margin: "5px 0 0", fontSize: 12, lineHeight: 1.55, color: "var(--ink-4)" }}>{c}</p> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| <p style={{ margin: 0, fontFamily: "var(--mono)", fontSize: 11, color: "var(--ink-4)" }}>multitenant workspaces · session security · approval gates</p> | |
| </section> | |
| <section className="auth-form" style={{ display: "flex", alignItems: "center", justifyContent: "center", borderLeft: "1px solid var(--line)", background: "rgba(0,0,0,0.3)", padding: 28, backdropFilter: "blur(20px)" }}> | |
| <div className="card rise" style={{ width: "100%", maxWidth: 400, padding: 28 }}> | |
| <p className="eyebrow">{HEAD[mode][0]}</p> | |
| <h2 style={{ margin: "12px 0 0", fontSize: 25, fontWeight: 600, letterSpacing: "-0.02em" }}>{HEAD[mode][1]}</h2> | |
| <p style={{ margin: "8px 0 0", fontSize: 13, lineHeight: 1.6, color: "var(--ink-3)" }}>{HEAD[mode][2]}</p> | |
| {(mode === "login" || mode === "signup") && ( | |
| <div style={{ display: "flex", gap: 4, marginTop: 20, padding: 4, borderRadius: "var(--r-sm)", border: "1px solid var(--line)", background: "rgba(0,0,0,0.3)" }}> | |
| {[["login", "Sign in"], ["signup", "Sign up"]].map(([m, lab]) => ( | |
| <button key={m} onClick={() => switchMode(m)} style={{ flex: 1, height: 34, borderRadius: 8, fontSize: 13, fontWeight: 600, background: mode === m ? "var(--acc-soft)" : "transparent", color: mode === m ? "var(--acc-2)" : "var(--ink-3)", border: "1px solid " + (mode === m ? "var(--line-2)" : "transparent") }}>{lab}</button> | |
| ))} | |
| </div> | |
| )} | |
| {mode === "verify" ? ( | |
| <div style={{ marginTop: 22, display: "grid", gap: 14 }}> | |
| {busy && <p style={{ fontSize: 13, color: "var(--ink-3)" }}>Verifying your email…</p>} | |
| {msg && <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "10px 12px", borderRadius: "var(--r-sm)", border: "1px solid var(--acc-line)", background: "var(--acc-soft)", color: "var(--acc-2)", fontSize: 12.5 }}><GI d={G.check} size={14} sw={2.4} /> {msg}</div>} | |
| {err && <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "10px 12px", borderRadius: "var(--r-sm)", border: "1px solid rgba(251,113,133,0.3)", background: "var(--red-soft)", color: "var(--red)", fontSize: 12.5 }}><GI d={G.x} size={14} /> {err}</div>} | |
| <button className="btn btn-primary" onClick={() => switchMode("login")} style={{ height: 46 }}>Continue to sign in</button> | |
| </div> | |
| ) : ( | |
| <form onSubmit={submit} style={{ display: "grid", gap: 14, marginTop: 18 }}> | |
| {mode === "signup" && <AField label="Full name" value={name} onChange={setName} placeholder="Maya Chen" icon="user" autoFocus />} | |
| {(mode === "login" || mode === "signup" || mode === "forgot") && | |
| <AField label="Work email" type="email" value={email} onChange={setEmail} placeholder="you@company.com" icon="globe" autoFocus={mode === "login" || mode === "forgot"} />} | |
| {mode !== "forgot" && <AField label={mode === "reset" ? "New password" : "Password"} type="password" value={pw} onChange={setPw} placeholder="••••••••" icon="key" autoFocus={mode === "reset"} />} | |
| {(mode === "signup" || mode === "reset") && <AField label="Confirm password" type="password" value={pw2} onChange={setPw2} placeholder="••••••••" icon="key" />} | |
| {err && <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "10px 12px", borderRadius: "var(--r-sm)", border: "1px solid rgba(251,113,133,0.3)", background: "var(--red-soft)", color: "var(--red)", fontSize: 12.5 }}><GI d={G.x} size={14} /> {err}</div>} | |
| {msg && <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "10px 12px", borderRadius: "var(--r-sm)", border: "1px solid var(--acc-line)", background: "var(--acc-soft)", color: "var(--acc-2)", fontSize: 12.5 }}><GI d={G.check} size={14} sw={2.4} /> {msg}</div>} | |
| {mode === "login" && ( | |
| <div style={{ textAlign: "right", marginTop: -4 }}> | |
| <button type="button" onClick={() => switchMode("forgot")} style={{ fontSize: 12, color: "var(--acc-2)", fontWeight: 600 }}>Forgot password?</button> | |
| </div> | |
| )} | |
| <button className="btn btn-primary" type="submit" disabled={busy} style={{ height: 46, marginTop: 2 }}>{busy ? "Please wait…" : ACTION[mode]}</button> | |
| </form> | |
| )} | |
| {(mode === "login" || mode === "signup") && ( | |
| <p style={{ margin: "16px 0 0", textAlign: "center", fontSize: 12.5, color: "var(--ink-3)" }}> | |
| {mode === "login" ? "Need an account? " : "Already have an account? "} | |
| <button onClick={() => switchMode(mode === "login" ? "signup" : "login")} style={{ color: "var(--acc-2)", fontWeight: 600 }}>{mode === "login" ? "Sign up" : "Sign in"}</button> | |
| </p> | |
| )} | |
| {(mode === "forgot" || mode === "reset") && ( | |
| <p style={{ margin: "16px 0 0", textAlign: "center", fontSize: 12.5, color: "var(--ink-3)" }}> | |
| <button onClick={() => switchMode("login")} style={{ color: "var(--acc-2)", fontWeight: 600 }}>← Back to sign in</button> | |
| </p> | |
| )} | |
| <p style={{ margin: "14px 0 0", textAlign: "center", fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--ink-4)" }}>multitenant workspaces · session security</p> | |
| </div> | |
| </section> | |
| </div> | |
| ); | |
| } | |
| /* --------------------------------------------------------------- | |
| MatrixShell — AI-assisted operator terminal, wired to the runtime. | |
| `matrix` subcommands hit the real /v1 API; shell passthrough is a | |
| safe sandboxed simulation; plain English becomes a confirmable | |
| command suggestion with a risk level. | |
| --------------------------------------------------------------- */ | |
| const MS_DENY = [/mkfs/i, /dd\s+if=/i, /\bfdisk\b/i, /\bdiskpart\b/i, /\bshutdown\b/i, /\breboot\b/i, /:\(\)\s*\{/, /\brm\s+-rf\s+(\/|~|\$HOME)(\s|$)/i]; | |
| const msDenied = (c) => MS_DENY.some((re) => re.test(c)); | |
| async function msMatrix(cmd) { | |
| const p = cmd.trim().split(/\s+/); const sub = (p[1] || "").toLowerCase(); | |
| try { | |
| if (sub === "status") { | |
| const [h, c] = await Promise.all([api.get("/v1/health"), api.get("/v1/capabilities")]); | |
| return { tone: "ok", lines: [ | |
| `runtime ${h.runtime_id} · mode ${h.mode} · v${h.version}`, | |
| `capabilities: ${(c.capabilities || []).join(", ")}`, | |
| `runtimes: node=${c.runtimes.node} python=${c.runtimes.python} ollama=${c.runtimes.ollama}`, | |
| `limits: max_ttl=${c.limits.max_ttl_seconds}s · max_jobs=${c.limits.max_concurrent_jobs}`] }; | |
| } | |
| if (sub === "ps") { | |
| const r = await api.get("/v1/jobs"); const jobs = (r.jobs || []).slice(0, 10); | |
| if (!jobs.length) return { tone: "dim", lines: ["no jobs yet — try: matrix inspect hf:Qwen/Qwen2.5-7B-Instruct"] }; | |
| return { tone: "ok", lines: ["RUNNING / RECENT JOBS", ...jobs.map((j) => ` ${j.job_id} ${j.type.padEnd(13)} ${j.status}`)] }; | |
| } | |
| if (sub === "capabilities" || sub === "caps") { | |
| const c = await api.get("/v1/capabilities"); | |
| return { tone: "ok", lines: ["CAPABILITIES", ...(c.capabilities || []).map((x) => " " + x)] }; | |
| } | |
| if (sub === "inspect") { | |
| const model = p[2] || "hf:Qwen/Qwen2.5-7B-Instruct"; | |
| const meta = await runJobToCompletion("model.inspect", { model, revision: "main" }); | |
| return { tone: "ok", lines: [ | |
| `model ${meta.model}`, | |
| `task=${meta.pipeline_tag || "?"} · library=${meta.library_name || "?"} · type=${meta.model_type || "?"}`, | |
| `license=${meta.license || "?"} · params≈${fmtParams(meta.estimated_parameters)} · runtime=${meta.recommended_runtime} · gpu=${meta.requires_gpu}`] }; | |
| } | |
| if (sub === "help" || p.length === 1) return { tone: "sys", lines: [ | |
| "MATRIX CLI — inside MatrixShell (wired to this runtime)", | |
| " matrix status runtime health + capabilities (live)", | |
| " matrix ps running / recent jobs (live)", | |
| " matrix capabilities advertised capabilities (live)", | |
| " matrix inspect <model> resolve a model via model.inspect (live)", | |
| " …or just describe what you want in plain English."] }; | |
| return { tone: "dim", lines: [`unknown: ${cmd}`, "try: matrix help"] }; | |
| } catch (e) { return { tone: "dim", lines: ["✗ " + (e.message || "command failed")] }; } | |
| } | |
| const MS_NL = [ | |
| { re: /inspect.*runtime|runtime.*(health|status)|status/i, cmd: "matrix status", risk: "low", expl: "Shows the connected runtime, its mode, version and capabilities — live from this runtime." }, | |
| { re: /recent jobs|show.*jobs|running jobs|\bjobs\b/i, cmd: "matrix ps", risk: "low", expl: "Lists the jobs currently running on the connected runtime." }, | |
| { re: /(list|show).*(capabilit|tools)/i, cmd: "matrix capabilities", risk: "low", expl: "Lists the capabilities this runtime advertises." }, | |
| { re: /inspect.*model|resolve.*model|qwen|llama|mistral/i, cmd: "matrix inspect hf:Qwen/Qwen2.5-7B-Instruct", risk: "low", expl: "Resolves model metadata (task, license, parameters, recommended runtime) via model.inspect." }, | |
| { re: /(biggest|largest) files?/i, cmd: "du -ah . | sort -hr | head -n 20", risk: "low", expl: "Lists the 20 largest files and folders in the current directory." }, | |
| { re: /disk|space|storage/i, cmd: "df -h", risk: "low", expl: "Shows disk space usage for mounted filesystems." }, | |
| { re: /(install|add).*(package|dependency)/i, cmd: "pip install <package>", risk: "medium", expl: "Installs a package into the sandbox. Write operation — requires approval." }, | |
| ]; | |
| function msSuggest(t) { for (const r of MS_NL) if (r.re.test(t)) return { cmd: r.cmd, risk: r.risk, expl: r.expl }; return { cmd: `echo "${t.replace(/"/g, "")}"`, risk: "low", expl: "Could not map this to a known operation — here is a safe echo. Refine it, or run a matrix command." }; } | |
| const MS_HEADS = ["ls", "cd", "pwd", "whoami", "echo", "cat", "ps", "df", "du", "git", "python", "python3", "pip", "uv", "matrix", "matrixsh", "grep", "rm", "mkdir", "mv", "cp", "head", "tail", "find", "which", "env", "node", "npm", "npx", "curl", "touch", "wc", "sort", "make", "go"]; | |
| function msLooksCmd(t) { const s = t.trim(); if (!s) return false; if (/[?]|\bhow\b|\bwhat\b|\bwhy\b|\bcan i\b|please|inspect|show|list|test|create/i.test(s) && !s.startsWith("matrix")) return false; return MS_HEADS.includes(s.split(/\s+/)[0].toLowerCase()); } | |
| function msTone(t) { return t === "ok" ? "var(--acc-2)" : t === "sys" ? "var(--blue)" : t === "warn" ? "var(--lime)" : t === "dim" ? "var(--ink-3)" : "var(--ink-2)"; } | |
| function Suggestion({ s, answered, onAnswer }) { | |
| const riskCls = s.risk === "high" ? "red" : s.risk === "medium" ? "amber" : "green"; | |
| return ( | |
| <div style={{ border: "1px solid var(--line-2)", borderRadius: "var(--r-md)", margin: "8px 0 4px", overflow: "hidden", background: "rgba(0,0,0,0.3)" }}> | |
| <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "8px 13px", borderBottom: "1px solid var(--line)", background: "rgba(92,200,255,0.05)" }}> | |
| <span style={{ color: "var(--blue)", display: "inline-flex" }}><GI d={G.cpu} size={14} /></span> | |
| <span style={{ fontSize: 12, fontWeight: 600, color: "var(--blue)", letterSpacing: "0.02em" }}>Suggested command</span> | |
| </div> | |
| <div style={{ padding: "12px 14px" }}> | |
| <div style={{ fontSize: 12.5, lineHeight: 1.6, color: "var(--ink-2)" }}>{s.expl}</div> | |
| <pre className="mono" style={{ margin: "10px 0 0", padding: "10px 12px", borderRadius: 8, background: "rgba(0,0,0,0.5)", border: "1px solid var(--line)", fontSize: 12.5, color: "#fff", overflowX: "auto" }}>{s.cmd}</pre> | |
| <div style={{ display: "flex", alignItems: "center", gap: 12, marginTop: 11 }}> | |
| <span className={"chip " + riskCls}>risk: {s.risk}</span> | |
| {s.risk !== "low" && <span style={{ fontSize: 11.5, color: "var(--lime)" }}>write operation · approval required</span>} | |
| </div> | |
| {!answered ? ( | |
| <div style={{ display: "flex", alignItems: "center", gap: 9, marginTop: 12 }}> | |
| <span style={{ fontSize: 12.5, color: "var(--ink-2)" }}>Execute it?</span> | |
| <button className="btn btn-primary btn-sm" onClick={() => onAnswer(true)}>Yes, run</button> | |
| <button className="btn btn-ghost btn-sm" onClick={() => onAnswer(false)}>No</button> | |
| </div> | |
| ) : (<div style={{ marginTop: 12, fontSize: 12.5, color: answered === "yes" ? "var(--acc-2)" : "var(--ink-3)" }}>{answered === "yes" ? "✓ executed" : "✗ cancelled"}</div>)} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function MatrixShellView({ back, user }) { | |
| const rt = useRuntime(); | |
| const [status, setStatus] = React.useState(null); // null=loading, {installed,version,...} | |
| const [lines, setLines] = React.useState([]); | |
| const [value, setValue] = React.useState(""); | |
| const [busy, setBusy] = React.useState(false); | |
| const [installing, setInstalling] = React.useState(false); | |
| const scroller = React.useRef(null); | |
| const inputRef = React.useRef(null); | |
| const hist = React.useRef([]); const hi = React.useRef(-1); | |
| const refreshStatus = React.useCallback(async () => { | |
| try { setStatus(await api.get("/v1/matrixshell/status")); } catch (e) { setStatus({ installed: false, error: e.message }); } | |
| }, []); | |
| React.useEffect(() => { refreshStatus(); inputRef.current && inputRef.current.focus(); }, [refreshStatus]); | |
| React.useEffect(() => { if (scroller.current) scroller.current.scrollTop = scroller.current.scrollHeight; }, [lines]); | |
| const push = (arr) => setLines((l) => [...l, ...arr]); | |
| const out = (text, c) => ({ kind: "out", c: c || "var(--ink-2)", text }); | |
| async function execMatrix(cmd) { | |
| const res = await msMatrix(cmd); | |
| push(res.lines.map((t) => out(t, msTone(res.tone)))); | |
| } | |
| // Real execution inside the local Python sandbox via /v1/matrixshell/exec. | |
| async function realExec(cmd) { | |
| if (msDenied(cmd)) { push([out("✗ Refusing to execute — blocked by safety denylist.", "var(--red)")]); return; } | |
| if (!status || !status.installed) { push([out("MatrixShell sandbox is not installed — click ‘Install MatrixShell’ above.", "var(--amber)")]); return; } | |
| setBusy(true); | |
| try { | |
| const r = await api.post("/v1/matrixshell/exec", { command: cmd }); | |
| const blocks = []; | |
| if (r.stdout) r.stdout.replace(/\n$/, "").split("\n").forEach((t) => blocks.push(out(t, "var(--ink)"))); | |
| if (r.stderr) r.stderr.replace(/\n$/, "").split("\n").forEach((t) => blocks.push(out(t, "var(--ink-3)"))); | |
| if (r.exit_code !== 0) blocks.push(out("exit " + r.exit_code, "var(--red)")); | |
| if (blocks.length === 0) blocks.push(out("✓ exit 0", "var(--acc-2)")); | |
| push(blocks); | |
| } catch (e) { push([out("✗ " + e.message, "var(--red)")]); } | |
| finally { setBusy(false); } | |
| } | |
| async function dispatch(cmd) { | |
| if (cmd.startsWith("matrix ") || cmd === "matrix") { await execMatrix(cmd); return; } | |
| await realExec(cmd); | |
| } | |
| async function answer(idx, yes) { | |
| setLines((l) => l.map((it, i) => i === idx ? { ...it, answered: yes ? "yes" : "no" } : it)); | |
| if (!yes) { push([out("Cancelled.", "var(--ink-3)")]); return; } | |
| await dispatch(lines[idx].s.cmd); | |
| } | |
| async function run(raw) { | |
| const text = raw.trim(); if (!text) return; | |
| hist.current.push(text); hi.current = hist.current.length; setValue(""); | |
| if (text === "clear") { setLines([]); return; } | |
| push([{ kind: "in", text }]); | |
| if (msLooksCmd(text)) { await dispatch(text); return; } | |
| push([{ kind: "suggestion", s: msSuggest(text) }]); | |
| } | |
| function onKey(e) { | |
| if (e.key === "ArrowUp") { e.preventDefault(); if (hi.current > 0) { hi.current--; setValue(hist.current[hi.current] || ""); } } | |
| else if (e.key === "ArrowDown") { e.preventDefault(); if (hi.current < hist.current.length - 1) { hi.current++; setValue(hist.current[hi.current] || ""); } else { hi.current = hist.current.length; setValue(""); } } | |
| } | |
| // Real install: stream the matrixshell.install job's pip/venv output. | |
| async function install() { | |
| setInstalling(true); | |
| push([out("$ installing MatrixShell into a local Python sandbox…", "var(--acc-2)")]); | |
| try { | |
| const r = await api.post("/v1/matrixshell/install", {}); | |
| const es = new EventSource(api.eventsURL("/v1/jobs/" + r.job_id + "/events")); | |
| es.onmessage = (ev) => { | |
| let d; try { d = JSON.parse(ev.data); } catch (e2) { return; } | |
| if (d.message) push([out(d.message, d.status === "error" ? "var(--red)" : "var(--ink-3)")]); | |
| if (d.step === "ready" || d.status === "error" || d.status === "complete") { es.close(); setInstalling(false); refreshStatus(); } | |
| }; | |
| es.onerror = () => { es.close(); setInstalling(false); refreshStatus(); }; | |
| } catch (e) { push([out("✗ " + e.message, "var(--red)")]); setInstalling(false); } | |
| } | |
| const installed = status && status.installed; | |
| const chips = installed | |
| ? ["matrix status", "matrixsh --help", "python --version", "ls -la", "pip list"] | |
| : ["matrix status", "matrix ps"]; | |
| const empty = lines.length === 0; | |
| const shellTag = status == null ? "checking…" : installed ? ("matrixsh " + status.version) : "not installed"; | |
| return ( | |
| <div className="wrap rise"> | |
| <div className="phead"> | |
| <div> | |
| <p className="eyebrow">Operator terminal</p> | |
| <h1>MatrixShell</h1> | |
| <p>The real MatrixShell CLI in a Python sandbox on this runtime's host. Type a command (executed for real), or describe what you want and confirm the suggestion.</p> | |
| </div> | |
| <div style={{ display: "flex", gap: 9, flexWrap: "wrap", alignItems: "center" }}> | |
| <span className={"chip " + (rt.online ? "green" : "red")}><span className={"dot " + (rt.online ? "green pulse" : "red")} /> {rt.online ? "Connected" : "Offline"}</span> | |
| <span className="chip green"><GI d={G.shield} size={12} /> Safe mode</span> | |
| <span className={"chip " + (installed ? "green" : "amber")}><GI d={G.terminal} size={12} /> {shellTag}</span> | |
| <button className="btn btn-ghost btn-sm" onClick={back}><GI d={G.x} size={14} /> Close</button> | |
| </div> | |
| </div> | |
| {status != null && !installed && ( | |
| <div className="card card-pad" style={{ marginBottom: 16, display: "flex", alignItems: "center", gap: 16, flexWrap: "wrap", borderColor: "var(--amber-soft)" }}> | |
| <div style={{ flex: 1, minWidth: 240 }}> | |
| <div style={{ fontWeight: 700, fontSize: 14 }}>MatrixShell is not installed yet</div> | |
| <p style={{ margin: "5px 0 0", fontSize: 12.5, color: "var(--ink-3)" }}>This installs the real <span className="mono">matrixsh</span> CLI from git into a dedicated Python venv on this host, then runs commands there. {status.error ? "(" + status.error + ")" : ""}</p> | |
| </div> | |
| <button className="btn btn-primary" onClick={install} disabled={installing}><GI d={G.download} size={15} /> {installing ? "Installing…" : "Install MatrixShell"}</button> | |
| </div> | |
| )} | |
| <div className="card card-pad" style={{ marginBottom: 16 }}> | |
| <div style={{ display: "flex", flexWrap: "wrap", gap: "14px 36px" }}> | |
| {[["Workspace", (user && user.workspace) || "—"], ["Runtime", (rt.health && rt.health.runtime_id) || "—"], ["Mode", (rt.health && rt.health.mode) || "—"], ["matrixsh", installed ? status.version : "—"], ["Sandbox", (status && status.sandbox) || "—"]].map(([k, v]) => ( | |
| <div key={k} style={{ minWidth: 0 }}> | |
| <div style={{ fontFamily: "var(--mono)", fontSize: 10, letterSpacing: "0.16em", textTransform: "uppercase", color: "var(--ink-4)" }}>{k}</div> | |
| <div className="mono" style={{ fontSize: 12.5, color: "var(--ink)", marginTop: 4, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", maxWidth: 320 }}>{v}</div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="card" style={{ display: "flex", flexDirection: "column", overflow: "hidden" }}> | |
| <div className="card-h"><span className="t">Terminal</span><span className="chip mono">{installed ? "sandbox · matrixsh " + status.version : "matrix-runtime"}</span></div> | |
| <div ref={scroller} style={{ minHeight: 320, maxHeight: "46vh", overflowY: "auto", padding: "16px 18px", fontFamily: "var(--mono)", fontSize: 12.5, lineHeight: 1.75 }}> | |
| <p style={{ margin: 0, color: "var(--ink-3)" }}>{installed ? "matrixsh " + status.version + " · real Python sandbox on " + ((rt.health && rt.health.runtime_id) || "this host") : "MatrixShell control terminal"}</p> | |
| <p style={{ margin: 0, color: "var(--ink-4)" }}>Commands run for real in the sandbox. Destructive operations are blocked. <span className="mono">matrix …</span> talks to the control plane.</p> | |
| {empty && ( | |
| <div style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", padding: "40px 0 32px", textAlign: "center", opacity: 0.7 }}> | |
| <span style={{ color: "var(--acc)", opacity: 0.5 }}><GI d={G.terminal} size={30} /></span> | |
| <p style={{ margin: "16px 0 0", fontSize: 13, color: "var(--ink-3)", fontFamily: "var(--font)" }}>{installed ? "Ready for commands" : "Install MatrixShell to run sandbox commands"}</p> | |
| <p style={{ margin: "6px 0 0", fontSize: 11.5, color: "var(--ink-4)", fontFamily: "var(--font)" }}>Try: matrixsh --help · python --version · matrix status</p> | |
| </div> | |
| )} | |
| <div style={{ marginTop: 12 }}> | |
| {lines.map((it, i) => it.kind === "suggestion" | |
| ? <Suggestion key={i} s={it.s} answered={it.answered} onAnswer={(yes) => answer(i, yes)} /> | |
| : it.kind === "in" | |
| ? <p key={i} style={{ margin: 0 }}><span style={{ color: "var(--acc)" }}>sandbox</span><span style={{ color: "var(--ink-4)" }}> $ </span><span style={{ color: "#fff" }}>{it.text}</span></p> | |
| : <p key={i} style={{ margin: 0, color: it.c, whiteSpace: "pre-wrap" }}>{it.text}</p>)} | |
| {busy && <p style={{ margin: 0, color: "var(--ink-4)" }}><span className="cur" /></p>} | |
| </div> | |
| </div> | |
| <div style={{ borderTop: "1px solid var(--line)", background: "rgba(0,0,0,0.25)", padding: 14, flexShrink: 0 }}> | |
| <div className="no-bar" style={{ display: "flex", gap: 8, overflowX: "auto", marginBottom: 11 }}> | |
| {chips.map((c) => <button key={c} onClick={() => run(c)} className="chip" style={{ flexShrink: 0, cursor: "pointer", height: 28, fontFamily: "var(--font)" }}>{c}</button>)} | |
| </div> | |
| <form onSubmit={(e) => { e.preventDefault(); run(value); }} style={{ display: "flex", alignItems: "center", gap: 10, height: 46, padding: "0 14px", borderRadius: "var(--r-sm)", border: "1px solid var(--line-2)", background: "rgba(0,0,0,0.4)" }}> | |
| <span className="mono" style={{ fontSize: 13.5, color: "var(--acc)", fontWeight: 700 }}>sandbox<span style={{ color: "var(--ink-4)" }}>></span></span> | |
| <input ref={inputRef} value={value} onChange={(e) => setValue(e.target.value)} onKeyDown={onKey} placeholder="type a command, or ask in plain English…" spellCheck={false} | |
| style={{ flex: 1, minWidth: 0, height: "100%", background: "transparent", border: "none", outline: "none", color: "var(--ink)", fontFamily: "var(--mono)", fontSize: 13.5 }} /> | |
| <button type="submit" aria-label="Run" disabled={busy} style={{ width: 34, height: 34, borderRadius: 9, background: "var(--acc)", color: "#03140c", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}><GI d={G.chevr} size={16} sw={2.4} /></button> | |
| </form> | |
| <div style={{ marginTop: 9, display: "flex", alignItems: "center", gap: 7, fontSize: 11, color: "var(--ink-4)" }}> | |
| <GI d={G.shield} size={12} style={{ color: "var(--acc)" }} /> Real execution in a Python sandbox · destructive commands are blocked. | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| /* --------------------------------------------------------------- | |
| Wizards — New Sandbox (real launch) + Attach Model. | |
| --------------------------------------------------------------- */ | |
| function Wizard({ title, eyebrow, steps, step, onCancel, children, footer }) { | |
| return ( | |
| <div className="wrap rise" style={{ maxWidth: 880 }}> | |
| <button className="chip" onClick={onCancel} style={{ marginBottom: 16, cursor: "pointer" }}><GI d={G.chev} size={13} style={{ transform: "rotate(90deg)" }} /> Cancel</button> | |
| <p className="eyebrow">{eyebrow}</p> | |
| <h1 style={{ margin: "7px 0 0", fontSize: 24, fontWeight: 700, letterSpacing: "-0.02em" }}>{title}</h1> | |
| <div style={{ display: "flex", alignItems: "center", gap: 0, margin: "22px 0 4px" }}> | |
| {steps.map((s, i) => { | |
| const state = i < step ? "done" : i === step ? "active" : "todo"; | |
| return ( | |
| <React.Fragment key={s}> | |
| <div style={{ display: "flex", alignItems: "center", gap: 9, flexShrink: 0 }}> | |
| <span style={{ width: 26, height: 26, borderRadius: 99, flexShrink: 0, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 12, fontWeight: 700, fontFamily: "var(--mono)", background: state === "done" ? "var(--acc)" : "transparent", border: "1px solid " + (state === "todo" ? "var(--line-3)" : "var(--acc)"), color: state === "done" ? "#042012" : state === "active" ? "var(--acc)" : "var(--ink-4)" }}>{state === "done" ? <GI d={G.check} size={14} sw={2.6} /> : i + 1}</span> | |
| <span style={{ fontSize: 12.5, fontWeight: 600, color: state === "todo" ? "var(--ink-4)" : "var(--ink)", whiteSpace: "nowrap" }} className="hide-sm">{s}</span> | |
| </div> | |
| {i < steps.length - 1 && <div style={{ flex: 1, height: 1, background: i < step ? "var(--acc)" : "var(--line-2)", margin: "0 12px", minWidth: 18 }} />} | |
| </React.Fragment> | |
| ); | |
| })} | |
| </div> | |
| <div className="card" style={{ marginTop: 18 }}> | |
| <div className="card-pad">{children}</div> | |
| <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, padding: "14px 18px", borderTop: "1px solid var(--line)" }}>{footer}</div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function PickRow({ active, onClick, mono, title, sub, right, disabled }) { | |
| return ( | |
| <button onClick={disabled ? undefined : onClick} disabled={disabled} | |
| style={{ display: "flex", alignItems: "center", gap: 13, width: "100%", textAlign: "left", padding: "13px 14px", borderRadius: "var(--r-md)", border: "1px solid " + (active ? "var(--acc-line)" : "var(--line-2)"), background: active ? "var(--acc-soft)" : "var(--inset)", opacity: disabled ? 0.5 : 1, cursor: disabled ? "not-allowed" : "pointer", transition: "all .14s" }}> | |
| <span style={{ width: 38, height: 38, borderRadius: 10, flexShrink: 0, display: "flex", alignItems: "center", justifyContent: "center", border: "1px solid var(--line-2)", background: "var(--raised)", color: "var(--acc)", fontFamily: "var(--mono)", fontWeight: 700, fontSize: 12 }}>{mono}</span> | |
| <span style={{ flex: 1, minWidth: 0 }}> | |
| <span style={{ display: "block", fontSize: 13.5, fontWeight: 600, color: "var(--ink)" }}>{title}</span> | |
| <span style={{ display: "block", fontSize: 12, color: "var(--ink-3)", marginTop: 1 }}>{sub}</span> | |
| </span> | |
| {right} | |
| <span style={{ width: 18, height: 18, borderRadius: 99, flexShrink: 0, border: "1px solid " + (active ? "var(--acc)" : "var(--line-3)"), background: active ? "var(--acc)" : "transparent", display: "flex", alignItems: "center", justifyContent: "center" }}>{active && <GI d={G.check} size={12} sw={3} style={{ color: "#042012" }} />}</span> | |
| </button> | |
| ); | |
| } | |
| function NewSandboxWizard({ onCancel, onLaunch }) { | |
| const [step, setStep] = React.useState(0); | |
| const [pick, setPick] = React.useState(null); | |
| const [runtime, setRuntime] = React.useState(""); | |
| const catalog = useCatalog(); | |
| const { runtimes: realRuntimes } = useRuntimes(); | |
| const items = (catalog || []).filter((it) => it.sandbox); | |
| const steps = ["Component", "Environment", "Review"]; | |
| const selected = items.find((i) => i.id === pick); | |
| const next = () => setStep((s) => Math.min(steps.length - 1, s + 1)); | |
| const back = () => setStep((s) => Math.max(0, s - 1)); | |
| const runtimes = (realRuntimes || []).filter((r) => r.statusClass !== "red"); | |
| React.useEffect(() => { if (!runtime && runtimes[0]) setRuntime(runtimes[0].name); }, [runtimes, runtime]); | |
| return ( | |
| <Wizard eyebrow="Sandboxes · New session" title="Start a sandbox" steps={steps} step={step} onCancel={onCancel} | |
| footer={<> | |
| <button className="btn btn-ghost" onClick={step === 0 ? onCancel : back}>{step === 0 ? "Cancel" : "Back"}</button> | |
| {step < steps.length - 1 | |
| ? <button className="btn btn-primary" disabled={step === 0 && !pick} onClick={next}>Continue <GI d={G.chevr} size={15} /></button> | |
| : <button className="btn btn-primary" onClick={() => onLaunch(selected)}><GI d={G.play} size={15} /> Launch sandbox</button>} | |
| </>}> | |
| {step === 0 && ( | |
| <div> | |
| <p style={{ margin: "0 0 14px", fontSize: 13, color: "var(--ink-3)" }}>Choose a sandbox-enabled MCP server to test in an isolated 10-minute session. No production secrets are used. Items badged <span className="chip green" style={{ height: 20 }}>live</span> run for real on this runtime.</p> | |
| <div style={{ display: "grid", gap: 9 }}> | |
| {items.map((it) => ( | |
| <PickRow key={it.id} active={pick === it.id} onClick={() => setPick(it.id)} mono={it.initials} title={it.name} sub={`${it.runtime} · ${it.secrets ? "needs secrets" : "no secrets"}`} | |
| right={it.startCommand ? <span className="chip green" style={{ height: 20 }}>live</span> : (it.verified ? <span className="chip green" style={{ height: 20 }}>Verified</span> : null)} /> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {step === 1 && ( | |
| <div> | |
| <p style={{ margin: "0 0 14px", fontSize: 13, color: "var(--ink-3)" }}>Select the runtime that will host the sandbox.</p> | |
| <div style={{ display: "grid", gap: 9 }}> | |
| {runtimes.length === 0 && <div style={{ color: "var(--ink-4)", fontSize: 12.5 }}>No runtimes available.</div>} | |
| {runtimes.map((r) => ( | |
| <PickRow key={r.id} active={runtime === r.name} onClick={() => setRuntime(r.name)} mono={(r.region || "lo").slice(0, 2).toUpperCase()} | |
| title={r.name} sub={`${r.mode} · ${r.region}`} right={r.live ? <LiveTag /> : <span className={"chip " + (r.statusClass || "green")} style={{ height: 20 }}><span className={"dot " + (r.statusClass || "green")} /> {r.status || "Online"}</span>} /> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {step === 2 && selected && ( | |
| <div> | |
| <p style={{ margin: "0 0 16px", fontSize: 13, color: "var(--ink-3)" }}>Review and launch. The session boots immediately and expires automatically.</p> | |
| <div style={{ display: "grid", gap: 1, borderRadius: "var(--r-sm)", overflow: "hidden", border: "1px solid var(--line)" }}> | |
| {[["Component", selected.name], ["Manifest", selected.id], ["Runtime", runtime || "—"], ["Command", selected.startCommand || "—"], ["TTL", "10 minutes"], ["Secrets", "None"]].map(([k, v]) => ( | |
| <div key={k} style={{ display: "flex", gap: 12, padding: "12px 14px", background: "var(--inset)" }}> | |
| <span style={{ width: 100, flexShrink: 0, fontSize: 12, color: "var(--ink-3)", textTransform: "uppercase", letterSpacing: "0.05em" }}>{k}</span> | |
| <span className="mono" style={{ fontSize: 12.5, color: "var(--ink)", wordBreak: "break-all" }}>{v}</span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </Wizard> | |
| ); | |
| } | |
| const MODEL_PROVIDERS = [ | |
| { id: "huggingface", mono: "HF", name: "Hugging Face", sub: "resolvable via model.inspect", models: ["hf:Qwen/Qwen2.5-7B-Instruct", "hf:mistralai/Mistral-7B-Instruct-v0.3", "hf:BAAI/bge-large-en-v1.5"] }, | |
| { id: "ollama", mono: "OL", name: "Ollama", sub: "local · 6 models", models: ["llama3.1", "mistral", "qwen2.5"] }, | |
| ]; | |
| function AttachModelWizard({ onCancel, onDone }) { | |
| const [step, setStep] = React.useState(0); | |
| const [prov, setProv] = React.useState("huggingface"); | |
| const [model, setModel] = React.useState(null); | |
| const [events, setEvents] = React.useState([]); | |
| const [done, setDone] = React.useState(false); | |
| const [err, setErr] = React.useState(""); | |
| const steps = ["Provider", "Model", "Attach"]; | |
| const provider = MODEL_PROVIDERS.find((p) => p.id === prov); | |
| const next = () => setStep((s) => Math.min(steps.length - 1, s + 1)); | |
| const back = () => setStep((s) => Math.max(0, s - 1)); | |
| React.useEffect(() => { | |
| if (step !== 2 || !model) return; | |
| let alive = true; setEvents([]); setDone(false); setErr(""); | |
| (async () => { | |
| const add = (k, msg) => alive && setEvents((e) => [...e, { k, msg }]); | |
| add("resolve", "Resolving metadata via model.inspect…"); | |
| try { | |
| if (prov === "huggingface") { | |
| const meta = await runJobToCompletion("model.inspect", { model, revision: "main" }); | |
| if (!alive) return; | |
| add("resolve", `task=${meta.pipeline_tag || "?"} · library=${meta.library_name || "?"}`); | |
| add("license", `License check · ${meta.license || "unknown"}`); | |
| add("params", `Parameters ≈ ${fmtParams(meta.estimated_parameters)} · runtime=${meta.recommended_runtime}`); | |
| add("ready", "Model resolved and ready to attach."); | |
| } else { | |
| await sleep(500); add("ready", "Provider model selected."); | |
| } | |
| if (alive) setDone(true); | |
| } catch (e) { if (alive) { setErr(e.message); add("error", "✗ " + e.message); } } | |
| })(); | |
| return () => { alive = false; }; | |
| }, [step, model, prov]); | |
| return ( | |
| <Wizard eyebrow="Models · Attach" title="Attach a model" steps={steps} step={step} onCancel={onCancel} | |
| footer={step < 2 ? ( | |
| <> | |
| <button className="btn btn-ghost" onClick={step === 0 ? onCancel : back}>{step === 0 ? "Cancel" : "Back"}</button> | |
| <button className="btn btn-primary" disabled={(step === 0 && !prov) || (step === 1 && !model)} onClick={next}>{step === 1 ? <><GI d={G.download} size={15} /> Attach model</> : <>Continue <GI d={G.chevr} size={15} /></>}</button> | |
| </> | |
| ) : ( | |
| <> | |
| <span style={{ fontSize: 12.5, color: err ? "var(--red)" : "var(--ink-3)" }}>{err ? "Failed" : (done ? "Resolved successfully." : "Resolving…")}</span> | |
| <button className="btn btn-primary" disabled={!done} onClick={onDone}><GI d={G.check} size={15} sw={2.4} /> Done</button> | |
| </> | |
| )}> | |
| {step === 0 && ( | |
| <div> | |
| <p style={{ margin: "0 0 14px", fontSize: 13, color: "var(--ink-3)" }}>Choose the provider that serves the model. Hugging Face models resolve live through this runtime.</p> | |
| <div style={{ display: "grid", gap: 9 }}> | |
| {MODEL_PROVIDERS.map((p) => ( | |
| <PickRow key={p.id} active={prov === p.id} onClick={() => { setProv(p.id); setModel(null); }} mono={p.mono} title={p.name} sub={p.sub} right={p.id === "huggingface" ? <LiveTag /> : <span className="chip green" style={{ height: 20 }}><span className="dot green" /> connected</span>} /> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {step === 1 && provider && ( | |
| <div> | |
| <p style={{ margin: "0 0 14px", fontSize: 13, color: "var(--ink-3)" }}>Pick a model from <b style={{ color: "var(--ink-2)" }}>{provider.name}</b>. Metadata is resolved before attach.</p> | |
| <div style={{ display: "grid", gap: 9 }}> | |
| {provider.models.map((m) => ( | |
| <PickRow key={m} active={model === m} onClick={() => setModel(m)} mono="M" title={m} sub={m.includes("bge") ? "embeddings" : "text-generation"} right={m.includes("7B") || m.includes("8B") ? <span className="chip amber" style={{ height: 20 }}>GPU</span> : null} /> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {step === 2 && ( | |
| <div> | |
| <div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 16 }}> | |
| <span style={{ width: 40, height: 40, borderRadius: 10, flexShrink: 0, display: "flex", alignItems: "center", justifyContent: "center", border: "1px solid var(--line-2)", background: "var(--raised)", color: "var(--acc)" }}><GI d={G.cpu} size={18} /></span> | |
| <div style={{ minWidth: 0 }}><div style={{ fontSize: 14, fontWeight: 700 }} className="mono">{model}</div><div style={{ fontSize: 12, color: "var(--ink-3)" }}>{provider.name}</div></div> | |
| <span className={"chip " + (err ? "red" : done ? "green" : "amber")} style={{ marginLeft: "auto" }}><span className={"dot " + (err ? "red" : done ? "green pulse" : "amber")} /> {err ? "Failed" : done ? "Ready" : "Resolving"}</span> | |
| </div> | |
| <div className="logwin" style={{ height: 200 }}> | |
| {events.map((e, i) => (<div key={i} className="rise" style={{ color: e.k === "error" ? "var(--red)" : "var(--ink-2)" }}><span style={{ color: e.k === "error" ? "var(--red)" : "var(--acc)" }}>{e.k === "error" ? "✗" : "✓"}</span> <span style={{ color: "var(--ink-4)" }}>[{e.k}]</span> {e.msg}</div>))} | |
| {!done && !err && <span className="cur" />} | |
| </div> | |
| {done && ( | |
| <div className="rise" style={{ marginTop: 14, padding: 14, borderRadius: "var(--r-md)", border: "1px solid var(--acc-line)", background: "var(--acc-soft)" }}> | |
| <div style={{ fontSize: 13.5, fontWeight: 700, color: "var(--acc-2)" }}>Model resolved</div> | |
| <p style={{ margin: "5px 0 0", fontSize: 12.5, color: "var(--ink-2)" }}><span className="mono">{model}</span> resolved and ready for agents to use.</p> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </Wizard> | |
| ); | |
| } | |
| /* --------------------------------------------------------------- | |
| Sidebar user account menu + sign-out. | |
| --------------------------------------------------------------- */ | |
| function MenuRow({ ic, label, chevron, danger, onClick }) { | |
| return ( | |
| <button className="navitem" onClick={onClick} style={{ fontSize: 13, color: danger ? "var(--red)" : undefined }}> | |
| {ic && <span className="i" style={danger ? { color: "var(--red)" } : null}><GI d={ic} size={15} /></span>} | |
| <span style={{ flex: 1, textAlign: "left" }}>{label}</span> | |
| {chevron && <GI d={G.chevr} size={13} style={{ color: "var(--ink-4)" }} />} | |
| </button> | |
| ); | |
| } | |
| function MenuDiv() { return <div style={{ height: 1, background: "var(--line)", margin: "5px 0" }} />; } | |
| function SignOutDialog({ onClose, onConfirm }) { | |
| return ReactDOM.createPortal( | |
| <div className="scrim" style={{ position: "fixed", inset: 0, zIndex: 80, display: "flex", alignItems: "center", justifyContent: "center", padding: 24, background: "rgba(5,8,12,0.7)", backdropFilter: "blur(6px)" }} onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}> | |
| <div className="rise card" style={{ width: "100%", maxWidth: 420, background: "var(--panel-2)" }}> | |
| <div className="card-h"><span className="t">Sign out of MatrixCloud?</span></div> | |
| <div style={{ padding: 18 }}> | |
| <p style={{ margin: 0, fontSize: 13, lineHeight: 1.6, color: "var(--ink-2)" }}>You will be signed out of this browser. Running jobs, runtimes, and sandboxes will continue.</p> | |
| <div style={{ display: "flex", gap: 10, marginTop: 18 }}> | |
| <button className="btn btn-ghost" style={{ flex: 1 }} onClick={onClose}>Cancel</button> | |
| <button className="btn btn-primary" style={{ flex: 1, background: "var(--red)", color: "#fff" }} onClick={() => onConfirm(false)}>Sign out</button> | |
| </div> | |
| <button className="navitem" style={{ marginTop: 8, fontSize: 12.5, color: "var(--ink-3)", justifyContent: "center" }} onClick={() => onConfirm(true)}>Sign out of all sessions</button> | |
| </div> | |
| </div> | |
| </div>, document.body); | |
| } | |
| function SidebarUser({ onProfile, go, user, onSignOut }) { | |
| const [open, setOpen] = React.useState(false); | |
| const [confirm, setConfirm] = React.useState(false); | |
| const initials = (user.name || user.email || "U").split(" ").map((x) => x[0]).slice(0, 2).join("").toUpperCase(); | |
| const act = (fn) => { setOpen(false); fn && fn(); }; | |
| return ( | |
| <div style={{ position: "relative" }}> | |
| {open && (<> | |
| <div style={{ position: "fixed", inset: 0, zIndex: 44 }} onClick={() => setOpen(false)} /> | |
| <div className="card rise no-bar" style={{ position: "absolute", bottom: "calc(100% + 8px)", left: 0, width: 250, zIndex: 45, padding: 6, background: "var(--panel-2)", boxShadow: "0 -8px 40px rgba(0,0,0,0.5)" }}> | |
| <div style={{ display: "flex", gap: 11, padding: "11px 11px 12px" }}> | |
| <span className="ua" style={{ width: 38, height: 38, fontSize: 15, borderRadius: 10 }}>{initials}</span> | |
| <div style={{ minWidth: 0 }}> | |
| <div style={{ fontSize: 13.5, fontWeight: 700, color: "var(--ink)" }}>{user.name}</div> | |
| <div style={{ fontSize: 12, color: "var(--ink-3)", fontFamily: "var(--mono)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{user.email}</div> | |
| <div style={{ fontSize: 11.5, color: "var(--acc-2)", marginTop: 3 }}>{user.role} · {user.workspace}</div> | |
| </div> | |
| </div> | |
| <MenuDiv /> | |
| <MenuRow ic={G.layers} label="Switch workspace" chevron onClick={() => act(() => go("settings"))} /> | |
| <MenuRow ic={G.user} label="Profile" onClick={() => act(onProfile)} /> | |
| <MenuRow ic={G.gear} label="Settings" onClick={() => act(() => go("settings"))} /> | |
| <MenuDiv /> | |
| <MenuRow ic={G.x} label="Sign out" danger onClick={() => { setOpen(false); setConfirm(true); }} /> | |
| </div> | |
| </>)} | |
| <button onClick={() => setOpen((v) => !v)} className={"user-row" + (open ? " open" : "")} aria-label="Account menu"> | |
| <span className="ua">{initials}</span> | |
| <span style={{ flex: 1, minWidth: 0, textAlign: "left" }}> | |
| <span style={{ display: "block", fontSize: 13, fontWeight: 600, color: "var(--ink)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{user.name}</span> | |
| <span style={{ display: "block", fontSize: 11, color: "var(--ink-4)", fontFamily: "var(--mono)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{user.email}</span> | |
| </span> | |
| <GI d={G.chev} size={15} style={{ color: "var(--ink-4)", transform: open ? "rotate(0deg)" : "rotate(180deg)", flexShrink: 0 }} /> | |
| </button> | |
| {confirm && <SignOutDialog onClose={() => setConfirm(false)} onConfirm={(all) => { setConfirm(false); onSignOut(all); }} />} | |
| </div> | |
| ); | |
| } | |
| const NAV = [ | |
| { grp: "Platform", items: [{ id: "overview", label: "Overview", ic: G.grid }, { id: "catalog", label: "Catalog", ic: G.layers }, { id: "sandboxes", label: "Sandboxes", ic: G.play }, { id: "models", label: "Models", ic: G.cpu }] }, | |
| { grp: "Runtime", items: [{ id: "runtimes", label: "Runtimes", ic: G.server }, { id: "agents", label: "Jobs", ic: G.activity }, { id: "logs", label: "Logs", ic: G.logs }, { id: "install", label: "Install Runtime", ic: G.download }] }, | |
| { grp: "Governance", items: [{ id: "policies", label: "Policies", ic: G.shield }, { id: "audit", label: "Audit", ic: G.audit }, { id: "settings", label: "Settings", ic: G.gear }] }, | |
| ]; | |
| function EnvSwitch() { | |
| const [open, setOpen] = React.useState(false); | |
| const [env, setEnv] = React.useState("Production"); | |
| const envs = [["Production", "green"], ["Staging", "amber"], ["Development", "blue"]]; | |
| return ( | |
| <div style={{ position: "relative" }}> | |
| <button className="switcher" onClick={() => setOpen((v) => !v)}><span className="dot green" /> <span className="env-switch-lab">{env}</span> <GI d={G.chev} size={14} className="chev" /></button> | |
| {open && (<> | |
| <div style={{ position: "fixed", inset: 0, zIndex: 40 }} onClick={() => setOpen(false)} /> | |
| <div className="card rise" style={{ position: "absolute", top: 40, left: 0, zIndex: 41, width: 180, padding: 6, background: "var(--panel-2)" }}> | |
| {envs.map(([e, c]) => <button key={e} className="navitem" onClick={() => { setEnv(e); setOpen(false); }} style={{ fontSize: 13 }}><span className={"dot " + c} /> {e}</button>)} | |
| </div> | |
| </>)} | |
| </div> | |
| ); | |
| } | |
| function WorkspaceSwitch() { | |
| return <button className="switcher"><span className="av" style={{ background: "linear-gradient(135deg,#2bd576,#1c9c54)" }}>A</span><span style={{ fontWeight: 600, color: "var(--ink)" }} className="hide-sm">Acme Corp</span><GI d={G.chev} size={14} className="chev" /></button>; | |
| } | |
| function UserMenu({ onProfile, onSettings }) { | |
| const [open, setOpen] = React.useState(false); | |
| const items = [{ label: "Profile & preferences", ic: G.user, fn: onProfile }, { label: "Workspace settings", ic: G.gear, fn: onSettings }]; | |
| return ( | |
| <div style={{ position: "relative" }}> | |
| <button className="user-av" onClick={() => setOpen((v) => !v)} aria-label="Account menu">N</button> | |
| {open && (<> | |
| <div style={{ position: "fixed", inset: 0, zIndex: 44 }} onClick={() => setOpen(false)} /> | |
| <div className="card rise" style={{ position: "absolute", top: 40, right: 0, zIndex: 45, width: 232, padding: 6, background: "var(--panel-2)" }}> | |
| <div style={{ padding: "10px 11px 11px", borderBottom: "1px solid var(--line)", marginBottom: 5 }}> | |
| <div style={{ fontSize: 13.5, fontWeight: 700 }}>Neo Anderson</div> | |
| <div style={{ fontSize: 12, color: "var(--ink-3)", marginTop: 1 }} className="mono">neo@acme.io</div> | |
| </div> | |
| {items.map((it) => <button key={it.label} className="navitem" onClick={() => { it.fn(); setOpen(false); }} style={{ fontSize: 13 }}><span className="i"><GI d={it.ic} size={16} /></span> {it.label}</button>)} | |
| <div style={{ borderTop: "1px solid var(--line)", marginTop: 5, paddingTop: 5 }}> | |
| <button className="navitem" style={{ fontSize: 13, color: "var(--red)" }}><span className="i" style={{ color: "var(--red)" }}><GI d={G.x} size={16} /></span> Sign out</button> | |
| </div> | |
| </div> | |
| </>)} | |
| </div> | |
| ); | |
| } | |
| function SandboxPlaceholder({ onNew }) { | |
| return ( | |
| <div className="wrap rise"> | |
| <div className="phead"><div><p className="eyebrow">Execution plane</p><h1>Sandboxes</h1><p>Temporary 10-minute sessions to test MCP servers safely before installing — no production secrets, auto-expiring.</p></div> | |
| <button className="btn btn-primary" onClick={onNew}><GI d={G.play} size={15} /> New sandbox</button></div> | |
| <div className="card card-pad" style={{ display: "flex", flexDirection: "column", alignItems: "center", textAlign: "center", padding: 48 }}> | |
| <span style={{ display: "inline-flex", color: "var(--ink-4)" }}><GI d={G.play} size={36} /></span> | |
| <h2 style={{ margin: "16px 0 0", fontSize: 18 }}>No active sandboxes</h2> | |
| <p style={{ margin: "8px 0 0", fontSize: 13, color: "var(--ink-3)", maxWidth: 380 }}>Start a guided 10-minute test session — pick a sandbox-enabled MCP server, choose a runtime, and launch.</p> | |
| <button className="btn btn-primary" style={{ marginTop: 18 }} onClick={onNew}><GI d={G.play} size={15} /> New sandbox</button> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function SideFoot() { | |
| const rt = useRuntime(); | |
| return ( | |
| <div className="side-foot"> | |
| <div style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 12, color: "var(--ink-2)" }}> | |
| <span className={"dot " + (rt.online ? "green pulse" : "red")} /> {rt.online ? "Control plane online" : (rt.loading ? "Connecting…" : "Runtime offline")} | |
| </div> | |
| <div style={{ fontSize: 11, color: "var(--ink-4)", marginTop: 5, fontFamily: "var(--mono)" }}>{rt.health ? rt.health.runtime_id + " · v" + rt.health.version : "cloud.matrixhub.io"}</div> | |
| <a href="/docs" target="_blank" rel="noopener noreferrer" style={{ display: "inline-flex", alignItems: "center", gap: 6, fontSize: 11, color: "var(--ink-3)", marginTop: 8 }}><GI d={G.logs} size={12} /> API docs</a> | |
| </div> | |
| ); | |
| } | |
| // fmtBytes renders a byte count as a compact human string. | |
| function fmtBytes(n) { | |
| n = Number(n) || 0; | |
| if (n < 1024) return n + " B"; | |
| const u = ["KB", "MB", "GB", "TB"]; | |
| let i = -1; | |
| do { n /= 1024; i++; } while (n >= 1024 && i < u.length - 1); | |
| return n.toFixed(n < 10 ? 1 : 0) + " " + u[i]; | |
| } | |
| // StorageCard shows data-directory usage from GET /v1/system/storage. | |
| function StorageCard() { | |
| const { data } = useFetch("/v1/system/storage", 15000); | |
| const areaLabels = { models: "Model cache", mcp: "MCP cache", agents: "Agents", jobs: "Jobs", logs: "Logs", database: "Database" }; | |
| const areas = data ? data.areas || {} : {}; | |
| const keys = Object.keys(areaLabels).filter((k) => k in areas); | |
| return ( | |
| <div className="card"> | |
| <div className="card-h"> | |
| <span className="t">Storage</span> | |
| {data && <span className="chip">{fmtBytes(data.total_bytes)} used{data.free_bytes ? " · " + fmtBytes(data.free_bytes) + " free" : ""}</span>} | |
| </div> | |
| <div style={{ padding: "4px 18px" }}> | |
| {!data && <div style={{ padding: "13px 0", color: "var(--ink-4)", fontSize: 12.5 }}>Loading…</div>} | |
| {data && keys.map((k) => ( | |
| <div key={k} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "12px 0", borderBottom: "1px solid var(--line)" }}> | |
| <span style={{ fontSize: 13.5, fontWeight: 600 }}>{areaLabels[k]}</span> | |
| <span className="mono" style={{ fontSize: 12.5, color: "var(--ink-3)" }}>{fmtBytes(areas[k])}</span> | |
| </div> | |
| ))} | |
| {data && <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "12px 0" }}> | |
| <span style={{ fontSize: 12, color: "var(--ink-4)" }}>{data.jobs_count || 0} job dir(s) · {data.data_dir}</span> | |
| </div>} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function CloudApp({ user, onSignOut }) { | |
| const [route, setRoute] = React.useState("overview"); | |
| const [sideOpen, setSideOpen] = React.useState(false); | |
| const [detailItem, setDetailItem] = React.useState(null); | |
| const [sandboxModal, setSandboxModal] = React.useState(null); | |
| const [sandboxSession, setSandboxSession] = React.useState(null); | |
| function go(id) { setRoute(id); setDetailItem(null); setSandboxSession(null); setSideOpen(false); } | |
| function openItem(item, sandbox) { if (sandbox) setSandboxModal(item); else setDetailItem(item); } | |
| function startSandbox(item) { setSandboxModal(null); setSandboxSession(item); setDetailItem(null); setRoute("sandboxes"); } | |
| let view; | |
| if (sandboxSession) view = <SandboxSession item={sandboxSession} back={() => { setSandboxSession(null); setRoute("catalog"); }} />; | |
| else if (detailItem) view = <DetailView item={detailItem} back={() => setDetailItem(null)} startSandbox={(it) => setSandboxModal(it)} />; | |
| else if (route === "overview") view = <OverviewView go={go} />; | |
| else if (route === "catalog") view = <CatalogView openItem={openItem} />; | |
| else if (route === "sandboxes") view = <SandboxPlaceholder onNew={() => setRoute("new-sandbox")} />; | |
| else if (route === "new-sandbox") view = <NewSandboxWizard onCancel={() => setRoute("sandboxes")} onLaunch={(it) => startSandbox(it)} />; | |
| else if (route === "models") view = <ModelsView onAttach={() => setRoute("attach-model")} />; | |
| else if (route === "attach-model") view = <AttachModelWizard onCancel={() => setRoute("models")} onDone={() => setRoute("models")} />; | |
| else if (route === "runtimes") view = <RuntimesView go={go} />; | |
| else if (route === "agents") view = <JobsView />; | |
| else if (route === "logs") view = <LogsView />; | |
| else if (route === "install") view = <InstallRuntimeView />; | |
| else if (route === "policies") view = <PoliciesView />; | |
| else if (route === "audit") view = <AuditView />; | |
| else if (route === "settings") view = <CloudSettingsView />; | |
| else if (route === "profile") view = <ProfileView />; | |
| else if (route === "matrixshell") view = <MatrixShellView user={user} back={() => go("overview")} />; | |
| else view = <OverviewView go={go} />; | |
| return ( | |
| <> | |
| <div className="top"> | |
| <button className="iconbtn menu-btn" onClick={() => setSideOpen((v) => !v)}><GI d={G.menu} size={18} /></button> | |
| <div className="brand"><span className="logo"><GI d={G.terminal} size={16} /></span><span className="nm hide-sm">MatrixCloud</span></div> | |
| <div className="sep hide-sm" /> | |
| <WorkspaceSwitch /> | |
| <EnvSwitch /> | |
| <div className="grow" /> | |
| <div className="topsearch"><GI d={G.search} size={15} /><input placeholder="Search…" /><kbd>⌘K</kbd></div> | |
| <button className={"btn btn-primary btn-sm" + (route === "matrixshell" ? "" : "")} onClick={() => go("matrixshell")} title="Open MatrixShell"> | |
| <GI d={G.terminal} size={15} /> <span className="hide-sm">MatrixShell</span> | |
| </button> | |
| <button className="iconbtn"><GI d={G.bell} size={17} /></button> | |
| </div> | |
| <div className="shell"> | |
| {sideOpen && <div className="side-scrim" onClick={() => setSideOpen(false)} />} | |
| <aside className={"side" + (sideOpen ? " open" : "")}> | |
| {NAV.map((grp) => ( | |
| <React.Fragment key={grp.grp}> | |
| <div className="navlabel">{grp.grp}</div> | |
| <nav> | |
| {grp.items.map((it) => ( | |
| <button key={it.id} className={"navitem" + (route === it.id ? " active" : "")} onClick={() => go(it.id)}> | |
| <span className="i"><GI d={it.ic} size={17} /></span> {it.label} | |
| {it.tag && <span className="tag">{it.tag}</span>} | |
| </button> | |
| ))} | |
| </nav> | |
| </React.Fragment> | |
| ))} | |
| <SideFoot /> | |
| <SidebarUser user={user} go={go} onProfile={() => go("profile")} onSignOut={onSignOut} /> | |
| </aside> | |
| <main className="main"><ReadinessBanner go={go} />{view}</main> | |
| </div> | |
| {sandboxModal && <SandboxModal item={sandboxModal} onClose={() => setSandboxModal(null)} onStart={() => startSandbox(sandboxModal)} />} | |
| </> | |
| ); | |
| } | |
| function Root() { | |
| const rt = useRuntimeState(); | |
| const [auth0, setAuth0] = React.useState({ loading: true, user: null }); | |
| React.useEffect(() => { | |
| let alive = true; | |
| if (!getToken()) { setAuth0({ loading: false, user: null }); return; } | |
| auth.me().then((u) => alive && setAuth0({ loading: false, user: u })) | |
| .catch(() => { setToken(null); alive && setAuth0({ loading: false, user: null }); }); | |
| return () => { alive = false; }; | |
| }, []); | |
| async function signOut(all) { await auth.logout(all); setAuth0({ loading: false, user: null }); } | |
| // Email-link flows (reset / verify) take precedence — they must work whether | |
| // or not a session is already active. | |
| const intent = React.useMemo(urlIntent, []); | |
| if (intent.kind === "reset" || intent.kind === "verify") { | |
| return <AuthScreen onAuthed={(u) => setAuth0({ loading: false, user: u })} />; | |
| } | |
| if (auth0.loading) { | |
| return <div style={{ minHeight: "100vh", display: "grid", placeItems: "center", color: "var(--ink-3)", fontFamily: "var(--mono)", fontSize: 13 }}>Loading MatrixCloud…</div>; | |
| } | |
| if (!auth0.user) return <AuthScreen onAuthed={(u) => setAuth0({ loading: false, user: u })} />; | |
| return ( | |
| <RuntimeCtx.Provider value={rt}> | |
| <CloudApp user={auth0.user} onSignOut={signOut} /> | |
| </RuntimeCtx.Provider> | |
| ); | |
| } | |
| ReactDOM.createRoot(document.getElementById("cloud-root")).render(<Root />); | |