| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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. */} |
| {/* β W37-T46 β THE STRIP IS ABSENT WHEN THE CALLER CANNOT REVERT, which is what the |
| prop's own doc twelve lines up already prescribed ("The control is not rendered at |
| all when it is absent, rather than rendered and refused"). Line 293 shipped |
| `onRevert === undefined` folded into `disabled` instead, so the one mount in the |
| product rendered a permanently dead "Earlier | v1" pill and the comment disowned the |
| code beside it. Two facts, two shapes: NOTHING TO GO BACK TO is an absent strip, and |
| BUSY is a disabled button. */} |
| {onRevert !== undefined && (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} |
| 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> |
| ); |
| }); |
| |