File size: 10,980 Bytes
cf17b22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
// ---------------------------------------------------------------------------
// 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>
  );
});