eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1// πŸ€– The quick-form planner β€” the pure half of "Fill in a form instead".
2// Submitting the form must leave EXACTLY the state the conversational path
3// would: the same bridge verbs run, the same step records emitted (labels and
4// values copied verbatim from setup.flow.js / welcome.flow.js, so transcript
5// lines read identically), and the flow's own resume machinery then carries
6// the run to the sign-in step. This module only PLANS β€” no DOM, no network β€”
7// so the mapping from form values to verbs+records is unit-testable in Node;
8// the host form (shell.js) and its tests consume the same plan.
9import { cleanNameAnswer } from "./flow.js";
10
11// planQuickForm(kind, values) β†’ { name, verbs: [{verb, args}], records:
12// [{step, label, value}] } β€” verbs in run order, records in the order the
13// conversation would have emitted them. `name` is the cleaned/capped screen
14// name (the flow's clean-before-cap rule), echoed back so the form can show
15// what will actually be saved.
16//
17// kind = "welcome": values = { name }
18// kind = "setup": values = { name, spaceName, email, invites, ai, bots, apps }
19export function planQuickForm(kind, values) {
20 const v = values || {};
21 // Clean BEFORE the cap, exactly like the typed name answer (flow.js).
22 const name = cleanNameAnswer((v.name || "").trim()).slice(0, 32);
23 const verbs = [{ verb: "setMyName", args: [name] }];
24 const records = [{ step: "name", label: `noted: you're ${name}`, value: name }];
25
26 if (kind === "welcome") {
27 // The photo is a form no-op β€” recorded as skipped (a skip is an answer;
28 // an unrecorded step would replay on the resume the form depends on).
29 records.push({ step: "photo", label: "skipped the profile photo", value: "skipped" });
30 return { name, verbs, records };
31 }
32
33 const spaceName = (v.spaceName || "").trim();
34 verbs.push({ verb: "setGroupName", args: [spaceName] });
35 records.push({ step: "group", label: `named this space β€œ${spaceName}”`, value: spaceName });
36
37 const email = (v.email || "").trim();
38 if (email) {
39 verbs.push({ verb: "setContactEmail", args: [email] });
40 records.push({ step: "email", label: "noted a contact email", value: email });
41 } else {
42 records.push({ step: "email", label: "skipped the contact email", value: "skipped" });
43 }
44
45 records.push({ step: "photo", label: "skipped the space photo", value: "skipped" });
46
47 const invites = v.invites !== false;
48 verbs.push({ verb: "setUserInvites", args: [invites] });
49 records.push(
50 invites
51 ? { step: "invites", label: "decided members can invite people", value: "members" }
52 : { step: "invites", label: "decided only the admin invites people", value: "admin only" },
53 );
54
55 // Bots + apps mirror the conversation's two paths: both on is the one-tap
56 // record (defaults are already on server-side β€” writing nothing is the
57 // point), anything off walks the granular records, writing each gate.
58 const extras = [
59 ["bots", v.bots !== false, "bots"],
60 ["apps", v.apps !== false, "apps"],
61 ];
62 if (extras.every(([, on]) => on)) {
63 records.push({ step: "extras", label: "kept bots and apps on", value: "all-on" });
64 } else {
65 for (const [key, on, noun] of extras) {
66 verbs.push({ verb: "setFeature", args: [key, on] });
67 records.push({
68 step: `extras-${key}`,
69 label: on ? `enabled ${noun}` : `switched ${noun} off`,
70 value: on ? "on" : "off",
71 });
72 }
73 }
74 // AI is its own explicit opt-IN (docs/ai-provisioning.md): ALWAYS written β€”
75 // unticked means off, matching the conversation's dedicated step β€” and
76 // never bundled into the everything-on shortcut, because saying yes
77 // provisions a real helper on a cloud space.
78 const ai = v.ai === true;
79 verbs.push({ verb: "setFeature", args: ["ai", ai] });
80 records.push({
81 step: "ai-optin",
82 label: ai ? "invited Eleven's AI in" : "kept AI off",
83 value: ai ? "on" : "off",
84 });
85 return { name, verbs, records };
86}
87