@graview/primitives 0.1.63 → 0.1.65
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/README.md +1 -1
- package/dist/app-bar.js +1 -1
- package/dist/app-bar.js.map +1 -1
- package/dist/app-title.d.ts +8 -0
- package/dist/app-title.d.ts.map +1 -1
- package/dist/app-title.js +39 -3
- package/dist/app-title.js.map +1 -1
- package/dist/chat.js +1 -1
- package/dist/chat.js.map +1 -1
- package/dist/find.js +3 -3
- package/dist/find.js.map +1 -1
- package/dist/first-look.js +1 -1
- package/dist/first-look.js.map +1 -1
- package/dist/frame.d.ts +1 -1
- package/dist/frame.d.ts.map +1 -1
- package/dist/frame.js +1 -1
- package/dist/frame.js.map +1 -1
- package/dist/here.d.ts +1 -1
- package/dist/here.js +2 -2
- package/dist/here.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/inline-ai-menu.d.ts +4 -2
- package/dist/inline-ai-menu.d.ts.map +1 -1
- package/dist/inline-ai-menu.js +34 -8
- package/dist/inline-ai-menu.js.map +1 -1
- package/dist/kind-stop.d.ts.map +1 -1
- package/dist/kind-stop.js +204 -21
- package/dist/kind-stop.js.map +1 -1
- package/dist/lines-key.d.ts.map +1 -1
- package/dist/lines-key.js +22 -2
- package/dist/lines-key.js.map +1 -1
- package/dist/map-panel.js +98 -6
- package/dist/map-panel.js.map +1 -1
- package/dist/menu-rank.d.ts +25 -0
- package/dist/menu-rank.d.ts.map +1 -0
- package/dist/menu-rank.js +56 -0
- package/dist/menu-rank.js.map +1 -0
- package/dist/menu-told-door.d.ts +17 -0
- package/dist/menu-told-door.d.ts.map +1 -0
- package/dist/menu-told-door.js +49 -0
- package/dist/menu-told-door.js.map +1 -0
- package/dist/menu-told.d.ts +37 -0
- package/dist/menu-told.d.ts.map +1 -0
- package/dist/menu-told.js +66 -0
- package/dist/menu-told.js.map +1 -0
- package/dist/menu-words.d.ts +15 -0
- package/dist/menu-words.d.ts.map +1 -0
- package/dist/menu-words.js +28 -0
- package/dist/menu-words.js.map +1 -0
- package/dist/pages.d.ts +3 -1
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +3 -1
- package/dist/pages.js.map +1 -1
- package/dist/quick-ask-desk.d.ts +16 -0
- package/dist/quick-ask-desk.d.ts.map +1 -1
- package/dist/quick-ask-desk.js +20 -0
- package/dist/quick-ask-desk.js.map +1 -1
- package/dist/quick-ask-door.d.ts.map +1 -1
- package/dist/quick-ask-door.js +5 -0
- package/dist/quick-ask-door.js.map +1 -1
- package/dist/quick-ask-plan.d.ts +52 -0
- package/dist/quick-ask-plan.d.ts.map +1 -0
- package/dist/quick-ask-plan.js +112 -0
- package/dist/quick-ask-plan.js.map +1 -0
- package/dist/quick-ask.d.ts.map +1 -1
- package/dist/quick-ask.js +341 -19
- package/dist/quick-ask.js.map +1 -1
- package/dist/scene-css.d.ts.map +1 -1
- package/dist/scene-css.js +16 -6
- package/dist/scene-css.js.map +1 -1
- package/dist/scene-places.d.ts +3 -2
- package/dist/scene-places.d.ts.map +1 -1
- package/dist/scene-places.js +47 -8
- package/dist/scene-places.js.map +1 -1
- package/dist/scene.d.ts +2 -0
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +2 -0
- package/dist/scene.js.map +1 -1
- package/dist/section-better.d.ts +38 -0
- package/dist/section-better.d.ts.map +1 -0
- package/dist/section-better.js +411 -0
- package/dist/section-better.js.map +1 -0
- package/dist/section-door.d.ts +32 -0
- package/dist/section-door.d.ts.map +1 -0
- package/dist/section-door.js +32 -0
- package/dist/section-door.js.map +1 -0
- package/dist/section-menu.d.ts +18 -0
- package/dist/section-menu.d.ts.map +1 -0
- package/dist/section-menu.js +76 -0
- package/dist/section-menu.js.map +1 -0
- package/dist/section-outline.d.ts +8 -0
- package/dist/section-outline.d.ts.map +1 -0
- package/dist/section-outline.js +48 -0
- package/dist/section-outline.js.map +1 -0
- package/dist/section-words.d.ts +24 -0
- package/dist/section-words.d.ts.map +1 -0
- package/dist/section-words.js +91 -0
- package/dist/section-words.js.map +1 -0
- package/dist/shell.d.ts +7 -0
- package/dist/shell.d.ts.map +1 -1
- package/dist/shell.js +13 -8
- package/dist/shell.js.map +1 -1
- package/dist/subject.js +2 -2
- package/dist/subject.js.map +1 -1
- package/dist/told-card.d.ts +66 -0
- package/dist/told-card.d.ts.map +1 -0
- package/dist/told-card.js +197 -0
- package/dist/told-card.js.map +1 -0
- package/dist/told-listed.d.ts +14 -0
- package/dist/told-listed.d.ts.map +1 -0
- package/dist/told-listed.js +46 -0
- package/dist/told-listed.js.map +1 -0
- package/dist/told-panel-door.d.ts +17 -0
- package/dist/told-panel-door.d.ts.map +1 -0
- package/dist/told-panel-door.js +6 -0
- package/dist/told-panel-door.js.map +1 -0
- package/dist/told-panel.d.ts +19 -0
- package/dist/told-panel.d.ts.map +1 -0
- package/dist/told-panel.js +83 -0
- package/dist/told-panel.js.map +1 -0
- package/dist/told-ui.d.ts +5 -0
- package/dist/told-ui.d.ts.map +1 -0
- package/dist/told-ui.js +12 -0
- package/dist/told-ui.js.map +1 -0
- package/dist/workbench/ai-entries.d.ts.map +1 -1
- package/dist/workbench/ai-entries.js +23 -2
- package/dist/workbench/ai-entries.js.map +1 -1
- package/dist/workbench/back-out.js +1 -1
- package/dist/workbench/back-out.js.map +1 -1
- package/dist/workbench/inspector.d.ts.map +1 -1
- package/dist/workbench/inspector.js +218 -18
- package/dist/workbench/inspector.js.map +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { nounOf, pluralOf, Store } from "@graview/core";
|
|
2
|
+
import { planEdits } from "@graview/core/catalog";
|
|
3
|
+
const capital = (words) => words.charAt(0).toUpperCase() + words.slice(1);
|
|
4
|
+
const vowel = (word) => /^(?:[aeio]|u(?![sn]i|se|ser))/i.test(word);
|
|
5
|
+
const slug = (words) => words.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "one";
|
|
6
|
+
const range = (from, to) => (from === to ? `step ${from}` : `steps ${from}–${to}`);
|
|
7
|
+
/** The app's kinds as a document reads them, for an app declared in code with none remembered: their words, fields and links, no acts. */
|
|
8
|
+
function documentOfSchema(env) {
|
|
9
|
+
const schema = env.store.schema;
|
|
10
|
+
const kinds = Object.fromEntries(schema.definitions.map((definition) => [
|
|
11
|
+
definition.kind,
|
|
12
|
+
{ noun: nounOf(definition, definition.kind), plural: pluralOf(schema, definition.kind), fields: Object.fromEntries(Object.keys(definition.fields.shape ?? {}).map((field) => [field, { type: "string" }])), edges: definition.edges ?? {} },
|
|
13
|
+
]));
|
|
14
|
+
return { name: env.app.name ?? "app", kinds, acts: {} };
|
|
15
|
+
}
|
|
16
|
+
/** Who may change the app, in words: the roles every act is granted to ("an owner"), else whoever may. */
|
|
17
|
+
function whoMayChange(env) {
|
|
18
|
+
const owners = (env.store.policy?.grants ?? []).filter((grant) => grant.mutations === "*" && !grant.self && grant.roles !== "*").flatMap((grant) => grant.roles);
|
|
19
|
+
const named = [...new Set(owners)];
|
|
20
|
+
return named.length > 0 ? `${vowel(named[0]) ? "an" : "a"} ${named.join(" or ")}` : "whoever may change this app";
|
|
21
|
+
}
|
|
22
|
+
/** The place the list is, as the steps say it: "in This week", else "here". */
|
|
23
|
+
function whereOf(env) {
|
|
24
|
+
const place = env.place ? env.places.find((one) => one.as === env.place) : undefined;
|
|
25
|
+
return place ? `in ${place.title}` : "here";
|
|
26
|
+
}
|
|
27
|
+
/** The plan for a missing capability, or a refusal in words when it could not be composed or would fail. */
|
|
28
|
+
export async function planFor(env, options) {
|
|
29
|
+
const { missing } = options;
|
|
30
|
+
if (!missing.need)
|
|
31
|
+
return { refused: missing.said };
|
|
32
|
+
const reshape = await import("@graview/tools/reshape");
|
|
33
|
+
const remembered = reshape.documentOfApp(env.app);
|
|
34
|
+
const document = remembered ?? documentOfSchema(env);
|
|
35
|
+
const planned = planEdits(document, missing.need);
|
|
36
|
+
if ("refused" in planned)
|
|
37
|
+
return { refused: `${missing.said} ${planned.refused}` };
|
|
38
|
+
const steps = planned.steps.map((said) => ({ part: "app", said }));
|
|
39
|
+
const appSteps = steps.length;
|
|
40
|
+
/* THE RECORDS THE PLAN ACTS ON are of the kind it changes: a list given where its tasks were meant is retargeted by code to the records it holds, never refused in words. */
|
|
41
|
+
const ofKind = (ids) => ids.filter((id) => env.store.graph.getNode(id)?.kind === planned.on);
|
|
42
|
+
const heldOfKind = (id) => [...env.store.graph.in(id), ...env.store.graph.out(id)].filter((node) => node.kind === planned.on).map((node) => node.id);
|
|
43
|
+
const members = ofKind(options.members).length > 0 ? ofKind(options.members) : [...new Set(options.members.flatMap(heldOfKind))];
|
|
44
|
+
/* THE RECORDS THAT FINISH THE ASK, each said as it will happen. */
|
|
45
|
+
const records = [];
|
|
46
|
+
const count = members.length;
|
|
47
|
+
const kindDoc = (kind) => document.kinds[kind] ?? undefined;
|
|
48
|
+
const plural = (kind) => (kindDoc(kind)?.plural ?? `${kindDoc(kind)?.noun ?? kind}s`).toLowerCase();
|
|
49
|
+
const noun = (kind) => (kindDoc(kind)?.noun ?? kind).toLowerCase();
|
|
50
|
+
const where = options.where ? `in ${options.where}` : whereOf(env);
|
|
51
|
+
if (count === 0)
|
|
52
|
+
return { refused: `${missing.said} There is nothing here to do it to.` };
|
|
53
|
+
const these = count === 1 ? `the ${noun(planned.on)} ${where}` : `the ${count} ${plural(planned.on)} ${where}`;
|
|
54
|
+
const called = capital(planned.called);
|
|
55
|
+
if (planned.relation && planned.to) {
|
|
56
|
+
const who = missing.who;
|
|
57
|
+
if (!who)
|
|
58
|
+
return { refused: `${missing.said} Say whom to give them to.` };
|
|
59
|
+
const toPlural = planned.made ? capital(planned.edits[0].plural ?? "people") : capital(plural(planned.to));
|
|
60
|
+
const found = planned.made ? undefined : env.store.graph.allNodes().find((node) => node.kind === planned.to && Object.values(node).some((value) => typeof value === "string" && value.toLowerCase() === who.toLowerCase()));
|
|
61
|
+
let whom = found?.id;
|
|
62
|
+
if (!whom) {
|
|
63
|
+
whom = `${planned.to}-${slug(who)}`;
|
|
64
|
+
const makes = planned.makes ?? env.store.allMutations().find((mutation) => mutation.creates?.includes(planned.to))?.name;
|
|
65
|
+
if (!makes) {
|
|
66
|
+
const some = env.store.graph.allNodes().filter((node) => node.kind === planned.to).slice(0, 3).map((node) => String(node["label"] ?? node["name"] ?? "")).filter(Boolean);
|
|
67
|
+
return { refused: `${missing.said} There is nobody called ${who} among the ${toPlural.toLowerCase()} here${some.length > 0 ? ` (${some.join(", ")})` : ""}, and no way to add one from here.` };
|
|
68
|
+
}
|
|
69
|
+
records.push({ name: makes, args: { name: who }, mintedHint: whom });
|
|
70
|
+
steps.push({ part: "record", said: `Add ${who} to ${toPlural}.` });
|
|
71
|
+
}
|
|
72
|
+
for (const id of members)
|
|
73
|
+
records.push({ name: planned.act, args: { id, [planned.arg]: whom } });
|
|
74
|
+
// "assigned to" → "Assign Nick to the 5 tasks in This week."; any other link, said as setting it.
|
|
75
|
+
const verb = /^(\w+?)ed to$/i.exec(planned.called)?.[1];
|
|
76
|
+
steps.push({ part: "record", said: verb ? `${capital(verb.toLowerCase())} ${who} to ${these}.` : `Set “${called}” to ${who} on ${these}.` });
|
|
77
|
+
}
|
|
78
|
+
else if (planned.field) {
|
|
79
|
+
const value = missing.value;
|
|
80
|
+
if (value === undefined)
|
|
81
|
+
return { refused: `${missing.said} Say what to set it to.` };
|
|
82
|
+
for (const id of members)
|
|
83
|
+
records.push({ name: planned.act, args: { id, [planned.arg]: value } });
|
|
84
|
+
steps.push({ part: "record", said: `Set “${called}” to ${value} on ${these}.` });
|
|
85
|
+
}
|
|
86
|
+
const numbered = steps.length;
|
|
87
|
+
/* WHO MAKES THE FIRST STEPS: this seat, through the host — or, said once, whoever may. */
|
|
88
|
+
const may = Boolean(remembered) && options.host && options.compose !== undefined && reshape.mayChangeDeclaration(env.store, env.principal);
|
|
89
|
+
if (!may) {
|
|
90
|
+
const whoMay = !remembered ? "whoever develops this app" : !options.host || !options.compose ? "whoever hosts this app" : whoMayChange(env);
|
|
91
|
+
return { question: `${missing.said} ${capital(whoMay)} would add ${range(1, appSteps)}; then I can do ${range(appSteps + 1, numbered)}.`, parts: steps, whoMay };
|
|
92
|
+
}
|
|
93
|
+
/* COMPOSED AND REHEARSED WHOLE before it is offered: the changed app, judged by the checker, and these records in it, as this person. */
|
|
94
|
+
const composed = options.compose({ document: remembered, edits: planned.edits });
|
|
95
|
+
// A checker's sentence is never a person's: the change could not be made, said plainly.
|
|
96
|
+
if (!composed.ok)
|
|
97
|
+
return { refused: `${missing.said} That change could not be made here.` };
|
|
98
|
+
const app = composed.app;
|
|
99
|
+
try {
|
|
100
|
+
const scratch = new Store({ schema: app.schema, mutations: app.mutations ?? [], invariants: app.invariants ?? [], ...(app.policy ? { policy: app.policy } : {}), snapshot: env.store.graph.snapshot() });
|
|
101
|
+
scratch.previewAll(records, { author: env.principal });
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return { refused: `${missing.said} I could not make that work here.` };
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
question: `${missing.said} I can do it in ${numbered} steps. Should I?`,
|
|
108
|
+
parts: steps,
|
|
109
|
+
proposal: { said: options.said, add: [], drop: [], card: { said: steps.map((one) => one.said).join(" "), adds: [], keeps: [], drops: [], breaking: [] }, edits: planned.edits, records, via: `ai:${env.ai.name ?? "model"}` },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=quick-ask-plan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quick-ask-plan.js","sourceRoot":"","sources":["../src/quick-ask-plan.ts"],"names":[],"mappings":"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"}
|
package/dist/quick-ask.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quick-ask.d.ts","sourceRoot":"","sources":["../src/quick-ask.tsx"],"names":[],"mappings":"AAUA,OAAO,EAAgB,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"quick-ask.d.ts","sourceRoot":"","sources":["../src/quick-ask.tsx"],"names":[],"mappings":"AAUA,OAAO,EAAgB,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAMzE,OAAO,YAAY,CAAC;AAiBpB;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;CAC5B;AAiED,wBAAgB,YAAY,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,iBAAiB,+BA2kBvE"}
|
package/dist/quick-ask.js
CHANGED
|
@@ -1,16 +1,63 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { layer, pluralOf } from "@graview/core";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { layer, nameOfAuthor, pluralOf } from "@graview/core";
|
|
3
3
|
import { aiTargetOf, catalogOf, existingViewsOf, recordsOfTarget } from "@graview/core/catalog";
|
|
4
4
|
import { search } from "@graview/core/search";
|
|
5
5
|
import { useAiDeskState, useGraview } from "@graview/react/provider";
|
|
6
6
|
import { applyAffordance, deriveAffordances, previewAffordance } from "@graview/tools/derive";
|
|
7
|
-
import { answerHere, targetWords } from "@graview/tools/inline-ai";
|
|
7
|
+
import { answerHere, plainly, targetWords } from "@graview/tools/inline-ai";
|
|
8
8
|
import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
9
9
|
import { askOf, drawPanel, GhostTray, takeOffer } from "./inline-ai-menu.js";
|
|
10
10
|
import { useInlineEnv } from "./inline-ai-env.js";
|
|
11
11
|
import { openQuickAsk } from "./quick-ask-desk.js";
|
|
12
12
|
import { VISUALLY_HIDDEN } from "./primitives/measure.js";
|
|
13
|
+
import { retryingImport } from "@graview/core/retry";
|
|
14
|
+
import { lazyModule } from "@graview/react/provider";
|
|
13
15
|
import "./frame.js"; // A door: tied to what the page drew first (TIED DOORS, packages/primitives/src/frame.ts).
|
|
16
|
+
/*
|
|
17
|
+
* WHAT WAS TOLD, FETCHED APART (Roadmap f-menu-learns): the telling, its
|
|
18
|
+
* thumbs and the like ones that worked are a chunk of their own, asked for
|
|
19
|
+
* once the card is open and the page is idle, and only where the host keeps
|
|
20
|
+
* tellings — never in the way of the bar, the answer or its first paint.
|
|
21
|
+
*/
|
|
22
|
+
const toldCard = /* @__PURE__ */ lazyModule(retryingImport(() => import("./told-ui.js")));
|
|
23
|
+
const Rate = /* @__PURE__ */ toldCard.part((module, props) => _jsx(module.RateTelling, { ...props }), { quiet: true });
|
|
24
|
+
/** After the next paint: what is kept never holds up what is drawn. */
|
|
25
|
+
const afterPaint = (run) => {
|
|
26
|
+
if (typeof requestAnimationFrame === "function")
|
|
27
|
+
requestAnimationFrame(() => setTimeout(run, 0));
|
|
28
|
+
else
|
|
29
|
+
setTimeout(run, 0);
|
|
30
|
+
};
|
|
31
|
+
const freshTellingId = () => `tl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
32
|
+
/** Words in the follow-up that say yes, or no, to a plan the card asks about. */
|
|
33
|
+
const YES = /^(yes|yep|yeah|sure|ok(ay)?|do it|go ahead|please do)\b/i;
|
|
34
|
+
const NO = /^(no|nope|don'?t|not now|never ?mind)\b/i;
|
|
35
|
+
/**
|
|
36
|
+
* THE RECORDS A LIST OR A PLACE SHOWS (Roadmap f-menu-type-anything): what
|
|
37
|
+
* "these" and "all the tasks in this list" mean — the ones drawn on the
|
|
38
|
+
* page, of the target's kind where it has one; on the scene, every record
|
|
39
|
+
* of the kind this reader may see.
|
|
40
|
+
*/
|
|
41
|
+
function membersOf(store, target, face) {
|
|
42
|
+
const kind = target.on === "list" ? target.kind : target.on === "lens" ? target.kind : undefined;
|
|
43
|
+
// A selection is those records; what a record holds the Fast slot reads from the graph (`decideOverTheGraph`).
|
|
44
|
+
if (target.on === "selection")
|
|
45
|
+
return target.ids;
|
|
46
|
+
if (target.on !== "list" && target.on !== "lens")
|
|
47
|
+
return [];
|
|
48
|
+
if (face === "pages" && typeof document !== "undefined") {
|
|
49
|
+
const ids = new Set();
|
|
50
|
+
for (const element of document.querySelectorAll("main [data-graview-listed], main [data-graview-record], main [data-graview-pick]")) {
|
|
51
|
+
const id = element.getAttribute("data-graview-listed") ?? element.getAttribute("data-graview-record") ?? element.getAttribute("data-graview-pick");
|
|
52
|
+
const node = id ? store.graph.getNode(id) : undefined;
|
|
53
|
+
if (id && node && (kind === undefined || node.kind === kind))
|
|
54
|
+
ids.add(id);
|
|
55
|
+
}
|
|
56
|
+
if (ids.size > 0)
|
|
57
|
+
return [...ids];
|
|
58
|
+
}
|
|
59
|
+
return kind ? store.graph.allNodes().filter((node) => node.kind === kind).map((node) => node.id) : [];
|
|
60
|
+
}
|
|
14
61
|
/** Words that say what kind of row is wanted, not which: "show", "go to", "open". */
|
|
15
62
|
const LEADING = new Set(["show", "go", "to", "open", "the", "a", "an", "me"]);
|
|
16
63
|
const folded = (text) => text.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "");
|
|
@@ -28,7 +75,8 @@ const WIDTH = 360;
|
|
|
28
75
|
const GAP = 12;
|
|
29
76
|
export function QuickAskCard({ face, opening, close }) {
|
|
30
77
|
const env = useInlineEnv();
|
|
31
|
-
const { setMenuAt } = useGraview();
|
|
78
|
+
const { setMenuAt, aiHere, session, people, seats, onProposeChange, composeChange } = useGraview();
|
|
79
|
+
const [planState, setPlanState] = useState({ state: "offered" });
|
|
32
80
|
const target = useMemo(() => opening.target ?? aiTargetOf(opening.pointed ?? { about: [] }, (id) => env.store.graph.has(id)), [opening, env.store]);
|
|
33
81
|
const about = useMemo(() => targetWords(env.store, target, env.places), [env.store, target, env.places]);
|
|
34
82
|
const [words, setWords] = useState("");
|
|
@@ -37,16 +85,77 @@ export function QuickAskCard({ face, opening, close }) {
|
|
|
37
85
|
const [said, setSaid] = useState(null);
|
|
38
86
|
const pane = useRef(null);
|
|
39
87
|
const field = useRef(null);
|
|
88
|
+
const mark = useRef(null);
|
|
40
89
|
const list = useId();
|
|
41
90
|
const live = useRef(true);
|
|
42
91
|
const { ghost: held } = useAiDeskState(env.desk);
|
|
92
|
+
/* WHAT WAS TOLD (Roadmap f-menu-learns): this card's latest telling, kept with the host's door, and the watch on how it ends. */
|
|
93
|
+
const [telling, setTelling] = useState(null);
|
|
94
|
+
const told = useRef(null);
|
|
95
|
+
/* A change to this card's telling: handed on once it is kept, held until then. */
|
|
96
|
+
const toldSay = useCallback((change) => {
|
|
97
|
+
const now = told.current;
|
|
98
|
+
if (!now || !env.ai.told)
|
|
99
|
+
return;
|
|
100
|
+
if (now.kept && toldCard.current)
|
|
101
|
+
toldCard.current.changeTelling(env.ai.told, now.id, change);
|
|
102
|
+
else
|
|
103
|
+
now.queued = { ...now.queued, ...change };
|
|
104
|
+
}, [env.ai.told]);
|
|
105
|
+
/* Open, and the page idle: the telling's chunk and what the host has kept, so an Enter has its examples at hand without waiting. */
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const keeps = env.ai.told;
|
|
108
|
+
if (!keeps)
|
|
109
|
+
return;
|
|
110
|
+
const idle = globalThis;
|
|
111
|
+
const go = () => void toldCard.load().then((module) => module.toldListed(keeps), () => undefined);
|
|
112
|
+
if (idle.requestIdleCallback) {
|
|
113
|
+
const asked = idle.requestIdleCallback(go, { timeout: 1500 });
|
|
114
|
+
return () => idle.cancelIdleCallback?.(asked);
|
|
115
|
+
}
|
|
116
|
+
const later = setTimeout(go, 200);
|
|
117
|
+
return () => clearTimeout(later);
|
|
118
|
+
}, [env.ai.told]);
|
|
43
119
|
useEffect(() => () => {
|
|
44
120
|
live.current = false;
|
|
121
|
+
// Put away with nothing done: abandoned — unless a change it proposed still waits on the tray, whose answer the watch hears.
|
|
122
|
+
const last = told.current;
|
|
123
|
+
const waitingOn = env.desk.get().ghost;
|
|
124
|
+
if (last && !(waitingOn && waitingOn.ghost.interaction === last.interaction && waitingOn.batch === undefined))
|
|
125
|
+
toldSay({ outcome: "abandoned" });
|
|
45
126
|
// Put away with a change still proposed on it: the change waits on the tray at the foot instead.
|
|
46
127
|
const waiting = env.desk.get().ghost;
|
|
47
128
|
if (waiting?.onCard)
|
|
48
129
|
env.desk.setGhost({ ...waiting, onCard: false });
|
|
49
130
|
}, [env.desk]);
|
|
131
|
+
/*
|
|
132
|
+
* GRAVIEW IS HERE WHILE IT THINKS (Roadmap f-menu-type-anything): from the
|
|
133
|
+
* words asked until this card is put away — its close, Escape, a press
|
|
134
|
+
* away, an answer taken back — the app's AI stands among who is here, as
|
|
135
|
+
* a bot editing does: "Graview, for Ada", at what it was asked about, so
|
|
136
|
+
* the bar says it and the picture marks the thing; the others are told
|
|
137
|
+
* on the presence channel where there is one. A page that dies leaves it
|
|
138
|
+
* to lapse by the presence's own time.
|
|
139
|
+
*/
|
|
140
|
+
const thinking = answering !== null;
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (!thinking)
|
|
143
|
+
return;
|
|
144
|
+
const at = recordsOfTarget(target)[0] ?? (target.on === "list" ? `kind:${target.kind}` : target.on === "lens" && target.kind ? `kind:${target.kind}` : null);
|
|
145
|
+
const person = env.principal;
|
|
146
|
+
const name = person.id ? nameOfAuthor(person, { graph: env.store.graph, schema: env.store.schema, ...(seats ? { seats } : {}), ...(people ? { people } : {}) }) : undefined;
|
|
147
|
+
aiHere({
|
|
148
|
+
participant: `agent:graview-for-${person.id ?? "anyone"}:${session}`,
|
|
149
|
+
kind: "agent",
|
|
150
|
+
name: "Graview",
|
|
151
|
+
...(person.id ? { onBehalfOf: person.id, ...(name ? { onBehalfOfName: name } : {}) } : {}),
|
|
152
|
+
over: at,
|
|
153
|
+
robot: { at, mode: "thinking" },
|
|
154
|
+
});
|
|
155
|
+
return () => aiHere(null);
|
|
156
|
+
// Stands from the ask to the card's close; where it is said once, as it began.
|
|
157
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
158
|
+
}, [thinking]);
|
|
50
159
|
/* THE CATALOG OF THIS TARGET: its panels ("Show …") and places, and the acts the derivation offers this viewer on its records. */
|
|
51
160
|
const catalog = useMemo(() => {
|
|
52
161
|
const ask = askOf(env, target);
|
|
@@ -94,19 +203,119 @@ export function QuickAskCard({ face, opening, close }) {
|
|
|
94
203
|
const asked = question.trim();
|
|
95
204
|
if (!asked)
|
|
96
205
|
return;
|
|
97
|
-
|
|
206
|
+
// A reply carries what it replies to: the words, the answer, and which records they were read as being about.
|
|
207
|
+
const before = answering?.result?.answer ? { said: answering.said, answer: answering.result.answer, ...(answering.result.reading ? { reading: answering.result.reading.which } : {}) } : undefined;
|
|
208
|
+
const place = target.on === "lens" ? env.places.find((one) => one.as === target.place)?.title : target.on === "list" ? undefined : undefined;
|
|
98
209
|
setWords("");
|
|
99
210
|
setActive(-1);
|
|
100
211
|
setSaid(null);
|
|
101
212
|
setAnswering({ said: asked, result: null });
|
|
102
|
-
|
|
213
|
+
setPlanState({ state: "offered" });
|
|
214
|
+
const members = membersOf(env.store, target, face);
|
|
215
|
+
/* A reply on the card follows the telling before it, in one conversation; one still only previewed was not taken. */
|
|
216
|
+
const earlier = told.current;
|
|
217
|
+
if (earlier)
|
|
218
|
+
toldSay({ outcome: "abandoned" });
|
|
219
|
+
const started = Date.now();
|
|
220
|
+
// The like 👍 tellings of this app, given to Jev and the light model as examples (Roadmap f-menu-learns): only what is at hand, never waited for.
|
|
221
|
+
// Already fetched (by the menu, or as this card opened) it is here in a tick; otherwise not waited for.
|
|
222
|
+
const toldHere = env.ai.told ? (toldCard.current ?? (await Promise.race([toldCard.load().catch(() => undefined), new Promise((resolve) => setTimeout(resolve, 50))]))) : undefined;
|
|
223
|
+
const like = env.ai.told && toldHere ? toldHere.examplesHere(env.ai.told, env.store, env.principal, asked, target) : { examples: [], ids: [] };
|
|
224
|
+
const examples = [...new Set([...(earlier ? [] : (opening.suggested ? [opening.suggested] : [])), ...like.ids])];
|
|
225
|
+
const result = await answerHere(env.ai, {
|
|
226
|
+
...askOf(env, target, asked),
|
|
227
|
+
said: asked,
|
|
228
|
+
...(before ? { follows: before } : {}),
|
|
229
|
+
...(members.length > 0 ? { members } : {}),
|
|
230
|
+
...(place ? { where: place } : {}),
|
|
231
|
+
...(like.examples.length > 0 ? { examples: like.examples } : {}),
|
|
232
|
+
});
|
|
103
233
|
env.desk.record(result.interaction);
|
|
104
234
|
// Its proposed change is a ghost, drawn on this card: accepted there as one batch, with its one undo.
|
|
105
235
|
if (result.ghost && result.ghost.proposals.length > 0)
|
|
106
236
|
env.desk.setGhost({ ghost: result.ghost, onCard: true });
|
|
237
|
+
/* The app lacks it: the whole plan that would finish the ask, composed and rehearsed before it is said. */
|
|
238
|
+
let plan;
|
|
239
|
+
if (result.missing) {
|
|
240
|
+
try {
|
|
241
|
+
const { planFor } = await import("./quick-ask-plan.js");
|
|
242
|
+
// The records the words were read as being about, where the plan says it.
|
|
243
|
+
const from = Date.now();
|
|
244
|
+
const where = place ?? (result.reading && result.reading.which === "held" && target.on === "record" ? result.reading.words.replace(/^the \d+ \S+ in /, "") : undefined);
|
|
245
|
+
plan = await planFor(env, { said: asked, missing: result.missing, members: result.reading?.records ?? members, host: onProposeChange !== undefined, ...(composeChange ? { compose: composeChange } : {}), ...(where ? { where } : {}) });
|
|
246
|
+
// The plan composed and rehearsed, timed beside the slots' stages.
|
|
247
|
+
env.desk.record({ ...result.interaction, stages: [...(result.interaction.stages ?? []), { stage: "compose", ms: Date.now() - from }] });
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
// The error is the pipeline's; the person is told plainly.
|
|
251
|
+
plan = { refused: `${result.missing.said} That change could not be made here.` };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
107
254
|
if (live.current)
|
|
108
|
-
setAnswering({ said: asked, result });
|
|
109
|
-
|
|
255
|
+
setAnswering({ said: asked, result, ...(plan ? { plan } : {}) });
|
|
256
|
+
/*
|
|
257
|
+
* KEPT AS A TELLING, AFTER THE ANSWER IS DRAWN: what was said and
|
|
258
|
+
* decided and how long it took, its id now so how it ends — said as
|
|
259
|
+
* the person goes on — is held until it is kept.
|
|
260
|
+
*/
|
|
261
|
+
const keeps = env.ai.told;
|
|
262
|
+
if (!keeps)
|
|
263
|
+
return;
|
|
264
|
+
const ms = Date.now() - started;
|
|
265
|
+
told.current?.stop?.();
|
|
266
|
+
const now = { id: freshTellingId(), conversation: earlier?.conversation ?? "", interaction: result.interaction.id };
|
|
267
|
+
told.current = { ...now, conversation: now.conversation || now.id };
|
|
268
|
+
const mine = told.current;
|
|
269
|
+
afterPaint(() => void toldCard.load().then((module) => {
|
|
270
|
+
const kept = module.keepTelling(keeps, { ...module.tellingDraftOf(env.store, { words: asked, target, result, ...(plan ? { plan } : {}), ms, ...(earlier ? { conversation: earlier.conversation, follows: earlier.id } : {}), examples }), id: mine.id, conversation: mine.conversation }, env.principal);
|
|
271
|
+
if (!kept)
|
|
272
|
+
return;
|
|
273
|
+
mine.kept = true;
|
|
274
|
+
if (mine.queued)
|
|
275
|
+
module.changeTelling(keeps, kept.id, mine.queued);
|
|
276
|
+
mine.stop = module.watchTelling(env.desk, keeps, kept);
|
|
277
|
+
if (live.current && told.current === mine)
|
|
278
|
+
setTelling(kept);
|
|
279
|
+
}, () => undefined));
|
|
280
|
+
}, [answering, env, target, face, onProposeChange, composeChange, opening.suggested, toldSay]);
|
|
281
|
+
/*
|
|
282
|
+
* YES: THE PLAN LANDS AS ONE CHANGE. The host applies the change to the
|
|
283
|
+
* app and the records that finish the ask together, as the person, and
|
|
284
|
+
* hands back its one way back; a host that declines is said on the card.
|
|
285
|
+
*/
|
|
286
|
+
const offered = answering?.plan && "question" in answering.plan ? answering.plan : undefined;
|
|
287
|
+
const yes = useCallback(async () => {
|
|
288
|
+
const proposal = offered?.proposal;
|
|
289
|
+
if (!proposal || !onProposeChange || planState.state !== "offered")
|
|
290
|
+
return;
|
|
291
|
+
setPlanState({ state: "applying" });
|
|
292
|
+
try {
|
|
293
|
+
const answered = await onProposeChange(proposal);
|
|
294
|
+
if (!live.current)
|
|
295
|
+
return;
|
|
296
|
+
toldSay({ outcome: answered === "declined" ? "declined" : "accepted" });
|
|
297
|
+
if (answered === "declined")
|
|
298
|
+
setPlanState({ state: "declined", said: "The host declined this change to the app." });
|
|
299
|
+
else
|
|
300
|
+
setPlanState({ state: "applied", ...(typeof answered === "object" ? { undo: answered.undo, ...(answered.note ? { note: answered.note } : {}) } : {}) });
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
if (live.current)
|
|
304
|
+
setPlanState({ state: "declined", said: error instanceof Error ? error.message : String(error) });
|
|
305
|
+
}
|
|
306
|
+
}, [offered, onProposeChange, planState, toldSay]);
|
|
307
|
+
const no = useCallback(() => {
|
|
308
|
+
toldSay({ outcome: "declined" });
|
|
309
|
+
setPlanState({ state: "declined", said: "Left as it is." });
|
|
310
|
+
}, [toldSay]);
|
|
311
|
+
const undoPlan = useCallback(async () => {
|
|
312
|
+
if (planState.state !== "applied" || !planState.undo)
|
|
313
|
+
return;
|
|
314
|
+
await planState.undo();
|
|
315
|
+
toldSay({ outcome: "undone" });
|
|
316
|
+
if (live.current)
|
|
317
|
+
setPlanState({ state: "undone" });
|
|
318
|
+
}, [planState, toldSay]);
|
|
110
319
|
/* Opened with words already (Find's "Ask: ‘…’", Activity's "Ask again"): asked at once. */
|
|
111
320
|
const first = useRef(opening.asked);
|
|
112
321
|
useEffect(() => {
|
|
@@ -202,13 +411,34 @@ export function QuickAskCard({ face, opening, close }) {
|
|
|
202
411
|
}
|
|
203
412
|
field.current?.focus({ preventScroll: true });
|
|
204
413
|
let frame = 0;
|
|
414
|
+
let marked = null;
|
|
415
|
+
/* OPENED FROM THE MENU, IT OPENS WHERE THE MENU STOOD and never moves: placed once, before it is first painted. */
|
|
416
|
+
const from = opening.from;
|
|
417
|
+
if (from)
|
|
418
|
+
pinAt(element, from);
|
|
205
419
|
const follow = () => {
|
|
206
|
-
|
|
420
|
+
const anchor = anchorOf(element, target, face);
|
|
421
|
+
if (from)
|
|
422
|
+
sayBeside(element, anchor);
|
|
423
|
+
else
|
|
424
|
+
placeBeside(element, anchor);
|
|
425
|
+
// The thinking mark, on the thing itself while the answer is on its way.
|
|
426
|
+
const on = mark.current ? anchor : null;
|
|
427
|
+
if (marked !== on) {
|
|
428
|
+
marked?.removeAttribute("data-graview-ai-thinking");
|
|
429
|
+
on?.setAttribute("data-graview-ai-thinking", "");
|
|
430
|
+
marked = on;
|
|
431
|
+
}
|
|
432
|
+
if (mark.current)
|
|
433
|
+
placeOn(mark.current, on);
|
|
207
434
|
frame = requestAnimationFrame(follow);
|
|
208
435
|
};
|
|
209
436
|
follow();
|
|
210
|
-
return () =>
|
|
211
|
-
|
|
437
|
+
return () => {
|
|
438
|
+
cancelAnimationFrame(frame);
|
|
439
|
+
marked?.removeAttribute("data-graview-ai-thinking");
|
|
440
|
+
};
|
|
441
|
+
}, [target, face, opening.from]);
|
|
212
442
|
/* A press outside the bar puts it away; an answer stays until it is closed. */
|
|
213
443
|
useEffect(() => {
|
|
214
444
|
if (answering)
|
|
@@ -235,28 +465,52 @@ export function QuickAskCard({ face, opening, close }) {
|
|
|
235
465
|
const row = rows[active];
|
|
236
466
|
if (row)
|
|
237
467
|
row.take();
|
|
468
|
+
// A plan the card asks about takes its answer in words too: "yes", "no"; anything else revises it.
|
|
469
|
+
else if (offered?.proposal && planState.state === "offered" && YES.test(words.trim())) {
|
|
470
|
+
setWords("");
|
|
471
|
+
void yes();
|
|
472
|
+
}
|
|
473
|
+
else if (offered && planState.state === "offered" && NO.test(words.trim())) {
|
|
474
|
+
setWords("");
|
|
475
|
+
no();
|
|
476
|
+
}
|
|
238
477
|
else if (words.trim())
|
|
239
478
|
void ask(words);
|
|
240
479
|
}
|
|
241
480
|
};
|
|
242
481
|
const result = answering?.result ?? null;
|
|
243
482
|
const lit = rows[active];
|
|
244
|
-
return (_jsxs("section", { ref: pane, popover: "manual", role: "region", "aria-label": answering ? `Answer about ${about}` : `
|
|
483
|
+
return (_jsxs("section", { ref: pane, popover: "manual", role: "region", "aria-label": answering ? `Answer about ${about}` : `Tell Graview about ${about}`, "data-testid": "quick-ask", "data-quick-ask": answering ? "answer" : "bar", "data-quick-ask-face": face, "data-graview-offstage": "", "data-graview-overlay": "", onKeyDown: (event) => {
|
|
245
484
|
if (event.key !== "Escape")
|
|
246
485
|
return;
|
|
247
486
|
// The card's Escape is the card's: the scene's back-out does not hear it too.
|
|
248
487
|
event.preventDefault();
|
|
249
488
|
event.stopPropagation();
|
|
250
489
|
putAway();
|
|
251
|
-
}, onMouseDown: (event) => event.stopPropagation(), style: CARD, children: [_jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: 8 }, children: [_jsxs("p", { "data-testid": "quick-ask-about", style: { margin: 0, flex: "1 1 auto", minWidth: 0, fontSize: "0.75rem", color: "var(--graview-ink-muted)", overflowWrap: "anywhere" }, children: ["About ", about] }), _jsx("button", { type: "button", "data-testid": "quick-ask-close", "aria-label": "Close", title: "Close", onClick: putAway, style: ICON, children: _jsx("span", { "aria-hidden": "true", children: "\u2715" }) })] }), answering ? (_jsxs("div", { "data-testid": "quick-answer", style: { display: "grid", gap: 8 }, children: [_jsxs("p", { "data-testid": "quick-answer-asked", style: { margin: 0, fontSize: "0.8125rem", color: "var(--graview-ink-muted)", fontStyle: "italic", overflowWrap: "anywhere" }, children: ["\u201C", answering.said, "\u201D"] }), _jsx("p", { "data-testid": "quick-answer-text", "aria-live": "polite", style: { margin: 0, fontSize: "0.9375rem", lineHeight: 1.45, color: result?.interaction.outcome === "failed" ? "var(--graview-warn)" : "var(--graview-ink)", overflowWrap: "anywhere" }, children: result === null
|
|
490
|
+
}, onMouseDown: (event) => event.stopPropagation(), style: CARD, children: [_jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: 8 }, children: [_jsxs("p", { "data-testid": "quick-ask-about", style: { margin: 0, flex: "1 1 auto", minWidth: 0, fontSize: "0.75rem", color: "var(--graview-ink-muted)", overflowWrap: "anywhere" }, children: ["About ", about] }), _jsx("button", { type: "button", "data-testid": "quick-ask-close", "aria-label": "Close", title: "Close", onClick: putAway, style: ICON, children: _jsx("span", { "aria-hidden": "true", children: "\u2715" }) })] }), answering ? (_jsxs("div", { "data-testid": "quick-answer", style: { display: "grid", gap: 8 }, children: [_jsxs("p", { "data-testid": "quick-answer-asked", style: { margin: 0, fontSize: "0.8125rem", color: "var(--graview-ink-muted)", fontStyle: "italic", overflowWrap: "anywhere" }, children: ["\u201C", answering.said, "\u201D"] }), _jsx("p", { "data-testid": "quick-answer-text", "aria-live": "polite", style: { margin: 0, fontSize: "0.9375rem", lineHeight: 1.45, color: result?.interaction.outcome === "failed" ? "var(--graview-warn)" : "var(--graview-ink)", overflowWrap: "anywhere" }, children: result === null
|
|
491
|
+
? "Thinking…"
|
|
492
|
+
: result.interaction.outcome === "failed"
|
|
493
|
+
? (result.interaction.failed ?? "The AI could not answer that here.")
|
|
494
|
+
: /* Every answer the card shows is a whole sentence for the person, never the pipeline's own note. */
|
|
495
|
+
plainly(answering.plan
|
|
496
|
+
? "question" in answering.plan
|
|
497
|
+
? answering.plan.question
|
|
498
|
+
: answering.plan.refused
|
|
499
|
+
: result.answer, result.ghost && result.ghost.proposals.length > 0 ? "Here is what I would change." : result.offers.length > 0 ? "These may help." : undefined) }), result?.taken ? (
|
|
500
|
+
/* A person said as a pronoun, and whom it was taken as, so a reply can correct it. */
|
|
501
|
+
_jsxs("p", { "data-testid": "quick-ask-taken", style: { margin: 0, fontSize: "0.8125rem", color: "var(--graview-ink-muted)" }, children: ["Taking \u201C", result.taken.said, "\u201D as ", result.taken.as, "."] })) : null, result?.reading?.close ? (
|
|
502
|
+
/* A close reading said, so a reply can flip it: "Taking this as the 6 tasks in This week." */
|
|
503
|
+
_jsxs("p", { "data-testid": "quick-ask-reading", style: { margin: 0, fontSize: "0.8125rem", color: "var(--graview-ink-muted)" }, children: ["Taking this as ", result.reading.words, "."] })) : null, offered ? (
|
|
504
|
+
/* The plan, whole, before it lands: each part said, Yes and No — or, for a seat that may not change the app, who could. */
|
|
505
|
+
_jsxs("section", { "data-testid": "quick-ask-plan", "data-plan-state": planState.state, "aria-label": "What I would do", style: PLAN, children: [_jsx("ol", { style: { margin: 0, paddingInlineStart: 18, display: "grid", gap: 2 }, children: offered.parts.map((part, at) => (_jsx("li", { "data-plan-part": part.part, style: { fontStyle: planState.state === "applied" ? "normal" : "italic" }, children: part.said }, at))) }), offered.proposal && onProposeChange ? (planState.state === "offered" || planState.state === "applying" ? (_jsxs("div", { style: { display: "flex", gap: 6 }, children: [_jsx("button", { type: "button", "data-testid": "quick-ask-plan-yes", disabled: planState.state === "applying", onClick: () => void yes(), style: OFFER, children: "Yes" }), _jsx("button", { type: "button", "data-testid": "quick-ask-plan-no", disabled: planState.state === "applying", onClick: no, style: OFFER, children: "No" })] })) : planState.state === "applied" ? (_jsxs("div", { role: "status", style: { display: "flex", gap: 6, alignItems: "center" }, children: [_jsxs("span", { "data-testid": "quick-ask-plan-done", children: ["Done, as one change.", planState.note ? ` ${planState.note}` : ""] }), planState.undo ? (_jsx("button", { type: "button", "data-testid": "quick-ask-plan-undo", onClick: () => void undoPlan(), style: OFFER, children: "Undo" })) : null] })) : (_jsx("p", { role: "status", style: { margin: 0, color: "var(--graview-ink-muted)" }, children: planState.state === "undone" ? "Taken back." : planState.said }))) : null] })) : null, result && result.offers.length > 0 ? (_jsx("ul", { "aria-label": "What acts on it", style: { margin: 0, padding: 0, listStyle: "none", display: "flex", flexWrap: "wrap", gap: 6 }, children: result.offers.map((offer) => (_jsx("li", { children: _jsx("button", { type: "button", "data-testid": "quick-answer-offer", "data-ai-offer": offer.id, "data-ai-offer-type": offer.type, onClick: () => {
|
|
252
506
|
// Put away as it lands, the way this face goes; a failure is said on the card that offered it.
|
|
253
|
-
void takeOffer(env, result, offer, close).then((failed) => {
|
|
507
|
+
void takeOffer(env, result, offer, close, { onCard: offer.type === "act" || offer.type === "fill" }).then((failed) => {
|
|
254
508
|
if (failed && live.current)
|
|
255
509
|
setSaid(failed);
|
|
256
510
|
});
|
|
257
|
-
}, style: OFFER, children: offer.type === "panel" ? `Show ${sentenceCase(offer.label)}` : offer.type === "place" ? `Go to ${offer.label}` : offer.label }) }, offer.id))) })) : null, held?.onCard && held.ghost.interaction === result?.interaction.id ? _jsx(GhostTray, { placement: "card" }) : null] })) : null, _jsxs("div", { style: { display: "grid", gap: 4 }, children: [_jsx("input", { ref: field, type: "text", role: "combobox", "data-testid": "quick-ask-input", "aria-label": answering ? `
|
|
511
|
+
}, style: OFFER, children: offer.type === "panel" ? `Show ${sentenceCase(offer.label)}` : offer.type === "place" ? `Go to ${offer.label}` : offer.type === "act" || offer.type === "fill" ? `${offer.label.replace(/[.?]$/, "")}?` : offer.label }) }, offer.id))) })) : null, held?.onCard && held.ghost.interaction === result?.interaction.id ? _jsx(GhostTray, { placement: "card" }) : null, result && telling && env.ai.told && telling.interaction === result.interaction.id ? _jsx(Rate, { told: env.ai.told, telling: telling }) : null] })) : null, _jsxs("div", { style: { display: "grid", gap: 4 }, children: [_jsx("input", { ref: field, type: "text", role: "combobox", "data-testid": "quick-ask-input", "aria-label": answering ? `Reply about ${about}` : `Tell Graview about ${about}, or go somewhere`, "aria-expanded": rows.length > 0, "aria-controls": list, "aria-autocomplete": "list", ...(lit ? { "aria-activedescendant": `${list}-${active}` } : {}), autoComplete: "off", enterKeyHint: "go", placeholder: answering ? "Reply…" : "Tell Graview, or go to…", value: words,
|
|
258
512
|
// Read-only while it answers, never disabled: a disabled field lets go of the keyboard.
|
|
259
|
-
readOnly: answering !== null && result === null, "aria-busy": answering !== null && result === null, onChange: (event) => setWords(event.target.value), onKeyDown: onKeyDown, style: FIELD }), _jsx("div", { id: list, role: "listbox", "aria-label": words.trim() ? `What “${words.trim()}” finds here` : "What can be done here", hidden: rows.length === 0, style: { display: rows.length === 0 ? "none" : "grid", gap: 1, maxHeight: 280, overflowY: "auto" }, children: rows.map((row, at) => (_jsx("div", { id: `${list}-${at}`, role: "option", "aria-selected": at === active, "data-testid": "quick-ask-option", "data-quick-ask-kind": row.kind, "data-quick-ask-row": row.id, onMouseEnter: () => setActive(at), onMouseDown: (event) => event.preventDefault(), onClick: row.take, style: { ...OPTION, background: at === active ? "var(--graview-panel-muted)" : "transparent", ...(row.kind === "ask" ? { borderTop: "1px solid var(--graview-edge)", marginTop: 2 } : {}) }, children: row.kind === "ask" ? (_jsxs("span", { children: ["
|
|
513
|
+
readOnly: answering !== null && result === null, "aria-busy": answering !== null && result === null, onChange: (event) => setWords(event.target.value), onKeyDown: onKeyDown, style: FIELD }), _jsx("div", { id: list, role: "listbox", "aria-label": words.trim() ? `What “${words.trim()}” finds here` : "What can be done here", hidden: rows.length === 0, style: { display: rows.length === 0 ? "none" : "grid", gap: 1, maxHeight: 280, overflowY: "auto" }, children: rows.map((row, at) => (_jsx("div", { id: `${list}-${at}`, role: "option", "aria-selected": at === active, "data-testid": "quick-ask-option", "data-quick-ask-kind": row.kind, "data-quick-ask-row": row.id, onMouseEnter: () => setActive(at), onMouseDown: (event) => event.preventDefault(), onClick: row.take, style: { ...OPTION, background: at === active ? "var(--graview-panel-muted)" : "transparent", ...(row.kind === "ask" ? { borderTop: "1px solid var(--graview-edge)", marginTop: 2 } : {}) }, children: row.kind === "ask" ? (_jsxs("span", { children: ["Tell Graview: ", _jsxs("span", { style: { fontWeight: 600 }, children: ["\u2018", row.label, "\u2019"] })] })) : (_jsxs("span", { children: [row.label, row.hint ? _jsxs("span", { style: { color: "var(--graview-ink-faint)" }, children: [" \u00B7 ", row.hint] }) : null] })) }, row.id))) })] }), said ? (_jsx("p", { role: "alert", "data-testid": "quick-ask-refused", style: { margin: 0, fontSize: "0.8125rem", color: "var(--graview-warn)" }, children: said })) : null, _jsx("span", { style: VISUALLY_HIDDEN, "aria-live": "polite", children: rows.length > 0 && words.trim() ? `${rows.length} ${rows.length === 1 ? "way" : "ways"} to go on` : "" }), answering !== null && result === null ? (_jsxs("span", { ref: mark, role: "status", "data-testid": "ai-thinking-mark", style: THINKING, children: [_jsx("style", { children: "@keyframes graview-thinking{from{opacity:.35}to{opacity:1}}@media (prefers-reduced-motion:reduce){[data-testid=ai-thinking-mark]>span{animation:none!important}}" }), _jsx("span", { "aria-hidden": "true", style: THINKING_DOT }), _jsxs("span", { children: ["Thinking about ", about, "\u2026"] })] })) : null] }));
|
|
260
514
|
}
|
|
261
515
|
/**
|
|
262
516
|
* WHAT THE CARD STANDS BESIDE: the record's card or the kind's district on
|
|
@@ -268,8 +522,8 @@ function anchorOf(pane, target, face) {
|
|
|
268
522
|
const root = pane?.closest("[data-graview-embed]") ?? document;
|
|
269
523
|
const ids = target.on === "list" ? [`aggregate:${target.kind}`, `kind:${target.kind}`] : target.on === "lens" && target.kind ? [`aggregate:${target.kind}`] : recordsOfTarget(target);
|
|
270
524
|
for (const id of ids) {
|
|
271
|
-
// The record's own card where the picture draws one; else where it is picked from (a row of a district's list).
|
|
272
|
-
for (const element of root.querySelectorAll(`[data-graview-view="${CSS.escape(id)}"], [data-graview-pick="${CSS.escape(id)}"]`)) {
|
|
525
|
+
// The record's own card where the picture draws one; else where it is picked from (a row of a district's list, a page's row or card).
|
|
526
|
+
for (const element of root.querySelectorAll(`[data-graview-view="${CSS.escape(id)}"], [data-graview-pick="${CSS.escape(id)}"], [data-graview-record="${CSS.escape(id)}"], [data-graview-listed="${CSS.escape(id)}"]`)) {
|
|
273
527
|
if (pane?.contains(element))
|
|
274
528
|
continue;
|
|
275
529
|
const box = element.getBoundingClientRect();
|
|
@@ -322,15 +576,83 @@ function placeBeside(pane, anchor) {
|
|
|
322
576
|
style.top = next.top;
|
|
323
577
|
if (style.width !== next.width)
|
|
324
578
|
style.width = next.width;
|
|
325
|
-
const beside = anchor?.getAttribute("data-graview-view") ?? anchor?.getAttribute("data-graview-pick") ?? (anchor ? "page" : "");
|
|
579
|
+
const beside = anchor?.getAttribute("data-graview-view") ?? anchor?.getAttribute("data-graview-pick") ?? anchor?.getAttribute("data-graview-record") ?? anchor?.getAttribute("data-graview-listed") ?? (anchor ? "page" : "");
|
|
326
580
|
if (pane.getAttribute("data-quick-ask-beside") !== beside)
|
|
327
581
|
pane.setAttribute("data-quick-ask-beside", beside);
|
|
328
582
|
if (pane.getAttribute("data-quick-ask-side") !== side)
|
|
329
583
|
pane.setAttribute("data-quick-ask-side", side);
|
|
330
584
|
}
|
|
585
|
+
/**
|
|
586
|
+
* WHERE THE MENU STOOD: the card's top-left corner is the menu's, growing
|
|
587
|
+
* down and right; it moves left only where it would leave the window, and
|
|
588
|
+
* up only where too little of the window is left below it — once, here,
|
|
589
|
+
* before it is painted. What it holds grows inside it and scrolls past the
|
|
590
|
+
* window's foot; nothing that arrives moves it.
|
|
591
|
+
*/
|
|
592
|
+
function pinAt(pane, from) {
|
|
593
|
+
const view = { width: document.documentElement.clientWidth || innerWidth, height: innerHeight };
|
|
594
|
+
const width = Math.min(WIDTH, view.width - 2 * GAP);
|
|
595
|
+
const left = from.left + width > view.width - GAP ? Math.max(GAP, view.width - GAP - width) : Math.max(GAP, from.left);
|
|
596
|
+
const room = 240;
|
|
597
|
+
const top = from.top + room > view.height - GAP ? Math.max(GAP, view.height - GAP - room) : Math.max(GAP, from.top);
|
|
598
|
+
pane.style.left = `${Math.round(left)}px`;
|
|
599
|
+
pane.style.top = `${Math.round(top)}px`;
|
|
600
|
+
pane.style.width = `${Math.round(width)}px`;
|
|
601
|
+
pane.style.maxHeight = `${Math.round(view.height - top - GAP)}px`;
|
|
602
|
+
pane.setAttribute("data-quick-ask-side", "where-the-menu-stood");
|
|
603
|
+
pane.setAttribute("data-quick-ask-from", `${Math.round(from.left)},${Math.round(from.top)}`);
|
|
604
|
+
}
|
|
605
|
+
/** What a pinned card is about, said on it without moving it. */
|
|
606
|
+
function sayBeside(pane, anchor) {
|
|
607
|
+
const beside = anchor?.getAttribute("data-graview-view") ?? anchor?.getAttribute("data-graview-pick") ?? anchor?.getAttribute("data-graview-record") ?? anchor?.getAttribute("data-graview-listed") ?? (anchor ? "page" : "");
|
|
608
|
+
if (pane.getAttribute("data-quick-ask-beside") !== beside)
|
|
609
|
+
pane.setAttribute("data-quick-ask-beside", beside);
|
|
610
|
+
}
|
|
611
|
+
/** The thinking mark, at the top right of what it is on, inside its corner; hidden where there is nothing to stand on. */
|
|
612
|
+
function placeOn(mark, anchor) {
|
|
613
|
+
const at = anchor?.getBoundingClientRect();
|
|
614
|
+
const on = anchor?.getAttribute("data-graview-view") ?? anchor?.getAttribute("data-graview-pick") ?? anchor?.getAttribute("data-graview-record") ?? anchor?.getAttribute("data-graview-listed") ?? (anchor ? "page" : "");
|
|
615
|
+
if (mark.getAttribute("data-thinking-on") !== on)
|
|
616
|
+
mark.setAttribute("data-thinking-on", on);
|
|
617
|
+
if (!at || at.width === 0) {
|
|
618
|
+
if (mark.style.visibility !== "hidden")
|
|
619
|
+
mark.style.visibility = "hidden";
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const width = mark.getBoundingClientRect().width;
|
|
623
|
+
const left = `${Math.round(Math.max(GAP / 2, at.right - width - 6))}px`;
|
|
624
|
+
const top = `${Math.round(Math.max(GAP / 2, at.top - 10))}px`;
|
|
625
|
+
if (mark.style.left !== left)
|
|
626
|
+
mark.style.left = left;
|
|
627
|
+
if (mark.style.top !== top)
|
|
628
|
+
mark.style.top = top;
|
|
629
|
+
if (mark.style.visibility !== "visible")
|
|
630
|
+
mark.style.visibility = "visible";
|
|
631
|
+
}
|
|
632
|
+
const THINKING = {
|
|
633
|
+
position: "fixed",
|
|
634
|
+
visibility: "hidden",
|
|
635
|
+
display: "inline-flex",
|
|
636
|
+
alignItems: "center",
|
|
637
|
+
gap: 6,
|
|
638
|
+
padding: "2px 8px",
|
|
639
|
+
borderRadius: 999,
|
|
640
|
+
fontSize: "0.75rem",
|
|
641
|
+
lineHeight: 1.4,
|
|
642
|
+
whiteSpace: "nowrap",
|
|
643
|
+
color: "var(--graview-ink)",
|
|
644
|
+
background: "var(--graview-float)",
|
|
645
|
+
border: "1px dashed var(--graview-accent)",
|
|
646
|
+
boxShadow: "var(--graview-lift-low)",
|
|
647
|
+
pointerEvents: "none",
|
|
648
|
+
};
|
|
649
|
+
const THINKING_DOT = { width: 7, height: 7, borderRadius: 999, background: "var(--graview-accent)", animation: "graview-thinking 1s ease-in-out infinite alternate" };
|
|
650
|
+
const PLAN = { display: "grid", gap: 8, padding: 10, borderRadius: 10, border: "1px dashed var(--graview-accent)", fontSize: "0.875rem" };
|
|
331
651
|
const CARD = {
|
|
332
652
|
position: "fixed",
|
|
333
653
|
inset: "auto",
|
|
654
|
+
// As tall as what it holds: WebKit's own popover height stretched the card to the window's (its max-height).
|
|
655
|
+
height: "auto",
|
|
334
656
|
margin: 0,
|
|
335
657
|
zIndex: layer("popover"),
|
|
336
658
|
boxSizing: "border-box",
|