@graview/primitives 0.1.65 β†’ 0.1.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/dist/app-bar.d.ts +88 -15
  2. package/dist/app-bar.d.ts.map +1 -1
  3. package/dist/app-bar.js +176 -130
  4. package/dist/app-bar.js.map +1 -1
  5. package/dist/app-menu.d.ts +9 -0
  6. package/dist/app-menu.d.ts.map +1 -0
  7. package/dist/app-menu.js +60 -0
  8. package/dist/app-menu.js.map +1 -0
  9. package/dist/bar-activity.d.ts +5 -9
  10. package/dist/bar-activity.d.ts.map +1 -1
  11. package/dist/bar-activity.js +24 -7
  12. package/dist/bar-activity.js.map +1 -1
  13. package/dist/bar-panes.d.ts +5 -5
  14. package/dist/bar-panes.d.ts.map +1 -1
  15. package/dist/bar-panes.js +9 -5
  16. package/dist/bar-panes.js.map +1 -1
  17. package/dist/bar-suggested.d.ts +9 -1
  18. package/dist/bar-suggested.d.ts.map +1 -1
  19. package/dist/bar-suggested.js +11 -17
  20. package/dist/bar-suggested.js.map +1 -1
  21. package/dist/better-bar.d.ts +65 -0
  22. package/dist/better-bar.d.ts.map +1 -0
  23. package/dist/better-bar.js +252 -0
  24. package/dist/better-bar.js.map +1 -0
  25. package/dist/better-panel.d.ts +50 -0
  26. package/dist/better-panel.d.ts.map +1 -0
  27. package/dist/better-panel.js +151 -0
  28. package/dist/better-panel.js.map +1 -0
  29. package/dist/editable.d.ts +8 -1
  30. package/dist/editable.d.ts.map +1 -1
  31. package/dist/editable.js +140 -16
  32. package/dist/editable.js.map +1 -1
  33. package/dist/first-look-door.d.ts +2 -2
  34. package/dist/first-look-door.js +2 -2
  35. package/dist/first-look.d.ts.map +1 -1
  36. package/dist/first-look.js +9 -7
  37. package/dist/first-look.js.map +1 -1
  38. package/dist/frame.d.ts +2 -2
  39. package/dist/frame.d.ts.map +1 -1
  40. package/dist/frame.js +1 -1
  41. package/dist/frame.js.map +1 -1
  42. package/dist/inbox-pane.d.ts +31 -0
  43. package/dist/inbox-pane.d.ts.map +1 -0
  44. package/dist/inbox-pane.js +89 -0
  45. package/dist/inbox-pane.js.map +1 -0
  46. package/dist/inbox.d.ts +17 -0
  47. package/dist/inbox.d.ts.map +1 -0
  48. package/dist/inbox.js +84 -0
  49. package/dist/inbox.js.map +1 -0
  50. package/dist/index.d.ts +4 -3
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +3 -2
  53. package/dist/index.js.map +1 -1
  54. package/dist/menu-suggested.d.ts +54 -0
  55. package/dist/menu-suggested.d.ts.map +1 -0
  56. package/dist/menu-suggested.js +140 -0
  57. package/dist/menu-suggested.js.map +1 -0
  58. package/dist/menu-told-door.js +1 -1
  59. package/dist/menu-told-door.js.map +1 -1
  60. package/dist/menu-told.d.ts +7 -0
  61. package/dist/menu-told.d.ts.map +1 -1
  62. package/dist/menu-told.js +24 -0
  63. package/dist/menu-told.js.map +1 -1
  64. package/dist/notify-sheet.js +2 -2
  65. package/dist/notify-sheet.js.map +1 -1
  66. package/dist/page-better.d.ts +13 -0
  67. package/dist/page-better.d.ts.map +1 -0
  68. package/dist/page-better.js +714 -0
  69. package/dist/page-better.js.map +1 -0
  70. package/dist/page-parts.d.ts +41 -0
  71. package/dist/page-parts.d.ts.map +1 -0
  72. package/dist/page-parts.js +100 -0
  73. package/dist/page-parts.js.map +1 -0
  74. package/dist/pages.d.ts +2 -3
  75. package/dist/pages.d.ts.map +1 -1
  76. package/dist/pages.js +2 -3
  77. package/dist/pages.js.map +1 -1
  78. package/dist/profile.d.ts +1 -10
  79. package/dist/profile.d.ts.map +1 -1
  80. package/dist/profile.js +2 -3
  81. package/dist/profile.js.map +1 -1
  82. package/dist/scene-bar.d.ts +1 -1
  83. package/dist/scene-bar.d.ts.map +1 -1
  84. package/dist/scene-bar.js +4 -7
  85. package/dist/scene-bar.js.map +1 -1
  86. package/dist/section-better.d.ts +47 -9
  87. package/dist/section-better.d.ts.map +1 -1
  88. package/dist/section-better.js +239 -121
  89. package/dist/section-better.js.map +1 -1
  90. package/dist/section-door.d.ts +7 -0
  91. package/dist/section-door.d.ts.map +1 -1
  92. package/dist/section-door.js +39 -5
  93. package/dist/section-door.js.map +1 -1
  94. package/dist/section-menu.d.ts +23 -9
  95. package/dist/section-menu.d.ts.map +1 -1
  96. package/dist/section-menu.js +176 -23
  97. package/dist/section-menu.js.map +1 -1
  98. package/dist/section-words.d.ts +8 -0
  99. package/dist/section-words.d.ts.map +1 -1
  100. package/dist/section-words.js +25 -0
  101. package/dist/section-words.js.map +1 -1
  102. package/dist/shell.d.ts +5 -1
  103. package/dist/shell.d.ts.map +1 -1
  104. package/dist/shell.js +4 -2
  105. package/dist/shell.js.map +1 -1
  106. package/dist/suggested.d.ts +21 -10
  107. package/dist/suggested.d.ts.map +1 -1
  108. package/dist/suggested.js +43 -64
  109. package/dist/suggested.js.map +1 -1
  110. package/dist/told-ui.d.ts +1 -1
  111. package/dist/told-ui.d.ts.map +1 -1
  112. package/dist/told-ui.js +1 -1
  113. package/dist/told-ui.js.map +1 -1
  114. package/dist/when-words.d.ts +21 -0
  115. package/dist/when-words.d.ts.map +1 -0
  116. package/dist/when-words.js +43 -0
  117. package/dist/when-words.js.map +1 -0
  118. package/dist/workbench/activity.d.ts +44 -0
  119. package/dist/workbench/activity.d.ts.map +1 -1
  120. package/dist/workbench/activity.js +74 -59
  121. package/dist/workbench/activity.js.map +1 -1
  122. package/dist/workbench/inspector.d.ts.map +1 -1
  123. package/dist/workbench/inspector.js +170 -31
  124. package/dist/workbench/inspector.js.map +1 -1
  125. package/dist/workbench/standing.d.ts.map +1 -1
  126. package/dist/workbench/standing.js +1 -1
  127. package/dist/workbench/standing.js.map +1 -1
  128. package/package.json +6 -6
@@ -1,17 +1,18 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { layer } from "@graview/core";
3
3
  import { compareViewRevision } from "@graview/core/revisions";
4
4
  import { useGraview } from "@graview/react/provider";
5
5
  import { sectionUnderNow, viewPlaceOf } from "@graview/react/drawing";
6
- import { reviseSection } from "@graview/tools/section";
6
+ import { CONTROL_WORDS, describeChange, drawnFindings, lookOf, runDesignAgent } from "@graview/tools/section";
7
7
  import { interactionId } from "@graview/tools/inline-ai";
8
8
  import { retryingImport } from "@graview/core/retry";
9
9
  import { lazyModule } from "@graview/react/provider";
10
10
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
11
11
  import { boxOf, SectionOutline } from "./section-outline.js";
12
+ import { readDrawn, Redesigning } from "./better-bar.js";
13
+ import { BetterPanel } from "./better-panel.js";
12
14
  export { SectionOutline };
13
- import { nameParts, partLines, placeOfSection } from "./section-words.js";
14
- import { VISUALLY_HIDDEN } from "./primitives/measure.js";
15
+ import { aboutOf, nameParts, partLines, placeOfSection } from "./section-words.js";
15
16
  /*
16
17
  * WHAT WAS TOLD (Roadmap f-menu-learns): each revision offered is kept as a
17
18
  * telling β€” the words, the view's section it was said on, the slot that
@@ -23,10 +24,15 @@ const toldCard = /* @__PURE__ */ lazyModule(retryingImport(() => import("./told-
23
24
  const Rate = /* @__PURE__ */ toldCard.part((module, props) => _jsx(module.RateTelling, { ...props }), { quiet: true });
24
25
  import "./frame.js"; // A door: tied to what the page drew first (TIED DOORS, packages/primitives/src/frame.ts).
25
26
  export function SectionDrawn({ ask, open, close }) {
26
- if (ask)
27
+ if (ask?.part)
28
+ return _jsx(PageCard, { ask: ask, close: close }, ask.at);
29
+ if (ask?.section)
27
30
  return _jsx(SectionCard, { ask: ask, close: close }, ask.at);
28
31
  return open ? _jsx(SectionOutline, { elements: open }) : null;
29
32
  }
33
+ /* A built-in page's card (Roadmap f-make-any-page-better), fetched the first time it is asked for. */
34
+ const pageCard = /* @__PURE__ */ lazyModule(retryingImport(() => import("./page-better.js")));
35
+ const PageCard = /* @__PURE__ */ pageCard.part((module, props) => _jsx(module.PageCard, { ...props }), { what: "Make this better" });
30
36
  const WIDTH = 360;
31
37
  const GAP = 12;
32
38
  /** The card's place: beside the box where the window has room, else under it, else over it. */
@@ -116,17 +122,34 @@ function judged(handle, before, live, liveRefused, draft, source) {
116
122
  void handle;
117
123
  return { findings, notes };
118
124
  }
125
+ /** The labels a view's drawing shows for its records: each keyed row's words. */
126
+ function keyedLabels(root) {
127
+ if (!root)
128
+ return [];
129
+ return [...new Set([...root.querySelectorAll("[data-key]")].map((one) => (one.textContent ?? "").replace(/\s+/g, " ").trim()).filter((label) => label.length > 1 && label.length < 80))].slice(0, 40);
130
+ }
119
131
  export function SectionCard({ ask, close }) {
120
- const { ai, aiDesk, principal, keptViews } = useGraview();
132
+ const { ai, aiDesk, aiHere, session, principal, keptViews } = useGraview();
121
133
  const [section, setSection] = useState(ask.section);
122
134
  const place = placeOfSection(ask.section);
123
135
  const handle = useRef(viewPlaceOf(place));
124
136
  const draft = useRef(null);
125
- const card = useRef(null);
126
- const field = useRef(null);
127
- const [phase, setPhase] = useState(ask.mode === "highlight" ? { at: "highlighting" } : ask.mode === "describe" ? { at: "asking" } : { at: "working" });
128
- const [words, setWords] = useState("");
137
+ const [phase, setPhase] = useState(ask.mode === "highlight" ? { at: "highlighting" } : { at: "open" });
138
+ /* The view's code before any step, the page as it was read, the drafts that passed, the conversation and this turn's steps. */
129
139
  const original = useRef(undefined);
140
+ const before = useRef(null);
141
+ const steps = useRef([]);
142
+ const talk = useRef([]);
143
+ const events = useRef([]);
144
+ const since = useRef(Date.now());
145
+ const stopping = useRef(null);
146
+ const pointedAt = useRef(null);
147
+ /** What the conversation is about, where the person pointed at a part of the section. */
148
+ const aboutNow = useRef(null);
149
+ const [covering, setCovering] = useState(false);
150
+ const [comparing, setComparing] = useState(false);
151
+ const [, setTick] = useState(0);
152
+ const redraw = useCallback(() => setTick((one) => one + 1), []);
130
153
  /* The telling of the revision on offer, kept with the host's door (`ai.told`), and how it ended. */
131
154
  const [telling, setTelling] = useState(null);
132
155
  const tellingNow = useRef(null);
@@ -135,23 +158,32 @@ export function SectionCard({ ask, close }) {
135
158
  if (now && ai.told)
136
159
  void toldCard.load().then((module) => module.changeTelling(ai.told, now.id, { outcome }), () => undefined);
137
160
  }, [ai.told]);
138
- /** Keeps what was said and decided as a telling: offered as a draft ("previewed"), or refused. */
161
+ /** Keeps what was said and decided as a telling: offered as a draft ("previewed"), or not. */
139
162
  const tell = useCallback((words, on, refused, stages, ms, interaction) => {
140
163
  const keeps = ai.told;
141
164
  if (!keeps)
142
165
  return;
143
- const slot = stages.at(-1)?.stage === "thinking" ? "full" : stages.length > 0 ? "light" : undefined;
144
166
  void toldCard.load().then((module) => {
145
- const kept = module.keepTelling(keeps, { words, target: { on: "place", place: on.view }, decided: refused ? { type: "refusal" } : { type: "view", section: on.name }, ...(slot ? { slot } : {}), stages: stages.map((one) => ({ stage: one.stage === "thinking" ? "full" : "light", ms: one.ms, model: one.model })), ms, outcome: refused ? "abandoned" : "previewed", interaction }, principal);
167
+ const kept = module.keepTelling(keeps, { words, target: { on: "place", place: on.view }, decided: refused ? { type: "refusal" } : { type: "view", section: on.name }, slot: "full", stages: stages.map((one) => ({ stage: one.stage === "thinking" ? "full" : "light", ms: one.ms, model: one.model })), ms, outcome: refused ? "abandoned" : "previewed", interaction }, principal);
146
168
  tellingNow.current = kept ?? null;
147
169
  setTelling(kept ?? null);
148
170
  }, () => undefined);
149
171
  }, [ai.told, principal]);
150
172
  /* Put away: a draft not kept goes with the card, and the view stands as it was. */
151
173
  useEffect(() => () => {
174
+ stopping.current?.abort();
152
175
  draft.current?.dispose();
153
176
  draft.current = null;
154
177
  }, []);
178
+ /* GRAVIEW IS HERE WHILE IT WORKS, as it is beside the quick ask. */
179
+ const working = phase.at === "working";
180
+ useEffect(() => {
181
+ if (!working)
182
+ return;
183
+ aiHere({ participant: `agent:graview-for-${principal.id ?? "anyone"}:${session}`, kind: "agent", name: "Graview", ...(principal.id ? { onBehalfOf: principal.id } : {}), over: null, robot: { at: null, mode: "thinking" } });
184
+ return () => aiHere(null);
185
+ // eslint-disable-next-line react-hooks/exhaustive-deps
186
+ }, [working]);
155
187
  const entryOf = useCallback((said, outcome, stages, extra = {}) => ({
156
188
  id: interactionId(),
157
189
  at: new Date().toISOString(),
@@ -165,95 +197,196 @@ export function SectionCard({ ask, close }) {
165
197
  usage: stages.flatMap((one) => (one.usage ? [{ tier: one.stage === "light" ? "fast" : "thinking", model: one.model, inputTokens: one.usage.inputTokens, outputTokens: one.usage.outputTokens }] : [])),
166
198
  ...extra,
167
199
  }), [principal, section.view]);
168
- /** Revise the section: asked, composed whole, judged as a draft in the view's place, offered or refused. */
169
- const revise = useCallback(async (mode, said, pointed, on = section) => {
200
+ /** The draft drawn in the view's place: the code given, drawn again. */
201
+ const drawDraft = useCallback(async (source) => {
202
+ draft.current?.dispose();
170
203
  const view = handle.current;
171
- const asked = mode === "just" ? `Make β€œ${on.name}” better` : `Make β€œ${on.name}” better: ${said}`;
172
- if (!view)
173
- return setPhase({ at: "refused", said: "This view is not drawn here any more." });
174
- setPhase({ at: "working", ...(pointed ? { pointed } : {}) });
175
- const before = await view.source();
176
- if (before === undefined)
177
- return setPhase({ at: "refused", said: "This view is a whole worker script, and only a plain script's section can be revised here." });
178
- original.current ??= before;
179
- const started = Date.now();
180
- const live = view.drawing();
204
+ const drawn = view.draft(source);
205
+ draft.current = drawn;
206
+ await drawn.drawn;
207
+ return drawn;
208
+ }, []);
209
+ /** The view as the person last saw it: the newest draft that passed, or the view as it was. */
210
+ const showLatest = useCallback(async () => {
211
+ const latest = steps.current.at(-1);
212
+ setComparing(false);
213
+ if (latest)
214
+ await drawDraft(latest.source);
215
+ else {
216
+ draft.current?.dispose();
217
+ draft.current = null;
218
+ }
219
+ }, [drawDraft]);
220
+ const say = useCallback((who, words) => {
221
+ talk.current.push({ who, said: words });
222
+ redraw();
223
+ }, [redraw]);
224
+ const askedWords = () => talk.current.filter((one) => one.who === "person").map((one) => one.said).join(" ");
225
+ /** One turn of the agent on the section: it looks, drafts the whole script with the section changed, is told what the checks found, and drafts again. */
226
+ const askFor = useCallback(async (words) => {
227
+ const view = handle.current;
228
+ if (!view || phase.at === "working")
229
+ return;
230
+ const pointed = pointedAt.current;
231
+ pointedAt.current = null;
232
+ // The person's words as they typed them: what they are about is said beside them, never glued on.
233
+ if (pointed)
234
+ aboutNow.current = pointed;
235
+ say("person", words);
236
+ events.current = [];
237
+ since.current = Date.now();
238
+ setPhase({ at: "working" });
239
+ const live = await view.source();
240
+ if (live === undefined) {
241
+ say("graview", "This view can't be changed here.");
242
+ return setPhase({ at: "open" });
243
+ }
244
+ original.current ??= live;
245
+ const liveDrawing = view.drawing();
181
246
  const liveRefused = view.refused();
182
- const theme = await view.theme().catch(() => ({}));
183
- const result = await reviseSection({
247
+ before.current ??= readDrawn(liveDrawing.root?.parentElement ?? liveDrawing.root ?? place, keyedLabels(liveDrawing.root));
248
+ const was = before.current;
249
+ const stop = new AbortController();
250
+ stopping.current = stop;
251
+ const started = Date.now();
252
+ const turnBegan = steps.current.length;
253
+ const result = await runDesignAgent({
184
254
  ai,
185
- source: before,
186
- section: on.name,
187
- mode,
188
- ...(said ? { words: said } : {}),
189
- ...(pointed ? { highlighted: [pointed.name, ...pointed.lines] } : {}),
190
- theme,
191
- shows: shownBy(on.elements),
192
- judge: async (source) => {
193
- draft.current?.dispose();
194
- draft.current = null;
195
- const refused = await view.check(source);
196
- if (refused.length > 0)
197
- return refused;
198
- const drawn = view.draft(source);
199
- draft.current = drawn;
200
- const { failed } = await drawn.drawn;
201
- if (failed)
202
- return [`The view did not draw: ${failed}`];
203
- await new Promise((settled) => requestAnimationFrame(() => requestAnimationFrame(settled)));
204
- return judged(view, before, live, liveRefused, drawn, source).findings;
255
+ part: { name: section.name, slot: "place" },
256
+ manifest: { name: view.name, attach: "view", cardinality: "many" },
257
+ talk: talk.current,
258
+ keep: { title: "", headings: was.headings, records: was.records.map((one) => one.label), says: Object.fromEntries(was.records.flatMap((one) => (one.facts?.length || one.under ? [[one.label, { ...(one.facts?.length ? { shows: one.facts } : {}), ...(one.under ? { under: one.under } : {}) }]] : []))) },
259
+ current: steps.current.at(-1)?.source ?? live,
260
+ section: section.name,
261
+ noun: "records",
262
+ ...(aboutNow.current ? { about: aboutNow.current } : {}),
263
+ host: {
264
+ look: () => lookOf(readDrawn(view.drawing().root?.parentElement ?? place, was.records.map((one) => one.label))),
265
+ read: () => ({ shows: shownBy(section.elements) }),
266
+ draft: async (source) => {
267
+ setCovering(true);
268
+ try {
269
+ const refused = await view.check(source);
270
+ if (refused.length > 0) {
271
+ await showLatest();
272
+ return { findings: refused, drew: undefined };
273
+ }
274
+ const drawn = await drawDraft(source);
275
+ const { failed } = await drawn.drawn;
276
+ if (failed) {
277
+ await showLatest();
278
+ return { findings: [`The view did not draw: ${failed}`], drew: undefined };
279
+ }
280
+ await new Promise((settled) => requestAnimationFrame(() => requestAnimationFrame(settled)));
281
+ const root = drawn.drawing().root;
282
+ const now = readDrawn(root?.parentElement ?? root, was.records.map((one) => one.label));
283
+ // And every control it draws pressed, headless and nothing applied: one that does nothing, throws or is refused goes back to the agent.
284
+ const pressed = view.presses ? await view.presses(source).catch(() => undefined) : undefined;
285
+ const findings = [...judged(view, original.current, liveDrawing, liveRefused, drawn, source).findings, ...drawnFindings(was, now, { asked: askedWords() }), ...(pressed?.problems ?? []).slice(0, 5).map((one) => `${one.control}: ${one.says}`)];
286
+ if (findings.length > 0)
287
+ await showLatest();
288
+ else {
289
+ steps.current.push({ source, said: describeChange(section.name, was, now) ?? `β€œ${section.name}” has changed. Compare Before and After.`, entry: entryOf(`Make β€œ${section.name}” better: ${askedWords()}`, "offered", []) });
290
+ setComparing(false);
291
+ }
292
+ return { findings, drew: lookOf(now) };
293
+ }
294
+ finally {
295
+ setCovering(false);
296
+ }
297
+ },
298
+ },
299
+ onEvent: (event) => {
300
+ // The words as the model writes them stand in for the step it is on, and give way to the step once it is said.
301
+ if (events.current.at(-1)?.kind === "streaming")
302
+ events.current.pop();
303
+ events.current.push(event);
304
+ redraw();
205
305
  },
306
+ signal: stop.signal,
206
307
  });
207
- if (!result.ok) {
208
- draft.current?.dispose();
209
- draft.current = null;
210
- const failed = entryOf(asked, "failed", result.stages, { failed: result.refused });
211
- aiDesk.record(failed);
212
- tell(said || asked, on, true, result.stages, Date.now() - started, failed.id);
213
- return setPhase({ at: "refused", said: result.refused });
214
- }
215
- const notes = draft.current ? judged(view, before, live, liveRefused, draft.current, result.source).notes : [];
216
- const entry = entryOf(asked, "offered", result.stages);
217
- tell(said || asked, on, false, result.stages, Date.now() - started, entry.id);
218
- setPhase({ at: "draft", source: result.source, said: result.said, notes: result.repaired ? ["Repaired once after the first try broke the view.", ...notes] : notes, entry });
219
- }, [ai, aiDesk, entryOf, section, tell]);
220
- /* Just do it: at once. */
308
+ if (stopping.current !== stop)
309
+ return;
310
+ stopping.current = null;
311
+ if (events.current.at(-1)?.kind === "streaming")
312
+ events.current.pop();
313
+ const offered = steps.current.length > turnBegan;
314
+ const entry = entryOf(`Make β€œ${section.name}” better: ${askedWords()}`, offered ? "offered" : "failed", result.stages, offered ? {} : { failed: result.last?.findings.join(" ") ?? result.said });
315
+ aiDesk.record(entry);
316
+ tell(askedWords(), section, !offered, result.stages, Date.now() - started, entry.id);
317
+ // What a control does is the press check's to say, never the model's.
318
+ say("graview", result.passed && CONTROL_WORDS.test(result.said) ? "Here it is on the page." : result.said);
319
+ setPhase(steps.current.length > 0 ? { at: "draft" } : { at: "open" });
320
+ }, [ai, aiDesk, drawDraft, entryOf, phase.at, place, redraw, say, section, showLatest, tell]);
321
+ /** Stop: at once β€” the turn's drafts gone, the view as it was before it began. */
322
+ const stopTurn = useCallback(() => {
323
+ if (phase.at !== "working")
324
+ return;
325
+ const stop = stopping.current;
326
+ stopping.current = null;
327
+ stop?.abort();
328
+ setCovering(false);
329
+ const began = new Date(since.current).toISOString();
330
+ steps.current = steps.current.filter((one) => one.entry.at < began);
331
+ void showLatest();
332
+ say("graview", "Stopped. The view is as it was.");
333
+ setPhase(steps.current.length > 0 ? { at: "draft" } : { at: "open" });
334
+ }, [phase.at, say, showLatest]);
335
+ /* Words that came with the ask β€” typed in the menu, or a suggestion taken β€” are asked at once. */
221
336
  const began = useRef(false);
222
337
  useEffect(() => {
223
- if (ask.mode !== "just" || began.current)
338
+ if (began.current || !(ask.mode === "just" || (ask.mode === "describe" && ask.words)))
224
339
  return;
225
340
  began.current = true;
226
- void revise("just", "");
227
- }, [ask.mode, revise]);
341
+ void askFor(ask.mode === "just" ? "Make it easier to read at a glance" : ask.words);
342
+ }, [ask.mode, ask.words, askFor]);
343
+ const current = steps.current.at(-1);
344
+ const compare = useCallback(async (originalShown) => {
345
+ if (!current || originalShown === comparing)
346
+ return;
347
+ setComparing(originalShown);
348
+ if (originalShown) {
349
+ draft.current?.dispose();
350
+ draft.current = null;
351
+ }
352
+ else
353
+ await drawDraft(current.source);
354
+ }, [current, comparing, drawDraft]);
228
355
  const keep = useCallback(async () => {
229
- if (phase.at !== "draft" || !keptViews?.revise || !handle.current)
356
+ const latest = steps.current.at(-1);
357
+ if (phase.at !== "draft" || !latest || !keptViews?.revise || !handle.current)
230
358
  return;
231
359
  const view = handle.current;
232
- const before = (await view.source()) ?? "";
233
- setPhase({ at: "keeping", source: phase.source, entry: phase.entry });
360
+ const was = (await view.source()) ?? "";
361
+ setPhase({ at: "keeping" });
234
362
  try {
235
- const answer = await keptViews.revise({ name: view.name, title: view.title, source: phase.source, before, section: section.name, said: phase.said });
363
+ const answer = await keptViews.revise({ name: view.name, title: view.title, source: latest.source, before: was, section: section.name, said: latest.said });
236
364
  draft.current?.dispose();
237
365
  draft.current = null;
238
- view.draw(phase.source);
239
- const entry = { ...phase.entry, outcome: "accepted" };
240
- aiDesk.record(entry);
366
+ view.draw(latest.source);
367
+ aiDesk.record({ ...latest.entry, outcome: "accepted" });
241
368
  ended("accepted");
242
- setPhase({ at: "kept", source: before, ...(answer && answer.note ? { note: answer.note } : {}), answer, entry });
369
+ steps.current = [];
370
+ setPhase({ at: "kept", source: was, ...(answer && answer.note ? { note: answer.note } : {}), answer, entry: latest.entry });
243
371
  }
244
- catch (error) {
245
- setPhase({ at: "refused", said: error instanceof Error ? error.message : "The host did not keep it." });
372
+ catch {
373
+ say("graview", "It couldn't be kept.");
374
+ setPhase({ at: "draft" });
246
375
  }
247
- }, [phase, keptViews, aiDesk, section.name, ended]);
376
+ }, [phase, keptViews, aiDesk, section.name, ended, say]);
248
377
  const discard = useCallback(() => {
249
378
  draft.current?.dispose();
250
379
  draft.current = null;
251
- if (phase.at === "draft") {
252
- aiDesk.record({ ...phase.entry, outcome: "dismissed" });
380
+ const latest = steps.current.at(-1);
381
+ if (latest) {
382
+ aiDesk.record({ ...latest.entry, outcome: "dismissed" });
253
383
  ended("declined");
254
384
  }
255
- close();
256
- }, [phase, aiDesk, close, ended]);
385
+ steps.current = [];
386
+ setComparing(false);
387
+ say("graview", "Put back as it was.");
388
+ setPhase({ at: "open" });
389
+ }, [aiDesk, ended, say]);
257
390
  const undo = useCallback(async () => {
258
391
  if (phase.at !== "kept" || !handle.current)
259
392
  return;
@@ -268,61 +401,44 @@ export function SectionCard({ ask, close }) {
268
401
  aiDesk.record({ ...phase.entry, outcome: "dismissed" });
269
402
  // A quiet sign, never a πŸ‘Ž.
270
403
  ended("undone");
404
+ say("graview", "Undone. The view is as it was.");
271
405
  setPhase({ at: "undone" });
272
406
  }
273
- catch (error) {
274
- setPhase({ at: "refused", said: error instanceof Error ? error.message : "The host did not put it back." });
407
+ catch {
408
+ say("graview", "It couldn't be put back.");
275
409
  }
276
- }, [phase, keptViews, aiDesk, section.name, ended]);
277
- /* Placed beside the section, kept there as the page moves. */
278
- useLayoutEffect(() => {
279
- let frame = 0;
280
- const follow = () => {
281
- if (card.current) {
282
- const drafted = place?.querySelector("[data-graview-section-draft]");
283
- placeBeside(card.current, drafted ? boxOf([drafted]) : boxOf(section.elements) ?? (place ? boxOf([place]) : null));
284
- }
285
- frame = requestAnimationFrame(follow);
286
- };
287
- follow();
288
- return () => cancelAnimationFrame(frame);
289
- }, [section, place]);
290
- /* The keyboard: in the field when it asks, else on the card's first button. */
291
- useEffect(() => {
292
- if (phase.at === "asking")
293
- field.current?.focus();
294
- else if (phase.at !== "highlighting")
295
- card.current?.querySelector("button[data-first]")?.focus();
296
- }, [phase.at]);
297
- /* Escape puts it away: a draft is discarded, the keyboard goes back where it was. */
410
+ }, [phase, keptViews, aiDesk, section.name, ended, say]);
411
+ /* Escape: Stop while it works, else put it away. */
298
412
  useEffect(() => {
299
413
  const key = (event) => {
300
- if (event.key !== "Escape")
414
+ if (event.key !== "Escape" || phase.at === "highlighting")
301
415
  return;
302
416
  event.preventDefault();
303
- discard();
417
+ if (phase.at === "working")
418
+ return stopTurn();
419
+ if (steps.current.length > 0)
420
+ ended("declined");
421
+ close();
304
422
  ask.returnTo?.focus?.();
305
423
  };
306
424
  document.addEventListener("keydown", key);
307
425
  return () => document.removeEventListener("keydown", key);
308
- }, [discard, ask.returnTo]);
309
- const pointed = phase.at === "asking" || phase.at === "working" ? phase.pointed : undefined;
426
+ }, [phase.at, stopTurn, close, ended, ask.returnTo]);
310
427
  if (phase.at === "highlighting") {
311
- return (_jsx(Highlighter, { place: place, section: section, cancel: close, chose: (elements) => {
428
+ return (_jsx(Highlighter, { box: place, root: place?.querySelector("[data-worker-view]")?.shadowRoot?.querySelector("[data-graview-view-root]") ?? undefined, whole: section.elements, cancel: () => setPhase({ at: "open" }), chose: (elements) => {
312
429
  const first = elements[0];
313
430
  const under = first ? sectionUnderNow(first) : undefined;
314
- const on = under ?? section;
315
- setSection(on);
316
- setPhase({ at: "asking", pointed: { name: nameParts(elements.map(readPart)), lines: partLines(elements.map(readPart)) } });
431
+ setSection(under ?? section);
432
+ pointedAt.current = { name: aboutOf(elements) ?? nameParts(elements.map(readPart)), lines: partLines(elements.map(readPart)) };
433
+ setPhase({ at: "open" });
317
434
  } }));
318
435
  }
319
- const drafted = phase.at === "draft" || phase.at === "keeping";
320
- const outlined = drafted ? [...(place?.querySelectorAll("[data-graview-section-draft]") ?? [])] : phase.at === "kept" || phase.at === "undone" ? [] : section.elements;
321
- return (_jsxs(_Fragment, { children: [outlined.length > 0 ? _jsx(SectionOutline, { elements: outlined, label: drafted ? "Draft" : undefined }) : null, _jsxs("div", { ref: card, role: "dialog", "aria-label": `Make β€œ${section.name}” better`, "data-testid": "section-card", "data-section-phase": phase.at, "data-graview-popover": "", style: CARD, children: [_jsxs("p", { style: HEAD, children: ["Make ", _jsxs("strong", { children: ["\u201C", section.name, "\u201D"] }), " better"] }), pointed ? (_jsxs("p", { "data-testid": "section-pointed", style: MUTED, children: ["You pointed at ", pointed.name, "."] })) : null, phase.at === "asking" ? (_jsxs("form", { onSubmit: (event) => {
322
- event.preventDefault();
323
- if (words.trim())
324
- void revise(ask.mode, words.trim(), phase.pointed);
325
- }, style: { display: "grid", gap: 8 }, children: [_jsx("label", { style: VISUALLY_HIDDEN, htmlFor: "graview-section-words", children: phase.pointed ? "What should change here?" : "What should change?" }), _jsx("input", { id: "graview-section-words", ref: field, "data-testid": "section-words", value: words, onChange: (event) => setWords(event.target.value), placeholder: phase.pointed ? "What should change here?" : "What should change?", style: FIELD }), _jsxs("div", { style: ROW, children: [_jsx("button", { type: "submit", "data-first": "", disabled: !words.trim(), style: PRIMARY, children: "Revise" }), _jsx("button", { type: "button", onClick: close, style: QUIET, children: "Cancel" })] })] })) : null, _jsx("p", { role: "status", "aria-live": "polite", "data-testid": "section-status", style: phase.at === "working" || phase.at === "keeping" ? THINKING : MUTED, children: phase.at === "working" ? `Revising β€œ${section.name}”…` : phase.at === "keeping" ? "Keeping it…" : phase.at === "draft" ? phase.said : phase.at === "kept" ? `Kept.${phase.note ? ` ${phase.note}` : ""}` : phase.at === "undone" ? "Put back as it was." : phase.at === "refused" ? phase.said : "" }), phase.at === "draft" ? (_jsxs(_Fragment, { children: [_jsx("p", { "data-testid": "section-draft-line", style: MUTED, children: "Draft \u2014 shown in place, not kept." }), phase.notes.map((note) => (_jsx("p", { "data-testid": "section-note", style: MUTED, children: note }, note))), _jsxs("div", { style: ROW, children: [_jsx("button", { type: "button", "data-first": "", "data-testid": "section-keep", onClick: () => void keep(), style: PRIMARY, children: "Keep" }), _jsx("button", { type: "button", "data-testid": "section-discard", onClick: discard, style: QUIET, children: "Discard" })] })] })) : null, phase.at === "kept" ? (_jsxs("div", { style: ROW, children: [_jsx("button", { type: "button", "data-first": "", "data-testid": "section-undo", onClick: () => void undo(), style: QUIET, children: "Undo" }), _jsx("button", { type: "button", onClick: close, style: QUIET, children: "Close" })] })) : null, phase.at === "refused" || phase.at === "undone" ? (_jsx("div", { style: ROW, children: _jsx("button", { type: "button", "data-first": "", onClick: close, style: QUIET, children: "Close" }) })) : null, telling && ai.told && phase.at !== "working" && phase.at !== "asking" ? _jsx(Rate, { told: ai.told, telling: telling }) : null] })] }));
436
+ return (_jsxs(_Fragment, { children: [section.elements.length > 0 && phase.at === "open" && talk.current.length === 0 ? _jsx(SectionOutline, { elements: section.elements }) : null, covering ? _jsx(Redesigning, { over: place, name: section.name, said: `Graview is drafting β€œ${section.name}”…` }) : null, _jsx(BetterPanel, { name: section.name, scope: `This changes how β€œ${section.name}” looks for everyone in this app.`, phase: phase.at, talk: talk.current, steps: events.current, since: since.current, working: phase.at === "working" || phase.at === "keeping", ...(current && phase.at === "draft" ? { draft: { said: current.said, before: comparing } } : {}), ...(phase.at === "kept" ? { kept: { ...(phase.note ? { note: phase.note } : {}), undo: () => void undo() } } : {}), about: (pointedAt.current ?? aboutNow.current)?.name ?? section.name, ask: (words) => void askFor(words), just: () => void askFor("Make it easier to read at a glance"), point: () => setPhase({ at: "highlighting" }), stop: stopTurn, compare: (originalShown) => void compare(originalShown), keep: () => void keep(), discard: discard, close: () => {
437
+ stopping.current?.abort();
438
+ if (steps.current.length > 0)
439
+ ended("declined");
440
+ close();
441
+ }, children: telling && ai.told && phase.at !== "working" ? _jsx(Rate, { told: ai.told, telling: telling }) : null })] }));
326
442
  }
327
443
  /**
328
444
  * HIGHLIGHT AREA: an overlay over the view. A drag draws a rectangle and
@@ -330,10 +446,9 @@ export function SectionCard({ ask, close }) {
330
446
  * or "Cancel", puts it away; "The whole section" takes the section itself
331
447
  * for a keyboard.
332
448
  */
333
- function Highlighter({ place, section, cancel, chose }) {
449
+ export function Highlighter({ box: place, root, whole, wholeSaid = "The whole section", cancel, chose }) {
334
450
  const cover = useRef(null);
335
451
  const [drag, setDrag] = useState(null);
336
- const root = place?.querySelector("[data-worker-view]")?.shadowRoot?.querySelector("[data-graview-view-root]") ?? undefined;
337
452
  useLayoutEffect(() => {
338
453
  let frame = 0;
339
454
  const follow = () => {
@@ -377,8 +492,9 @@ function Highlighter({ place, section, cancel, chose }) {
377
492
  event.preventDefault();
378
493
  event.currentTarget.setPointerCapture?.(event.pointerId);
379
494
  setDrag({ x0: event.clientX, y0: event.clientY, x1: event.clientX, y1: event.clientY });
380
- }, onPointerMove: (event) => setDrag((was) => (was ? { ...was, x1: event.clientX, y1: event.clientY } : was)), onPointerUp: end, onPointerCancel: () => setDrag(null), style: COVER, children: [_jsxs("p", { style: COVER_HINT, children: ["Drag over the part to change, or click one \u00B7", " ", _jsx("button", { type: "button", onPointerDown: (event) => event.stopPropagation(), onClick: () => chose(section.elements), style: LINK, children: "The whole section" }), " ", "\u00B7", " ", _jsx("button", { type: "button", onPointerDown: (event) => event.stopPropagation(), onClick: cancel, style: LINK, children: "Cancel" })] }), box ? _jsx("div", { "data-testid": "section-drag", style: { ...DRAG, left: box.left - (cover.current?.getBoundingClientRect().left ?? 0), top: box.top - (cover.current?.getBoundingClientRect().top ?? 0), width: box.width, height: box.height } }) : null] }));
495
+ }, onPointerMove: (event) => setDrag((was) => (was ? { ...was, x1: event.clientX, y1: event.clientY } : was)), onPointerUp: end, onPointerCancel: () => setDrag(null), style: COVER, children: [_jsxs("p", { style: COVER_HINT, children: ["Drag over the part to change, or click one \u00B7", " ", _jsx("button", { type: "button", onPointerDown: (event) => event.stopPropagation(), onClick: () => chose(whole), style: LINK, children: wholeSaid }), " ", "\u00B7", " ", _jsx("button", { type: "button", onPointerDown: (event) => event.stopPropagation(), onClick: cancel, style: LINK, children: "Cancel" })] }), box ? _jsx("div", { "data-testid": "section-drag", style: { ...DRAG, left: box.left - (cover.current?.getBoundingClientRect().left ?? 0), top: box.top - (cover.current?.getBoundingClientRect().top ?? 0), width: box.width, height: box.height } }) : null] }));
381
496
  }
497
+ export { readPart, placeBeside };
382
498
  const CARD = {
383
499
  position: "fixed",
384
500
  zIndex: layer("popover"),
@@ -408,4 +524,6 @@ const PAD = 12;
408
524
  const COVER = { userSelect: "none", WebkitUserSelect: "none", position: "fixed", zIndex: layer("popover"), boxSizing: "border-box", cursor: "crosshair", touchAction: "none", outline: "2px dashed var(--graview-accent)", background: "color-mix(in srgb, var(--graview-accent) 6%, transparent)" };
409
525
  const COVER_HINT = { position: "absolute", left: 8, top: 8, margin: 0, padding: "4px 10px", borderRadius: 999, fontSize: "0.8125rem", color: "var(--graview-ink)", background: "var(--graview-float, var(--graview-panel))", border: "1px solid var(--graview-edge)", boxShadow: "var(--graview-lift-low)", cursor: "default" };
410
526
  const DRAG = { position: "absolute", border: "2px solid var(--graview-accent)", background: "color-mix(in srgb, var(--graview-accent) 14%, transparent)", borderRadius: 4, pointerEvents: "none" };
527
+ /** The card's look, shared with a built-in page's card. */
528
+ export const LOOK = { CARD, HEAD, MUTED, THINKING, ROW, FIELD, PRIMARY, QUIET };
411
529
  //# sourceMappingURL=section-better.js.map