File size: 16,766 Bytes
aef86ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f2db836
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aef86ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f2db836
 
aef86ea
 
f2db836
aef86ea
 
 
 
 
 
f2db836
 
 
 
 
aef86ea
 
f2db836
 
 
 
 
 
 
aef86ea
 
 
 
 
 
 
 
 
 
 
f2db836
 
 
 
 
aef86ea
 
 
 
f2db836
aef86ea
 
 
 
 
 
 
 
 
 
 
f2db836
 
 
aef86ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f2db836
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aef86ea
 
 
 
 
f2db836
aef86ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
// ---------------------------------------------------------------------------
// 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>

  );

});