@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.
- package/dist/app-bar.d.ts +88 -15
- package/dist/app-bar.d.ts.map +1 -1
- package/dist/app-bar.js +176 -130
- package/dist/app-bar.js.map +1 -1
- package/dist/app-menu.d.ts +9 -0
- package/dist/app-menu.d.ts.map +1 -0
- package/dist/app-menu.js +60 -0
- package/dist/app-menu.js.map +1 -0
- package/dist/bar-activity.d.ts +5 -9
- package/dist/bar-activity.d.ts.map +1 -1
- package/dist/bar-activity.js +24 -7
- package/dist/bar-activity.js.map +1 -1
- package/dist/bar-panes.d.ts +5 -5
- package/dist/bar-panes.d.ts.map +1 -1
- package/dist/bar-panes.js +9 -5
- package/dist/bar-panes.js.map +1 -1
- package/dist/bar-suggested.d.ts +9 -1
- package/dist/bar-suggested.d.ts.map +1 -1
- package/dist/bar-suggested.js +11 -17
- package/dist/bar-suggested.js.map +1 -1
- package/dist/better-bar.d.ts +65 -0
- package/dist/better-bar.d.ts.map +1 -0
- package/dist/better-bar.js +252 -0
- package/dist/better-bar.js.map +1 -0
- package/dist/better-panel.d.ts +50 -0
- package/dist/better-panel.d.ts.map +1 -0
- package/dist/better-panel.js +151 -0
- package/dist/better-panel.js.map +1 -0
- package/dist/editable.d.ts +8 -1
- package/dist/editable.d.ts.map +1 -1
- package/dist/editable.js +140 -16
- package/dist/editable.js.map +1 -1
- package/dist/first-look-door.d.ts +2 -2
- package/dist/first-look-door.js +2 -2
- package/dist/first-look.d.ts.map +1 -1
- package/dist/first-look.js +9 -7
- package/dist/first-look.js.map +1 -1
- package/dist/frame.d.ts +2 -2
- package/dist/frame.d.ts.map +1 -1
- package/dist/frame.js +1 -1
- package/dist/frame.js.map +1 -1
- package/dist/inbox-pane.d.ts +31 -0
- package/dist/inbox-pane.d.ts.map +1 -0
- package/dist/inbox-pane.js +89 -0
- package/dist/inbox-pane.js.map +1 -0
- package/dist/inbox.d.ts +17 -0
- package/dist/inbox.d.ts.map +1 -0
- package/dist/inbox.js +84 -0
- package/dist/inbox.js.map +1 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/menu-suggested.d.ts +54 -0
- package/dist/menu-suggested.d.ts.map +1 -0
- package/dist/menu-suggested.js +140 -0
- package/dist/menu-suggested.js.map +1 -0
- package/dist/menu-told-door.js +1 -1
- package/dist/menu-told-door.js.map +1 -1
- package/dist/menu-told.d.ts +7 -0
- package/dist/menu-told.d.ts.map +1 -1
- package/dist/menu-told.js +24 -0
- package/dist/menu-told.js.map +1 -1
- package/dist/notify-sheet.js +2 -2
- package/dist/notify-sheet.js.map +1 -1
- package/dist/page-better.d.ts +13 -0
- package/dist/page-better.d.ts.map +1 -0
- package/dist/page-better.js +714 -0
- package/dist/page-better.js.map +1 -0
- package/dist/page-parts.d.ts +41 -0
- package/dist/page-parts.d.ts.map +1 -0
- package/dist/page-parts.js +100 -0
- package/dist/page-parts.js.map +1 -0
- package/dist/pages.d.ts +2 -3
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +2 -3
- package/dist/pages.js.map +1 -1
- package/dist/profile.d.ts +1 -10
- package/dist/profile.d.ts.map +1 -1
- package/dist/profile.js +2 -3
- package/dist/profile.js.map +1 -1
- package/dist/scene-bar.d.ts +1 -1
- package/dist/scene-bar.d.ts.map +1 -1
- package/dist/scene-bar.js +4 -7
- package/dist/scene-bar.js.map +1 -1
- package/dist/section-better.d.ts +47 -9
- package/dist/section-better.d.ts.map +1 -1
- package/dist/section-better.js +239 -121
- package/dist/section-better.js.map +1 -1
- package/dist/section-door.d.ts +7 -0
- package/dist/section-door.d.ts.map +1 -1
- package/dist/section-door.js +39 -5
- package/dist/section-door.js.map +1 -1
- package/dist/section-menu.d.ts +23 -9
- package/dist/section-menu.d.ts.map +1 -1
- package/dist/section-menu.js +176 -23
- package/dist/section-menu.js.map +1 -1
- package/dist/section-words.d.ts +8 -0
- package/dist/section-words.d.ts.map +1 -1
- package/dist/section-words.js +25 -0
- package/dist/section-words.js.map +1 -1
- package/dist/shell.d.ts +5 -1
- package/dist/shell.d.ts.map +1 -1
- package/dist/shell.js +4 -2
- package/dist/shell.js.map +1 -1
- package/dist/suggested.d.ts +21 -10
- package/dist/suggested.d.ts.map +1 -1
- package/dist/suggested.js +43 -64
- package/dist/suggested.js.map +1 -1
- package/dist/told-ui.d.ts +1 -1
- package/dist/told-ui.d.ts.map +1 -1
- package/dist/told-ui.js +1 -1
- package/dist/told-ui.js.map +1 -1
- package/dist/when-words.d.ts +21 -0
- package/dist/when-words.d.ts.map +1 -0
- package/dist/when-words.js +43 -0
- package/dist/when-words.js.map +1 -0
- package/dist/workbench/activity.d.ts +44 -0
- package/dist/workbench/activity.d.ts.map +1 -1
- package/dist/workbench/activity.js +74 -59
- package/dist/workbench/activity.js.map +1 -1
- package/dist/workbench/inspector.d.ts.map +1 -1
- package/dist/workbench/inspector.js +170 -31
- package/dist/workbench/inspector.js.map +1 -1
- package/dist/workbench/standing.d.ts.map +1 -1
- package/dist/workbench/standing.js +1 -1
- package/dist/workbench/standing.js.map +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import {} from "@graview/core";
|
|
3
|
+
import { useGraview } from "@graview/react/provider";
|
|
4
|
+
import { viewPlaceOf } from "@graview/react/drawing";
|
|
5
|
+
import { CONTROL_WORDS, describeChange, draftPageSpec, drawnFindings, forkManifest, IT_DOES_NOT_WORK, lookOf, mayChangeDeclaration, pressFindings, pressSaid, runDesignAgent, specCanSay } from "@graview/tools/section";
|
|
6
|
+
import { interactionId } from "@graview/tools/inline-ai";
|
|
7
|
+
import { useCallback, useEffect, useMemo, useSyncExternalStore } from "react";
|
|
8
|
+
import { Highlighter, readPart } from "./section-better.js";
|
|
9
|
+
import { readDrawn, Redesigning } from "./better-bar.js";
|
|
10
|
+
import { BetterPanel } from "./better-panel.js";
|
|
11
|
+
import { aboutOf, nameParts, partLines } from "./section-words.js";
|
|
12
|
+
import "./frame.js"; // A door: tied to what the page drew first (TIED DOORS, packages/primitives/src/frame.ts).
|
|
13
|
+
const words = (text) => (text ?? "").replace(/\s+/g, " ").trim();
|
|
14
|
+
const frame = () => new Promise((done) => requestAnimationFrame(() => done()));
|
|
15
|
+
const wait = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
16
|
+
/** A heading's own words, without a count or a disclosure's arrow drawn beside them: "Someday", from "▾ Someday · 3". */
|
|
17
|
+
function headingWords(heading) {
|
|
18
|
+
const copy = heading.cloneNode(true);
|
|
19
|
+
for (const span of copy.querySelectorAll("span"))
|
|
20
|
+
span.remove();
|
|
21
|
+
return words(copy.textContent).replace(/^[▸▾]\s*/, "");
|
|
22
|
+
}
|
|
23
|
+
/** What a page shows, as a fingerprint: its words and its shape. A draft that leaves it the same changed nothing drawn here. */
|
|
24
|
+
export function drawnSignature(main) {
|
|
25
|
+
if (!main || !main.isConnected)
|
|
26
|
+
return "";
|
|
27
|
+
const text = main.innerText ?? main.textContent ?? "";
|
|
28
|
+
return `${main.querySelectorAll("*").length}|${words(text)}`;
|
|
29
|
+
}
|
|
30
|
+
/** The page as it stands, a line each: its headings, its rows and cards, its facts — what a view of it is seeded from. */
|
|
31
|
+
export function outlineOf(main, most = 80) {
|
|
32
|
+
if (!main)
|
|
33
|
+
return "";
|
|
34
|
+
const lines = [];
|
|
35
|
+
const taken = new Set();
|
|
36
|
+
const ROW = 'li, tr, article, [data-graview-listed], [data-graview-record], [data-graview-field]';
|
|
37
|
+
for (const element of main.querySelectorAll(`h1, h2, h3, h4, ${ROW}`)) {
|
|
38
|
+
if (lines.length >= most)
|
|
39
|
+
break;
|
|
40
|
+
if ([...taken].some((one) => one.contains(element)))
|
|
41
|
+
continue;
|
|
42
|
+
if (element.closest("[data-graview-popover], [hidden], nav, [aria-hidden='true']"))
|
|
43
|
+
continue;
|
|
44
|
+
const heading = /^h([1-4])$/i.exec(element.localName);
|
|
45
|
+
const said = (heading ? headingWords(element) : words(element.innerText ?? element.textContent)).slice(0, 140);
|
|
46
|
+
if (!said)
|
|
47
|
+
continue;
|
|
48
|
+
if (heading)
|
|
49
|
+
lines.push(`${"#".repeat(Number(heading[1]))} ${said}`);
|
|
50
|
+
else {
|
|
51
|
+
const field = element.getAttribute("data-graview-field");
|
|
52
|
+
lines.push(field ? `${field}: ${said}` : `- ${said}`);
|
|
53
|
+
taken.add(element);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return lines.join("\n");
|
|
57
|
+
}
|
|
58
|
+
/** The records a view of the page reads, a few of each, as the view is handed them. */
|
|
59
|
+
function dataOf(here, part) {
|
|
60
|
+
const { store } = here;
|
|
61
|
+
const plain = (node) => Object.fromEntries(Object.entries(node).filter(([, value]) => value !== undefined && typeof value !== "object").map(([key, value]) => [key, typeof value === "string" ? value.slice(0, 80) : value]));
|
|
62
|
+
const kinds = part.kind ? [part.kind] : store.schema.kinds.slice(0, 6);
|
|
63
|
+
return {
|
|
64
|
+
kinds: kinds.map((kind) => {
|
|
65
|
+
const nodes = store.graph.nodesOfKind(kind);
|
|
66
|
+
const definition = store.schema.tryDefinition(kind);
|
|
67
|
+
return { kind, count: nodes.length, fields: Object.keys(definition?.fields?.shape ?? {}), relations: Object.keys(definition?.edges ?? {}), some: (part.record ? nodes.filter((node) => node.id === part.record) : nodes.slice(0, 3)).map(plain) };
|
|
68
|
+
}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** The kinds and relations a view of this page reads: the kind's links, both ways; the home, everything. */
|
|
72
|
+
function linksOf(here, part) {
|
|
73
|
+
const schema = here.store.schema;
|
|
74
|
+
const kinds = new Set();
|
|
75
|
+
const edges = new Set();
|
|
76
|
+
for (const definition of schema.definitions) {
|
|
77
|
+
for (const [name, edge] of Object.entries((definition.edges ?? {}))) {
|
|
78
|
+
const to = Array.isArray(edge?.to) ? edge.to : [];
|
|
79
|
+
const touches = part.kind === undefined || definition.kind === part.kind || to.includes(part.kind);
|
|
80
|
+
if (!touches)
|
|
81
|
+
continue;
|
|
82
|
+
edges.add(name);
|
|
83
|
+
for (const kind of [definition.kind, ...to])
|
|
84
|
+
if (kind !== part.kind && schema.tryDefinition(kind))
|
|
85
|
+
kinds.add(kind);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (part.kind === undefined)
|
|
89
|
+
for (const kind of schema.kinds)
|
|
90
|
+
kinds.add(kind);
|
|
91
|
+
return { kinds: [...kinds], edges: [...edges] };
|
|
92
|
+
}
|
|
93
|
+
/** Why a view could not be drawn, in words a model can act on, from the mark its host left where it fell back. */
|
|
94
|
+
const FELL_BACK = {
|
|
95
|
+
manifest: "The view could not start: what it reads is not this app's.",
|
|
96
|
+
source: "The script is too long: make it shorter.",
|
|
97
|
+
start: "The script did not start: it must be one plain script, with no syntax errors, that calls graview.onProps and graview.render.",
|
|
98
|
+
error: "The script threw while drawing: read only what props gives (props.nodes for a list, props.node for one record), guard each value that may be missing, and call no function that does not exist.",
|
|
99
|
+
};
|
|
100
|
+
/** The view's place on the page, once the host has drawn the view of this name there: its handle — or why it fell back. */
|
|
101
|
+
async function placeOfView(name, source, ms = 15000) {
|
|
102
|
+
const until = Date.now() + ms;
|
|
103
|
+
while (Date.now() < until) {
|
|
104
|
+
for (const element of document.querySelectorAll("[data-worker-view-place]")) {
|
|
105
|
+
const handle = viewPlaceOf(element);
|
|
106
|
+
// The place drawn now, with something in it: never one the page is tearing down, nor one not yet drawn after the page was drawn again.
|
|
107
|
+
const drawn = handle?.name === name ? handle.drawing().root : undefined;
|
|
108
|
+
// And the draft itself, where one was asked for: a design kept under the same name is not it.
|
|
109
|
+
if (handle && element.isConnected && drawn?.isConnected && drawn.childElementCount > 0 && (source === undefined || (await handle.source()) === source))
|
|
110
|
+
return { element, handle };
|
|
111
|
+
}
|
|
112
|
+
const fell = document.querySelector(`[data-worker-view-fell-back="${CSS.escape(name)}"]`);
|
|
113
|
+
if (fell)
|
|
114
|
+
return { failed: FELL_BACK[fell.getAttribute("data-worker-view-failed") ?? ""] ?? FELL_BACK["error"] };
|
|
115
|
+
await wait(80);
|
|
116
|
+
}
|
|
117
|
+
return { failed: "The view did not draw in time: keep it simple and quick to draw." };
|
|
118
|
+
}
|
|
119
|
+
const CARDS = /* @__PURE__ */ new Map();
|
|
120
|
+
const cardOf = (at, first) => {
|
|
121
|
+
let held = CARDS.get(at);
|
|
122
|
+
if (!held)
|
|
123
|
+
CARDS.set(at, (held = { phase: first(), steps: [], before: false, putBack: null, began: false, original: null, markup: null, talk: [], events: [], since: Date.now(), stop: null, covering: false, pointed: null, about: null, kept: null, pressed: null, version: 0, heard: new Set() }));
|
|
124
|
+
return held;
|
|
125
|
+
};
|
|
126
|
+
/** The labels of the records the page showed: what a draft must still show. */
|
|
127
|
+
function labelsShown(here, part, text) {
|
|
128
|
+
const { store } = here;
|
|
129
|
+
const nodes = part.record ? [store.graph.getNode(part.record)].filter(Boolean) : part.kind ? store.graph.nodesOfKind(part.kind) : store.schema.kinds.flatMap((kind) => store.graph.nodesOfKind(kind));
|
|
130
|
+
const read = words(text).toLowerCase();
|
|
131
|
+
return nodes
|
|
132
|
+
.map((node) => String(node["label"] ?? ""))
|
|
133
|
+
.filter((label) => label.length > 1 && read.includes(words(label).toLowerCase()))
|
|
134
|
+
.slice(0, 40);
|
|
135
|
+
}
|
|
136
|
+
/** The fields of the part's kind, by name: words a draft must not show as they are kept. */
|
|
137
|
+
function fieldsOf(here, part) {
|
|
138
|
+
const kinds = part.kind ? [part.kind] : here.store.schema.kinds;
|
|
139
|
+
return kinds.flatMap((kind) => Object.keys((here.store.schema.tryDefinition(kind)?.fields?.shape ?? {})));
|
|
140
|
+
}
|
|
141
|
+
export function PageCard({ ask, close }) {
|
|
142
|
+
const here = useGraview();
|
|
143
|
+
const { ai, aiDesk, aiHere, session, principal, keptViews, store, onProposeChange, composeChange, onDraftChange, app } = here;
|
|
144
|
+
const part = ask.part;
|
|
145
|
+
const held = useMemo(() => cardOf(ask.at, () => (ask.mode === "highlight" ? { at: "highlighting" } : { at: "open" })), [ask]);
|
|
146
|
+
const subscribe = useCallback((listener) => (held.heard.add(listener), () => void held.heard.delete(listener)), [held]);
|
|
147
|
+
const phase = useSyncExternalStore(subscribe, () => held.phase, () => held.phase);
|
|
148
|
+
const version = useSyncExternalStore(subscribe, () => held.version, () => held.version);
|
|
149
|
+
void version;
|
|
150
|
+
const tell = useCallback(() => {
|
|
151
|
+
held.version += 1;
|
|
152
|
+
held.heard.forEach((listener) => listener());
|
|
153
|
+
}, [held]);
|
|
154
|
+
const setPhase = useCallback((next) => {
|
|
155
|
+
held.phase = next;
|
|
156
|
+
tell();
|
|
157
|
+
}, [held, tell]);
|
|
158
|
+
const backFromDraft = useCallback(async () => {
|
|
159
|
+
const back = held.putBack;
|
|
160
|
+
held.putBack = null;
|
|
161
|
+
if (back)
|
|
162
|
+
await back();
|
|
163
|
+
}, [held]);
|
|
164
|
+
/* Put away for good: a draft not kept goes with it, and the page stands as it was. */
|
|
165
|
+
const putAway = useCallback(() => {
|
|
166
|
+
held.stop?.abort();
|
|
167
|
+
held.stop = null;
|
|
168
|
+
void backFromDraft();
|
|
169
|
+
CARDS.delete(ask.at);
|
|
170
|
+
close();
|
|
171
|
+
}, [backFromDraft, ask.at, close, held]);
|
|
172
|
+
/* The page's own region now: the one the part was read in, or the one drawn again since. */
|
|
173
|
+
const mainNow = useCallback(() => (part.main?.isConnected ? part.main : document_main()), [part]);
|
|
174
|
+
const said = { name: part.name, slot: part.slot, ...(part.kind ? { kind: part.kind } : {}), ...(part.group ? { group: part.group } : {}) };
|
|
175
|
+
const placeName = part.place ?? (part.kind ? `${part.kind}-${part.slot}` : "home");
|
|
176
|
+
const scope = ask.fromScene ? `From the scene: Better UI changes how “${part.name}” looks on its page here, for everyone in this app.` : `This changes how ${part.name} looks for everyone in this app.`;
|
|
177
|
+
const entryOf = useCallback((asked, outcome, stages, extra = {}) => ({
|
|
178
|
+
id: interactionId(),
|
|
179
|
+
at: new Date().toISOString(),
|
|
180
|
+
by: principal,
|
|
181
|
+
target: { on: "place", place: placeName },
|
|
182
|
+
targetLabel: part.name,
|
|
183
|
+
ask: { kind: "section", said: asked },
|
|
184
|
+
offered: [],
|
|
185
|
+
outcome,
|
|
186
|
+
stages: stages.map((one) => ({ stage: one.stage, ms: one.ms, model: one.model })),
|
|
187
|
+
usage: stages.flatMap((one) => (one.usage ? [{ tier: one.stage === "light" ? "fast" : "thinking", model: one.model, inputTokens: one.usage.inputTokens, outputTokens: one.usage.outputTokens }] : [])),
|
|
188
|
+
...extra,
|
|
189
|
+
}), [principal, placeName, part.name]);
|
|
190
|
+
/* What this seat may do here: change the app's settings through the host, and give the page a new design. */
|
|
191
|
+
const specHost = onProposeChange !== undefined && composeChange !== undefined && mayChangeDeclaration(store, principal);
|
|
192
|
+
const acts = useMemo(() => actsOf(here, part), [here, part]);
|
|
193
|
+
const manifest = forkManifest(said, linksOf(here, part), acts.map((one) => one.act));
|
|
194
|
+
const forkHost = manifest !== undefined && typeof keptViews?.fork === "function" && typeof keptViews.draftFork === "function" && keptViews.current().may.views === true;
|
|
195
|
+
const from = part.slot === "record" ? { page: "record", kind: part.kind } : part.slot === "home" ? { page: "home" } : { page: "list", kind: part.kind ?? "" };
|
|
196
|
+
const forkOf = useCallback((source) => ({ manifest: manifest, source, from, said: "" }), [manifest, from]);
|
|
197
|
+
/* GRAVIEW IS HERE WHILE IT WORKS, as it is beside the quick ask: on the part, among who is here. */
|
|
198
|
+
const working = phase.at === "working";
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (!working)
|
|
201
|
+
return;
|
|
202
|
+
const at = part.record ?? (part.kind ? `kind:${part.kind}` : null);
|
|
203
|
+
aiHere({ participant: `agent:graview-for-${principal.id ?? "anyone"}:${session}`, kind: "agent", name: "Graview", ...(principal.id ? { onBehalfOf: principal.id } : {}), over: at, robot: { at, mode: "thinking" } });
|
|
204
|
+
return () => aiHere(null);
|
|
205
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
206
|
+
}, [working]);
|
|
207
|
+
/** The page as it was, read once before any draft: what every draft is judged against and compared with. */
|
|
208
|
+
const original = useCallback(() => {
|
|
209
|
+
if (held.original)
|
|
210
|
+
return held.original;
|
|
211
|
+
const main = mainNow();
|
|
212
|
+
const text = main?.innerText ?? "";
|
|
213
|
+
held.original = readDrawn(regionOf(main), labelsShown(here, part, text));
|
|
214
|
+
return held.original;
|
|
215
|
+
}, [held, mainNow, here, part]);
|
|
216
|
+
/** The page now, read as the original was. */
|
|
217
|
+
const drawnNow = useCallback(() => readDrawn(regionOf(mainNow()), original().records.map((one) => one.label)), [mainNow, original]);
|
|
218
|
+
/** A step shown on the page: the settings' edits laid over this page alone, or the new design drawn in its place. */
|
|
219
|
+
const show = useCallback(async (step) => {
|
|
220
|
+
await backFromDraft();
|
|
221
|
+
if (step.kind === "spec") {
|
|
222
|
+
const shown = await onDraftChange(proposalOf(step.edits, step.said, ai.name));
|
|
223
|
+
held.putBack = shown.undo;
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
keptViews.draftFork(forkOf(step.source));
|
|
227
|
+
held.putBack = () => keptViews.draftFork(null);
|
|
228
|
+
}
|
|
229
|
+
}, [backFromDraft, onDraftChange, held, keptViews, forkOf, ai.name]);
|
|
230
|
+
/** The page as the person last saw it: the newest draft, or the page as it was. */
|
|
231
|
+
const showLatest = useCallback(async () => {
|
|
232
|
+
const step = held.steps.at(-1);
|
|
233
|
+
held.before = false;
|
|
234
|
+
if (step)
|
|
235
|
+
await show(step);
|
|
236
|
+
else
|
|
237
|
+
await backFromDraft();
|
|
238
|
+
}, [held, show, backFromDraft]);
|
|
239
|
+
const say = useCallback((who, words) => {
|
|
240
|
+
held.talk.push({ who, said: words });
|
|
241
|
+
tell();
|
|
242
|
+
}, [held, tell]);
|
|
243
|
+
const step = useCallback((event) => {
|
|
244
|
+
// 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.
|
|
245
|
+
if (held.events.at(-1)?.kind === "streaming")
|
|
246
|
+
held.events.pop();
|
|
247
|
+
held.events.push(event);
|
|
248
|
+
tell();
|
|
249
|
+
}, [held, tell]);
|
|
250
|
+
/** What the person has asked of it, all of it: a fact or a group they asked away is theirs to change. */
|
|
251
|
+
const askedWords = () => held.talk.filter((one) => one.who === "person").map((one) => one.said).join(" ");
|
|
252
|
+
/**
|
|
253
|
+
* THE QUICK WAY FIRST: where the Fast slot says the page's settings can say
|
|
254
|
+
* it, the least edits that do, checked by the host's composer and drawn
|
|
255
|
+
* on this page only — in seconds, with no conversation needed. Answers
|
|
256
|
+
* whether it made a draft; where it could not, the agent takes it.
|
|
257
|
+
*/
|
|
258
|
+
const bySettings = useCallback(async (asked) => {
|
|
259
|
+
if (!specHost || !onDraftChange)
|
|
260
|
+
return false;
|
|
261
|
+
const reshape = await import("@graview/tools/reshape");
|
|
262
|
+
const remembered = reshape.documentOfApp(app ?? {});
|
|
263
|
+
if (!remembered)
|
|
264
|
+
return false;
|
|
265
|
+
const after = held.steps.at(-1);
|
|
266
|
+
if (after && after.kind !== "spec")
|
|
267
|
+
return false;
|
|
268
|
+
const before = after?.edits ?? [];
|
|
269
|
+
const composedBefore = before.length > 0 ? composeChange({ document: remembered, edits: before }) : undefined;
|
|
270
|
+
const document = composedBefore?.ok ? composedBefore.document : remembered;
|
|
271
|
+
const can = asked.mode === "just" ? { can: true } : await specCanSay(ai, asked.words, said);
|
|
272
|
+
if (!can.can)
|
|
273
|
+
return false;
|
|
274
|
+
step({ kind: "look", text: `Looking at how ${part.name} is set up` });
|
|
275
|
+
let lines = [];
|
|
276
|
+
const drafted = await draftPageSpec({
|
|
277
|
+
ai,
|
|
278
|
+
document,
|
|
279
|
+
part: said,
|
|
280
|
+
mode: asked.mode === "choose" ? "just" : asked.mode,
|
|
281
|
+
...(asked.words ? { words: asked.words } : {}),
|
|
282
|
+
...(asked.pointed ? { highlighted: [asked.pointed.name, ...asked.pointed.lines] } : {}),
|
|
283
|
+
shows: outlineOf(mainNow(), 30),
|
|
284
|
+
judge: async (edits) => {
|
|
285
|
+
const composed = composeChange({ document: remembered, edits: [...before, ...edits] });
|
|
286
|
+
if (!composed.ok)
|
|
287
|
+
return [composed.refused];
|
|
288
|
+
lines = composed.said;
|
|
289
|
+
return [];
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
if (!drafted.ok)
|
|
293
|
+
return false;
|
|
294
|
+
const edits = [...before, ...drafted.edits];
|
|
295
|
+
const entry = entryOf(`Make “${part.name}” better: ${asked.words}`, "offered", drafted.stages);
|
|
296
|
+
const was = drawnSignature(mainNow());
|
|
297
|
+
const next = { kind: "spec", edits, said: lines[lines.length - 1] ?? lines[0] ?? `${part.name} changed.`, entry };
|
|
298
|
+
held.covering = true;
|
|
299
|
+
tell();
|
|
300
|
+
try {
|
|
301
|
+
await show(next);
|
|
302
|
+
let now = was;
|
|
303
|
+
for (const until = Date.now() + 1500; Date.now() < until && now === was;) {
|
|
304
|
+
await frame();
|
|
305
|
+
now = drawnSignature(mainNow());
|
|
306
|
+
}
|
|
307
|
+
const findings = now === was ? ["nothing changed"] : drawnFindings(original(), drawnNow(), { fields: fieldsOf(here, part), asked: askedWords() });
|
|
308
|
+
if (findings.length > 0) {
|
|
309
|
+
await showLatest();
|
|
310
|
+
aiDesk.record({ ...entry, outcome: "failed", failed: findings.join(" ") });
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
await showLatest();
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
held.covering = false;
|
|
320
|
+
tell();
|
|
321
|
+
}
|
|
322
|
+
held.steps.push(next);
|
|
323
|
+
held.before = false;
|
|
324
|
+
step({ kind: "passed", text: "Changed its settings — it's on the page" });
|
|
325
|
+
say("graview", `Done: ${next.said.replace(/\.$/, "")}.`);
|
|
326
|
+
return true;
|
|
327
|
+
}, [ai, aiDesk, app, composeChange, entryOf, held, here, mainNow, onDraftChange, original, drawnNow, part, said, show, showLatest, specHost, step, say, tell]);
|
|
328
|
+
/**
|
|
329
|
+
* A DRAFT THE AGENT MADE, drawn where nobody sees it — under a cover over
|
|
330
|
+
* the part — and judged: the host's checks of its code and of what it
|
|
331
|
+
* drew, and the page read again against the page as it was. What passes
|
|
332
|
+
* is left on the page as the newest draft; what does not is taken away
|
|
333
|
+
* again, and its findings go back to the agent.
|
|
334
|
+
*/
|
|
335
|
+
const judgeDraft = useCallback(async (source) => {
|
|
336
|
+
if (!manifest || !keptViews?.draftFork)
|
|
337
|
+
return { findings: ["This page cannot be drawn anew here."], drew: undefined };
|
|
338
|
+
const was = original();
|
|
339
|
+
held.covering = true;
|
|
340
|
+
tell();
|
|
341
|
+
try {
|
|
342
|
+
keptViews.draftFork(forkOf(source));
|
|
343
|
+
held.putBack = () => keptViews.draftFork(null);
|
|
344
|
+
const drawn = await placeOfView(manifest.name, source);
|
|
345
|
+
let findings;
|
|
346
|
+
if ("failed" in drawn)
|
|
347
|
+
findings = [drawn.failed];
|
|
348
|
+
else {
|
|
349
|
+
const refused = [...(await drawn.handle.check(source)), ...drawn.handle.refused()];
|
|
350
|
+
if (refused.length > 0)
|
|
351
|
+
findings = refused;
|
|
352
|
+
else {
|
|
353
|
+
await frame();
|
|
354
|
+
await frame();
|
|
355
|
+
// Read where the draft was drawn: the page's region holding that place, whatever the page drew again meanwhile.
|
|
356
|
+
const region = drawn.element.closest("[data-graview-page-main], main") ?? regionOf(mainNow());
|
|
357
|
+
findings = drawnFindings(was, readDrawn(region, was.records.map((one) => one.label)), { fields: fieldsOf(here, part), asked: askedWords() });
|
|
358
|
+
// Every control it drew pressed, headless and nothing applied, and what a person could do on each record kept.
|
|
359
|
+
const pressed = drawn.handle.presses ? await drawn.handle.presses(source).catch(() => undefined) : undefined;
|
|
360
|
+
if (pressed) {
|
|
361
|
+
held.pressed = pressed;
|
|
362
|
+
findings = [...findings, ...pressFindings(pressed, wantedOf(was, idsOf(here, part, was)), acts.filter((one) => one.toggle).map((one) => one.act), nounOf(part))];
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const drew = lookOf(drawnNow());
|
|
367
|
+
if (findings.length === 0) {
|
|
368
|
+
const entry = entryOf(`Draw “${part.name}” anew: ${askedWords()}`, "offered", []);
|
|
369
|
+
held.steps.push({ kind: "view", source, said: describeChange(part.name, was, drawnNow()) ?? `${part.name} has a new design. Compare Before and After.`, entry });
|
|
370
|
+
held.before = false;
|
|
371
|
+
}
|
|
372
|
+
else
|
|
373
|
+
await showLatest();
|
|
374
|
+
return { findings, drew };
|
|
375
|
+
}
|
|
376
|
+
finally {
|
|
377
|
+
held.covering = false;
|
|
378
|
+
tell();
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
382
|
+
[manifest, keptViews, original, held, tell, forkOf, drawnNow, here, part, entryOf, showLatest]);
|
|
383
|
+
/** One turn of the agent, from the person's last words: it looks, reads, drafts, is told what the checks found, and drafts again. */
|
|
384
|
+
const agentTurn = useCallback(async (pressedLines = []) => {
|
|
385
|
+
if (!manifest || !forkHost) {
|
|
386
|
+
say("graview", "Its settings can't do that, and a new design can't be drawn for it here.");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const stop = new AbortController();
|
|
390
|
+
held.stop = stop;
|
|
391
|
+
held.markup ??= markupOf(mainNow());
|
|
392
|
+
const stepsBefore = held.steps.length;
|
|
393
|
+
const latest = [...held.steps].reverse().find((one) => one.kind === "view");
|
|
394
|
+
const result = await runDesignAgent({
|
|
395
|
+
ai,
|
|
396
|
+
part: said,
|
|
397
|
+
manifest,
|
|
398
|
+
talk: held.talk,
|
|
399
|
+
keep: keepsOf(original(), mainNow(), part.name, here, part),
|
|
400
|
+
...(held.markup ? { markup: held.markup } : {}),
|
|
401
|
+
...(latest ? { current: latest.source } : held.kept ? { current: held.kept } : {}),
|
|
402
|
+
host: { look: () => lookOf(drawnNow()), read: () => dataOf(here, part), draft: judgeDraft },
|
|
403
|
+
onEvent: (event) => step(event),
|
|
404
|
+
...(held.about ? { about: held.about } : {}),
|
|
405
|
+
acts,
|
|
406
|
+
...(pressedLines.length > 0 ? { pressed: pressedLines } : {}),
|
|
407
|
+
noun: nounOf(part),
|
|
408
|
+
signal: stop.signal,
|
|
409
|
+
});
|
|
410
|
+
// Stopped: the page was put back when Stop was pressed; nothing more of this turn.
|
|
411
|
+
if (held.stop !== stop)
|
|
412
|
+
return;
|
|
413
|
+
held.stop = null;
|
|
414
|
+
if (held.events.at(-1)?.kind === "streaming")
|
|
415
|
+
held.events.pop();
|
|
416
|
+
aiDesk.record(entryOf(`Draw “${part.name}” anew: ${askedWords()}`, held.steps.length > stepsBefore ? "offered" : "failed", result.stages, held.steps.length > stepsBefore ? {} : { failed: result.last?.findings.join(" ") ?? result.said }));
|
|
417
|
+
/*
|
|
418
|
+
* NEVER A CLAIM NOBODY CHECKED: what the controls of a draft do is said by
|
|
419
|
+
* the press check — what was pressed and accepted — and a sentence of
|
|
420
|
+
* the model's that says what a control does is not said for it.
|
|
421
|
+
*/
|
|
422
|
+
if (result.passed) {
|
|
423
|
+
const checked = held.pressed ? pressSaid(held.pressed, acts, nounOf(part)) : undefined;
|
|
424
|
+
const own = CONTROL_WORDS.test(result.said) ? undefined : result.said;
|
|
425
|
+
say("graview", [own ?? (checked ? undefined : "Here it is on the page."), checked].filter(Boolean).join(" "));
|
|
426
|
+
}
|
|
427
|
+
else
|
|
428
|
+
say("graview", result.said);
|
|
429
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
430
|
+
}, [ai, aiDesk, acts, drawnNow, entryOf, forkHost, held, here, judgeDraft, mainNow, manifest, original, part, said, say, step]);
|
|
431
|
+
/**
|
|
432
|
+
* "IT DOESN'T WORK": the design on the page — the newest draft, or the
|
|
433
|
+
* one kept — has every control pressed, headless and nothing applied,
|
|
434
|
+
* and what came of it is said to the person and handed to the agent,
|
|
435
|
+
* before it says anything of its own.
|
|
436
|
+
*/
|
|
437
|
+
const pressTheDesign = useCallback(async () => {
|
|
438
|
+
const latest = [...held.steps].reverse().find((one) => one.kind === "view");
|
|
439
|
+
const source = latest?.source ?? held.kept;
|
|
440
|
+
if (!source || !manifest)
|
|
441
|
+
return [];
|
|
442
|
+
step({ kind: "look", text: "Pressing every control on the page" });
|
|
443
|
+
const drawn = await placeOfView(manifest.name);
|
|
444
|
+
if ("failed" in drawn || !drawn.handle.presses)
|
|
445
|
+
return [];
|
|
446
|
+
const pressed = await drawn.handle.presses(source).catch(() => undefined);
|
|
447
|
+
if (!pressed)
|
|
448
|
+
return [];
|
|
449
|
+
held.pressed = pressed;
|
|
450
|
+
const was = original();
|
|
451
|
+
const broken = pressFindings(pressed, wantedOf(was, idsOf(here, part, was)), acts.filter((one) => one.toggle).map((one) => one.act), nounOf(part));
|
|
452
|
+
const lines = broken.length > 0 ? broken : pressed.controls.slice(0, 8).map((one) => `${one.control}: ${one.says}`);
|
|
453
|
+
step(broken.length > 0 ? { kind: "check", text: `Check: ${broken.length} ${broken.length === 1 ? "control doesn't" : "controls don't"} work — fixing` } : { kind: "passed", text: "Check: every control works" });
|
|
454
|
+
say("graview", broken.length > 0 ? `I pressed every control: ${broken.join(" ")}` : `I pressed every control. ${pressSaid(pressed, acts, nounOf(part)) ?? "Each one did what it says."}`);
|
|
455
|
+
return lines;
|
|
456
|
+
}, [acts, held, here, manifest, original, part, say, step]);
|
|
457
|
+
/** The person's words: the settings where they can say it, else the agent — one turn. */
|
|
458
|
+
const askFor = useCallback(async (words, mode = "describe") => {
|
|
459
|
+
if (held.phase.at === "working")
|
|
460
|
+
return;
|
|
461
|
+
const pointed = held.pointed;
|
|
462
|
+
held.pointed = null;
|
|
463
|
+
const asked = { mode, words, ...(pointed ? { pointed } : {}) };
|
|
464
|
+
// The person's words as they typed them: what they are about is said beside them, never glued on.
|
|
465
|
+
held.about = pointed ?? held.about;
|
|
466
|
+
say("person", words);
|
|
467
|
+
held.events = [];
|
|
468
|
+
held.since = Date.now();
|
|
469
|
+
setPhase({ at: "working" });
|
|
470
|
+
original();
|
|
471
|
+
held.markup ??= markupOf(mainNow());
|
|
472
|
+
// Told a control does not work, the design on the page is pressed first, and what was found goes to the agent.
|
|
473
|
+
const complaint = IT_DOES_NOT_WORK.test(words) ? await pressTheDesign().catch(() => []) : [];
|
|
474
|
+
// A control that does not work is never a setting: those words go to the agent, with the press check behind them.
|
|
475
|
+
const quick = !IT_DOES_NOT_WORK.test(words) && (await bySettings(asked).catch(() => false));
|
|
476
|
+
if (!quick)
|
|
477
|
+
await agentTurn(complaint);
|
|
478
|
+
if (held.phase.at === "working")
|
|
479
|
+
setPhase(held.steps.length > 0 ? { at: "draft" } : { at: "open" });
|
|
480
|
+
}, [agentTurn, bySettings, held, mainNow, original, pressTheDesign, say, setPhase]);
|
|
481
|
+
/** Stop: at once — the turn's drafts gone, the page as it was before it began. */
|
|
482
|
+
const stopTurn = useCallback(() => {
|
|
483
|
+
if (held.phase.at !== "working")
|
|
484
|
+
return;
|
|
485
|
+
const stop = held.stop;
|
|
486
|
+
held.stop = null;
|
|
487
|
+
stop?.abort();
|
|
488
|
+
held.covering = false;
|
|
489
|
+
held.steps = held.steps.filter((one) => one.entry.at < new Date(held.since).toISOString());
|
|
490
|
+
void showLatest();
|
|
491
|
+
say("graview", "Stopped. The page is as it was.");
|
|
492
|
+
setPhase(held.steps.length > 0 ? { at: "draft" } : { at: "open" });
|
|
493
|
+
}, [held, say, setPhase, showLatest]);
|
|
494
|
+
const compare = useCallback(async (before) => {
|
|
495
|
+
const latest = held.steps.at(-1);
|
|
496
|
+
if (!latest || before === held.before)
|
|
497
|
+
return;
|
|
498
|
+
held.before = before;
|
|
499
|
+
tell();
|
|
500
|
+
if (before)
|
|
501
|
+
await backFromDraft();
|
|
502
|
+
else
|
|
503
|
+
await show(latest);
|
|
504
|
+
}, [held, backFromDraft, show, tell]);
|
|
505
|
+
const keep = useCallback(async () => {
|
|
506
|
+
const latest = held.steps.at(-1);
|
|
507
|
+
if (!latest || held.phase.at !== "draft")
|
|
508
|
+
return;
|
|
509
|
+
setPhase({ at: "keeping" });
|
|
510
|
+
try {
|
|
511
|
+
let undo;
|
|
512
|
+
let note;
|
|
513
|
+
if (latest.kind === "spec") {
|
|
514
|
+
await backFromDraft();
|
|
515
|
+
const answer = await onProposeChange(proposalOf(latest.edits, latest.said, ai.name));
|
|
516
|
+
if (answer === "declined") {
|
|
517
|
+
say("graview", "It wasn't kept.");
|
|
518
|
+
return setPhase({ at: "draft" });
|
|
519
|
+
}
|
|
520
|
+
if (typeof answer === "object")
|
|
521
|
+
[undo, note] = [answer.undo, answer.note];
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
const answer = await keptViews.fork(forkOf(latest.source));
|
|
525
|
+
held.putBack = null;
|
|
526
|
+
keptViews.draftFork?.(null);
|
|
527
|
+
held.kept = latest.source;
|
|
528
|
+
if (answer)
|
|
529
|
+
[undo, note] = [answer.undo, answer.note];
|
|
530
|
+
}
|
|
531
|
+
aiDesk.record({ ...latest.entry, outcome: "accepted" });
|
|
532
|
+
held.steps = [];
|
|
533
|
+
held.before = false;
|
|
534
|
+
setPhase({ at: "kept", ...(undo ? { undo } : {}), ...(note ? { note } : {}) });
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
say("graview", "It couldn't be kept.");
|
|
538
|
+
setPhase({ at: "draft" });
|
|
539
|
+
}
|
|
540
|
+
}, [held, setPhase, backFromDraft, onProposeChange, ai.name, keptViews, forkOf, aiDesk, say]);
|
|
541
|
+
const discard = useCallback(() => {
|
|
542
|
+
const latest = held.steps.at(-1);
|
|
543
|
+
if (latest)
|
|
544
|
+
aiDesk.record({ ...latest.entry, outcome: "dismissed" });
|
|
545
|
+
held.steps = [];
|
|
546
|
+
held.before = false;
|
|
547
|
+
void backFromDraft();
|
|
548
|
+
say("graview", "Put back as it was.");
|
|
549
|
+
setPhase({ at: "open" });
|
|
550
|
+
}, [held, aiDesk, backFromDraft, say, setPhase]);
|
|
551
|
+
const undo = useCallback(async () => {
|
|
552
|
+
if (held.phase.at !== "kept" || !held.phase.undo)
|
|
553
|
+
return;
|
|
554
|
+
try {
|
|
555
|
+
await held.phase.undo();
|
|
556
|
+
held.kept = null;
|
|
557
|
+
say("graview", "Undone. The page is as it was.");
|
|
558
|
+
setPhase({ at: "undone" });
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
say("graview", "It couldn't be put back.");
|
|
562
|
+
}
|
|
563
|
+
}, [held, say, setPhase]);
|
|
564
|
+
/* Words that came with the ask — typed in the menu, or a suggestion taken — are asked at once. */
|
|
565
|
+
useEffect(() => {
|
|
566
|
+
if (held.began || !(ask.mode === "just" || (ask.mode === "describe" && ask.words)))
|
|
567
|
+
return;
|
|
568
|
+
held.began = true;
|
|
569
|
+
void askFor(ask.mode === "just" ? "Make it easier to read at a glance" : ask.words, ask.mode);
|
|
570
|
+
}, [held, ask.mode, ask.words, askFor]);
|
|
571
|
+
/* Escape: Stop while it works, else put it away. */
|
|
572
|
+
useEffect(() => {
|
|
573
|
+
const key = (event) => {
|
|
574
|
+
if (event.key !== "Escape" || held.phase.at === "highlighting")
|
|
575
|
+
return;
|
|
576
|
+
event.preventDefault();
|
|
577
|
+
if (held.phase.at === "working")
|
|
578
|
+
return stopTurn();
|
|
579
|
+
putAway();
|
|
580
|
+
const back = ask.returnTo?.isConnected ? ask.returnTo : document.querySelector("[data-graview-page-title], main h1");
|
|
581
|
+
if (back && !back.hasAttribute("tabindex") && !back.matches("a[href], button, input, select, textarea"))
|
|
582
|
+
back.setAttribute("tabindex", "-1");
|
|
583
|
+
back?.focus?.();
|
|
584
|
+
};
|
|
585
|
+
document.addEventListener("keydown", key);
|
|
586
|
+
return () => document.removeEventListener("keydown", key);
|
|
587
|
+
}, [held, putAway, stopTurn, ask.returnTo]);
|
|
588
|
+
if (phase.at === "highlighting") {
|
|
589
|
+
return (_jsx(Highlighter, { box: mainNow(), root: mainNow(), whole: part.elements, wholeSaid: "The whole page", cancel: () => setPhase({ at: "open" }), chose: (elements) => {
|
|
590
|
+
held.pointed = { name: aboutOf(elements) ?? nameParts(elements.map(readPart)), lines: partLines(elements.map(readPart)) };
|
|
591
|
+
setPhase({ at: "open" });
|
|
592
|
+
} }));
|
|
593
|
+
}
|
|
594
|
+
const latest = held.steps.at(-1);
|
|
595
|
+
return (_jsxs(_Fragment, { children: [held.covering ? _jsx(Redesigning, { over: mainNow(), name: part.name, said: `Graview is drafting “${part.name}”…` }) : null, _jsx(BetterPanel, { name: part.name, scope: scope, phase: phase.at, talk: held.talk, steps: held.events, since: held.since, working: phase.at === "working" || phase.at === "keeping", ...(latest && phase.at === "draft" ? { draft: { said: latest.said, before: held.before } } : {}), ...(phase.at === "kept" ? { kept: { ...(phase.note ? { note: phase.note } : {}), ...(phase.undo ? { undo: () => void undo() } : {}) } } : {}), about: (held.pointed ?? held.about)?.name ?? part.name, ask: (words) => void askFor(words), just: () => void askFor("Make it easier to read at a glance", "just"), point: () => setPhase({ at: "highlighting" }), stop: stopTurn, compare: (before) => void compare(before), keep: () => void keep(), discard: discard, close: putAway })] }));
|
|
596
|
+
}
|
|
597
|
+
/** What the host is handed for the settings' edits: the change to the app, said by the checker. */
|
|
598
|
+
function proposalOf(edits, said, model) {
|
|
599
|
+
return { said, add: [], drop: [], card: { said, adds: [], keeps: [], drops: [], breaking: [] }, edits, via: `ai:${model ?? "model"}` };
|
|
600
|
+
}
|
|
601
|
+
/** What a new design must keep of the page: the title the page draws above it (never again), its other headings, the records it showed. */
|
|
602
|
+
function keepsOf(was, main, name, here, part) {
|
|
603
|
+
// The page's own title, never the app's name in the bar above it: the page draws it, so a draft never does.
|
|
604
|
+
const page = regionOf(main);
|
|
605
|
+
// The home's body is all a new design replaces: what is drawn over it — the app's name in the bar — is its title.
|
|
606
|
+
const above = part.slot === "home" ? [...(main?.querySelectorAll("h1") ?? [])].find((one) => page !== main && !page?.contains(one)) : undefined;
|
|
607
|
+
const title = words(above?.textContent) || words(page?.querySelector("[data-graview-page-title]")?.textContent) || (part.slot === "home" ? "" : words(page?.querySelector("h1")?.textContent)) || (part.record ? words(String(here.store.graph.getNode(part.record)?.["label"] ?? "")) : "") || name;
|
|
608
|
+
const ids = idsOf(here, part, was);
|
|
609
|
+
const says = Object.fromEntries(was.records.flatMap((one) => (one.facts?.length || (one.under && one.under !== title) ? [[one.label, { ...(ids[one.label] ? { id: ids[one.label] } : {}), ...(one.facts?.length ? { shows: one.facts } : {}), ...(one.under && one.under !== title ? { under: one.under } : {}) }]] : [])));
|
|
610
|
+
const grouping = groupingOf(here, was, ids, title);
|
|
611
|
+
return { title, headings: was.headings.filter((one) => one !== title), records: was.records.map((one) => one.label), ...(Object.keys(says).length > 0 ? { says } : {}), ...(grouping ? { grouping } : {}) };
|
|
612
|
+
}
|
|
613
|
+
/** Each record the page showed, by its words: its id, for the checklist a draft is held to. */
|
|
614
|
+
function idsOf(here, part, was) {
|
|
615
|
+
const { store } = here;
|
|
616
|
+
const nodes = part.record ? [store.graph.getNode(part.record)].filter((one) => one !== undefined) : part.kind ? store.graph.nodesOfKind(part.kind) : store.schema.kinds.flatMap((kind) => store.graph.nodesOfKind(kind));
|
|
617
|
+
const out = {};
|
|
618
|
+
for (const one of was.records) {
|
|
619
|
+
const node = nodes.find((each) => words(String(each["label"] ?? "")) === words(one.label));
|
|
620
|
+
if (node)
|
|
621
|
+
out[one.label] = node.id;
|
|
622
|
+
}
|
|
623
|
+
return out;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* HOW THE PAGE GROUPED ITS RECORDS, read from the graph: each record sits
|
|
627
|
+
* under the heading that is the name of a record it is linked to (a task
|
|
628
|
+
* under its list's name) — the link's kind, the kind of the record named,
|
|
629
|
+
* and which end the grouped record is. Undefined where no one link says it
|
|
630
|
+
* for most of them: the checklist's headings stand alone then.
|
|
631
|
+
*/
|
|
632
|
+
function groupingOf(here, was, ids, title) {
|
|
633
|
+
const { store } = here;
|
|
634
|
+
const grouped = was.records.filter((one) => one.under && one.under !== title && ids[one.label]);
|
|
635
|
+
if (grouped.length < 2)
|
|
636
|
+
return undefined;
|
|
637
|
+
const edges = store.graph.allEdges();
|
|
638
|
+
const count = new Map();
|
|
639
|
+
for (const one of grouped) {
|
|
640
|
+
const id = ids[one.label];
|
|
641
|
+
for (const edge of edges) {
|
|
642
|
+
const other = edge.from === id ? edge.to : edge.to === id ? edge.from : undefined;
|
|
643
|
+
const named = other !== undefined ? store.graph.getNode(other) : undefined;
|
|
644
|
+
if (!named || words(String(named["label"] ?? "")) !== words(one.under))
|
|
645
|
+
continue;
|
|
646
|
+
const key = `${edge.kind}\u0000${named.kind}\u0000${edge.from === id ? "from" : "to"}`;
|
|
647
|
+
count.set(key, (count.get(key) ?? 0) + 1);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
const best = [...count].sort((a, b) => b[1] - a[1])[0];
|
|
651
|
+
if (!best || best[1] < grouped.length / 2)
|
|
652
|
+
return undefined;
|
|
653
|
+
const [edge, kind, end] = best[0].split("\u0000");
|
|
654
|
+
return { edge, kind, record: end };
|
|
655
|
+
}
|
|
656
|
+
/** The page's own region inside the face: its <main>, never the app's bar and footer around it. */
|
|
657
|
+
const regionOf = (face) => face?.querySelector("main") ?? face;
|
|
658
|
+
/**
|
|
659
|
+
* THE PAGE AS IT WAS DRAWN, its markup for a new design to start from:
|
|
660
|
+
* elements and their words, the classes and data a person never reads
|
|
661
|
+
* left out, what is hidden and what draws no words left out, kept short.
|
|
662
|
+
*/
|
|
663
|
+
function markupOf(main) {
|
|
664
|
+
if (!main?.isConnected)
|
|
665
|
+
return null;
|
|
666
|
+
// The page's own region, not the app's bar and footer around it.
|
|
667
|
+
const copy = (main.querySelector("main") ?? main).cloneNode(true);
|
|
668
|
+
for (const gone of copy.querySelectorAll("script, style, svg, button, input, select, textarea, [hidden], [aria-hidden='true'], [data-graview-popover]"))
|
|
669
|
+
gone.remove();
|
|
670
|
+
for (const element of [copy, ...copy.querySelectorAll("*")])
|
|
671
|
+
for (const name of [...element.getAttributeNames()])
|
|
672
|
+
element.removeAttribute(name);
|
|
673
|
+
const html = copy.outerHTML.replace(/\s+/g, " ").replace(/<(div|span)>\s*<\/\1>/g, "").trim();
|
|
674
|
+
return html.length > 6000 ? html.slice(0, 6000) : html;
|
|
675
|
+
}
|
|
676
|
+
/** What the records of the part are called in the agent's steps: "tasks". */
|
|
677
|
+
const nounOf = (part) => (part.slot === "list" || part.slot === "group" ? part.name.toLowerCase() : part.slot === "record" ? "parts" : "records");
|
|
678
|
+
/** Each record the page showed, with its id and what a person could do on it there. */
|
|
679
|
+
function wantedOf(was, ids) {
|
|
680
|
+
return was.records.flatMap((one) => (ids[one.label] ? [{ label: one.label, id: ids[one.label], ...(one.presses ? { presses: true } : {}), ...(one.opens ? { opens: true } : {}) }] : []));
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* THE ACTS A PRESS MAY ASK FOR ON THE PART'S RECORDS: each act whose
|
|
684
|
+
* subject is the kind (none that makes a record), its title, the argument
|
|
685
|
+
* the record goes in, and — where it writes one yes-or-no field — what it
|
|
686
|
+
* marks ("done"), so a new design can finish a task as the page did.
|
|
687
|
+
*/
|
|
688
|
+
function actsOf(here, part) {
|
|
689
|
+
const { store } = here;
|
|
690
|
+
const kinds = part.kind ? [part.kind] : store.schema.kinds;
|
|
691
|
+
const shapeOf = (kind) => (store.schema.tryDefinition(kind)?.fields?.shape ?? {});
|
|
692
|
+
const yesOrNo = (field) => {
|
|
693
|
+
let def = field?._zod?.def;
|
|
694
|
+
for (let depth = 0; depth < 6 && def && def.type !== "boolean" && def.innerType !== undefined; depth++)
|
|
695
|
+
def = def.innerType._zod?.def;
|
|
696
|
+
return def?.type === "boolean";
|
|
697
|
+
};
|
|
698
|
+
return store.allMutations().flatMap((mutation) => {
|
|
699
|
+
const subject = mutation.subject;
|
|
700
|
+
if (!subject || mutation.creates?.length)
|
|
701
|
+
return [];
|
|
702
|
+
const kind = kinds.find((one) => subject.kinds === "*" || subject.kinds.includes(one));
|
|
703
|
+
if (!kind)
|
|
704
|
+
return [];
|
|
705
|
+
const writes = mutation.writes ?? [];
|
|
706
|
+
const toggle = writes.length === 1 && yesOrNo(shapeOf(kind)[writes[0]]) ? writes[0] : undefined;
|
|
707
|
+
return [{ act: mutation.name, title: String(mutation.title ?? mutation.name), record: subject.arg, ...(toggle ? { toggle } : {}) }];
|
|
708
|
+
}).slice(0, 24);
|
|
709
|
+
}
|
|
710
|
+
/** The page's own region, found again where the draft drew the page anew. */
|
|
711
|
+
function document_main() {
|
|
712
|
+
return document.querySelector("[data-graview-page-main], main") ?? document.querySelector("[data-graview-face]");
|
|
713
|
+
}
|
|
714
|
+
//# sourceMappingURL=page-better.js.map
|