|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| import { memo, useEffect, useRef, useState } from "react";
|
| import { barScale, renderableSpec, specKindName } from "./scriptViews";
|
| import type { DrawableSpec, ScriptRun, ScriptView as ScriptViewRecord } from "./scriptViews";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|
| );
|
| }
|
|
|
|
|
| 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>
|
| );
|
| }
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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}
|
| </>
|
| );
|
| }
|
|
|
| |
| |
| |
| |
| |
| |
|
|
| 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(() => {
|
|
|
|
|
| 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">
|
| {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>
|
| );
|
| });
|
| |