loopable / web /src /customer-grid /ScriptViewPanel.tsx
fsanyoto's picture
Deploy AIOS web (React glide grid + FastAPI slice)
f2db836 verified
Raw
History Blame
16.8 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>
);
}
/**
* ⭐⭐ W37-T41 β€” THE SHAPE OF A SCRIPT, SHOWN AS A PLACEHOLDER RATHER THAN PREFILLED.
*
* Picking Custom View mints a view with an EMPTY source (verified: `script_sandbox.check_source("")`
* accepts it, which is why "mint on pick" was safe to hand lane C in mailbox E-7). Without this, the
* person who has just created one meets a blank box and a Run button, which is not an invitation.
*
* β›” A PLACEHOLDER, NEVER A PREFILLED `value`. Prefilling would make the view DIRTY the moment it
* opened, arm Save on code nobody wrote, and put a version nobody asked for into a 40-deep history.
* A placeholder vanishes on the first keystroke and is never stored.
* ⚠ It is the two lines the sandbox's own fixture uses, so the example a person is shown is the
* example the gate proves runs [[test-double-built-from-the-producer]].
*/
const SCRIPT_STARTER =
"rows = scoped_table()\nemit({'kind': 'kpi', 'label': 'Records', 'value': len(rows)})";
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,
loading = false,
reverting = false,
onRun,
onSave,
onRevert,
}: {
view: ScriptViewRecord | null;
running: boolean;
run: ScriptRun | null;
saving?: boolean;
readOnly?: boolean;
/** ⚠ The host knows whether a fetch is outstanding; the panel cannot tell that from a null view.
* Defaulted so no existing caller breaks, which is safe HERE because the default is the older,
* more pessimistic sentence rather than a silently disabled feature. */
loading?: boolean;
reverting?: boolean;
onRun: (draft: string) => void;
onSave?: (source: string) => void;
/**
* ⭐ W37-T46 / D-338. Optional for the SAME reason `onSave` is: a read-only reader gets the
* editor without the controls, and a caller that cannot revert simply does not pass this. The
* control is not rendered at all when it is absent, rather than rendered and refused
* [[permitted-is-not-answerable]].
*/
onRevert?: (version: number) => 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;
// β›” W37-T41: "STILL LOADING" AND "COULD NOT BE OPENED" ARE DIFFERENT FACTS AND USED TO SHARE A
// SENTENCE. With mint-on-pick (E-7) the normal path now passes through `view === null` for as
// long as the fetch takes, so the old copy told every person who created a Custom View that it
// was broken, for a moment, every time. A state that renames itself is exactly what cost this
// product a QA pass one wave ago [[a-store-outage-renames-itself-at-every-layer]].
if (view === null)
return (
<div className="cg-script">
<p className="cg-script-empty cg-script-empty--pad">
{loading === true ? "Opening this view." : "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}`}
{/* ⚠ "v5" alone cannot tell a roll-back from a coincidence of matching code. This is
the sentence that makes the history read as what happened. */}
{view.restoredFrom ? `, restored from v${view.restoredFrom}` : ""}
{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>
{/* ⭐⭐ W37-T41 β€” THE PLACEHOLDER IS THE CREATE FLOW'S WHOLE EMPTY STATE, and it is the
half that made item 10 worth reopening. Picking Custom View mints a view with an
EMPTY source (verified: `script_sandbox.check_source("")` accepts it, which is why
"mint on pick" was safe to hand lane C in mailbox E-7). Without this the person who
just created one meets a blank box and a Run button, which is not an invitation, it
is a puzzle.
β›” A PLACEHOLDER, NOT A PREFILLED `value`. Prefilling would make the view DIRTY the
moment it opened, arm Save on code nobody wrote, and put a version nobody asked for
into a 40-deep history. It vanishes on the first keystroke and is never saved. */}
{/* ⭐⭐ W37-T46 / D-338 β€” THE CONTROL THAT WAS MISSING. The API already returned
`history` newest-first and the header already showed `v<n>`; there was simply no way
to go back, which is the whole of D-338. It matters more now that a chat writes the
code (R2): generation without roll-back means one bad answer costs somebody their
view, and the person cannot tell in advance which answer that will be.
β›” THE STRIP IS ABSENT, NOT DISABLED, WHEN THERE IS NOTHING TO GO BACK TO. A first
version has no history, and a row of dead controls reads as a broken feature rather
than as an empty one.
⚠ `trimmed` is PRINTED when it is non-zero. A history capped at 40 that silently
showed 40 would let a reader conclude the view was only ever saved 40 times; saying
how many were dropped is the difference between a partial record and a wrong one. */}
{(view.history?.length ?? 0) > 0 ? (
<div className="cg-script-history" aria-label="Earlier versions">
<span className="cg-script-history-lead">Earlier</span>
{[...(view.history ?? [])].map((h) => (
<button
type="button"
key={h.version}
className="cg-script-history-item"
disabled={readOnly || reverting || onRevert === undefined}
title={`Go back to version ${h.version}, saved by ${h.author || "somebody"}`}
onClick={() => onRevert?.(h.version)}
>
{`v${h.version}`}
</button>
))}
{view.trimmed > 0 ? (
<span className="cg-script-history-trimmed">
{`${view.trimmed} older ${view.trimmed === 1 ? "version" : "versions"} dropped`}
</span>
) : null}
{reverting ? <span className="cg-script-history-trimmed">Going back</span> : null}
</div>
) : null}
<textarea
className="cg-script-code"
value={draft}
spellCheck={false}
readOnly={readOnly}
placeholder={SCRIPT_STARTER}
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>
);
});