loopable / web /src /customer-grid /ScriptViewPanel.tsx
fsanyoto's picture
Deploy AIOS web (React glide grid + FastAPI slice)
cf17b22 verified
Raw
History Blame
11 kB
// ---------------------------------------------------------------------------
// customer-grid / ScriptViewPanel.tsx — owner item 6's renderer (W36-T04, contract C3).
//
// ⛔ ITS OWN MODULE, AND THAT IS A HARNESS FACT AS WELL AS A TIDINESS ONE. It began inside
// `viewModes.tsx`, where the other display modes live — and that module `require`s `./cells`,
// which imports glide-data-grid, whose CJS build cannot load under this repo's node harness. So
// putting it there made the whole component untestable: the ONE thing this surface must prove is
// that a hostile spec is rendered as text, and that claim can only be made by RENDERING. Here the
// import graph is `react` + `./scriptViews` and nothing else, so `gridUx.test.ts` renders it for
// real and asserts on the HTML a person would receive.
// ⚠ The stylesheet is imported by `CustomerGrid.tsx` (the mount) for the same reason: tsc
// preserves a side-effect CSS import into the emitted CommonJS and node dies on it.
// ---------------------------------------------------------------------------
import { memo, useEffect, useRef, useState } from "react";
import { barScale, renderableSpec, specKindName } from "./scriptViews";
import type { DrawableSpec, ScriptRun, ScriptView as ScriptViewRecord } from "./scriptViews";
// ═══════════════════════════════════════════════════════════════════════════════════════════
// ⭐⭐ W36-T04 (owner item 6 · rulings R3/R10 · contract C3) — THE CODE-SCRIPT VIEW
//
// Owner, verbatim: *"Add code script as an interface (database View) so a user can build whatever
// they want through the Agent chat interface. be able to create any dashboard they want. User
// should have the ability to see the code AND the dashboard output of course."* Both halves are
// on screen at once, which is the whole of that sentence.
//
// ⛔⛔ NO BRANCH OF THIS RENDERER EXECUTES A STRING, AND THAT IS THE TICKET'S OWN `done-when`.
// R10 runs the Python in a SERVER sandbox exactly so the browser never has to trust the result;
// a renderer that then evaluated one would hand back everything the sandbox contains. So there is
// no `dangerouslySetInnerHTML`, no `new Function`, no `eval`, no `href`/`src` taken from a spec,
// and no `style` built from spec data anywhere below. Every value reaching the DOM is a SCALAR
// that `renderableSpec` admitted, rendered as TEXT — the one exception being a bar's width, which
// is a number clamped to a percentage here and never a string from the wire.
//
// ⛔ AN UNKNOWN `kind` IS A SENTENCE, NOT A BLANK. `spec` is whatever the script emitted (E's C3
// note 1: there is no fixed vocabulary, deliberately), so a view whose author invented
// `kind: "sankey"` is a legitimate thing this build cannot draw. Saying so, by name, is the
// difference between "this build does not know that shape yet" and "your script is broken".
//
// ⚠ `ok: false` IS A 200. A script that timed out or divided by zero asked a well-formed question
// whose ANSWER is that no view was produced; it is rendered beside the code, never thrown, and
// never a toast that disappears while the reader is looking at the line that caused it.
// ═══════════════════════════════════════════════════════════════════════════════════════════
function SpecTable({ spec }: { spec: DrawableSpec }) {
return (
<table className="cg-script-table">
<thead>
<tr>{(spec.columns ?? []).map((c, i) => <th key={`${c}:${i}`} scope="col">{c}</th>)}</tr>
</thead>
<tbody>
{(spec.rows ?? []).map((row, r) => (
<tr key={r}>{row.map((cell, c) => <td key={c}>{cell}</td>)}</tr>
))}
</tbody>
</table>
);
}
function SpecMetrics({ spec }: { spec: DrawableSpec }) {
return (
<div className="cg-script-metrics">
{(spec.items ?? []).map((item, i) => (
<div className="cg-script-metric" key={`${item.label}:${i}`}>
<div className="cg-script-metric-label">{item.label}</div>
<div className="cg-script-metric-value">{item.value}</div>
{item.note ? <div className="cg-script-metric-note">{item.note}</div> : null}
</div>
))}
</div>
);
}
function SpecBars({ spec }: { spec: DrawableSpec }) {
const series = spec.series ?? [];
const scale = barScale(series);
return (
<div className="cg-script-bars">
{series.map((s, i) => (
<div className="cg-script-bar" key={`${s.label}:${i}`}>
<span>{s.label}</span>
<span className="cg-script-bar-track">
{/* ⚠ The ONE computed style on this surface, and it is arithmetic on a NUMBER the
spec reader already validated as finite, clamped here. Never a string from the
wire, which is what would make this an injection point. */}
<span
className="cg-script-bar-fill"
style={{ width: `${Math.min(100, Math.round((Math.abs(s.value) / scale) * 100))}%` }}
/>
</span>
<span className="cg-script-bar-value">{s.display}</span>
</div>
))}
</div>
);
}
/** The refusal, and the four limit fields PRINTED rather than re-worded (C3 note 2). */
function ScriptRefusal({ run }: { run: ScriptRun }) {
return (
<div className="cg-script-refusal" role="status">
<h4>This script did not produce a view</h4>
<p>{run.error ?? "The run ended without emitting anything."}</p>
<dl>
<dt>Reason</dt>
<dd>{run.code}</dd>
{run.limit ? <><dt>Subject</dt><dd>{run.limit.subject}</dd></> : null}
{run.limit ? <><dt>Effect</dt><dd>{run.limit.effect}</dd></> : null}
{run.limit ? <><dt>Cause</dt><dd>{run.limit.cause}</dd></> : null}
{run.limit ? <><dt>What to do</dt><dd>{run.limit.recommendation}</dd></> : null}
</dl>
</div>
);
}
export function ScriptOutput({ run }: { run: ScriptRun | null }) {
if (run === null)
return <p className="cg-script-empty">Run the script to see what it draws.</p>;
const drawable = run.ok ? renderableSpec(run.spec) : null;
const claimed = specKindName(run.spec);
return (
<>
{!run.ok ? <ScriptRefusal run={run} /> : null}
{run.ok && drawable === null ? (
<div className="cg-script-refusal" role="status">
<h4>This build cannot draw that shape yet</h4>
<p>
{claimed
? `The script emitted a view of kind "${claimed}", which this version does not
render. It knows table, metrics, bars and text.`
: `The script emitted a view with no recognisable kind. This version renders
table, metrics, bars and text.`}
</p>
</div>
) : null}
{drawable !== null ? (
<>
{drawable.title ? <h3 className="cg-script-title">{drawable.title}</h3> : null}
{drawable.kind === "table" ? <SpecTable spec={drawable} /> : null}
{drawable.kind === "metrics" ? <SpecMetrics spec={drawable} /> : null}
{drawable.kind === "bars" ? <SpecBars spec={drawable} /> : null}
{drawable.kind === "text" ? <p className="cg-script-text">{drawable.body}</p> : null}
</>
) : null}
{run.stdout ? (
<pre className="cg-script-stdout">
{run.stdout}
{run.truncated ? "\n[output truncated]" : ""}
</pre>
) : null}
</>
);
}
/**
* The whole View: the source on the left, what it drew on the right.
*
* ⚠ A DRAFT RUN NEEDS NO SAVE (C3 note 3), which is what makes this usable: type, run, save when
* it works. The Save button is separate and deliberately not automatic, because a PUT is a NEW
* VERSION and an autosaving editor would fill the 40-deep history with keystrokes.
*/
export const ScriptView = memo(function ScriptView({
view,
running,
run,
saving,
readOnly = false,
onRun,
onSave,
}: {
view: ScriptViewRecord | null;
running: boolean;
run: ScriptRun | null;
saving?: boolean;
readOnly?: boolean;
onRun: (draft: string) => void;
onSave?: (source: string) => void;
}) {
const [draft, setDraft] = useState(view?.source ?? "");
const loadedId = useRef<string | null>(null);
useEffect(() => {
// Re-seed only when a DIFFERENT view is opened. Re-seeding on every `view` identity would
// discard what somebody is typing the moment the list refreshes underneath them.
if (!view || loadedId.current === view.id) return;
loadedId.current = view.id;
setDraft(view.source);
}, [view]);
const dirty = view !== null && draft !== view.source;
if (view === null)
return (
<div className="cg-script">
<p className="cg-script-empty cg-script-empty--pad">
This script view could not be opened.
</p>
</div>
);
return (
<div className="cg-script">
<div className="cg-script-body">
<section className="cg-script-pane" aria-label="Script source">
<div className="cg-script-pane-head">
<span>Code</span>
<span className="cg-script-meta">
{`v${view.version}`}
{dirty ? ", unsaved changes" : ""}
</span>
<span className="cg-script-spacer" />
<button
type="button"
className="cg-script-ghost"
disabled={!dirty || saving === true || readOnly || onSave === undefined}
onClick={() => onSave?.(draft)}
>
{saving === true ? "Saving" : "Save version"}
</button>
<button
type="button"
className="cg-script-run"
disabled={running}
onClick={() => onRun(draft)}
>
{running ? "Running" : "Run"}
</button>
</div>
<textarea
className="cg-script-code"
value={draft}
spellCheck={false}
readOnly={readOnly}
aria-label="Script source"
onChange={(e) => setDraft(e.target.value)}
/>
</section>
<section className="cg-script-pane" aria-label="Script output">
<div className="cg-script-pane-head">
<span>Output</span>
<span className="cg-script-spacer" />
{run !== null ? (
<span className="cg-script-meta">
{run.ms > 0 ? `${run.ms} ms` : ""}
</span>
) : null}
</div>
<div className="cg-script-out">
<ScriptOutput run={run} />
</div>
</section>
</div>
</div>
);
});