| 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. |
| 9 | import { 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 } |
| 19 | export 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 | |