// 🤖 The quick-form planner — the pure half of "Fill in a form instead". // Submitting the form must leave EXACTLY the state the conversational path // would: the same bridge verbs run, the same step records emitted (labels and // values copied verbatim from setup.flow.js / welcome.flow.js, so transcript // lines read identically), and the flow's own resume machinery then carries // the run to the sign-in step. This module only PLANS — no DOM, no network — // so the mapping from form values to verbs+records is unit-testable in Node; // the host form (shell.js) and its tests consume the same plan. import { cleanNameAnswer } from "./flow.js"; // planQuickForm(kind, values) → { name, verbs: [{verb, args}], records: // [{step, label, value}] } — verbs in run order, records in the order the // conversation would have emitted them. `name` is the cleaned/capped screen // name (the flow's clean-before-cap rule), echoed back so the form can show // what will actually be saved. // // kind = "welcome": values = { name } // kind = "setup": values = { name, spaceName, email, invites, ai, bots, apps } export function planQuickForm(kind, values) { const v = values || {}; // Clean BEFORE the cap, exactly like the typed name answer (flow.js). const name = cleanNameAnswer((v.name || "").trim()).slice(0, 32); const verbs = [{ verb: "setMyName", args: [name] }]; const records = [{ step: "name", label: `noted: you're ${name}`, value: name }]; if (kind === "welcome") { // The photo is a form no-op — recorded as skipped (a skip is an answer; // an unrecorded step would replay on the resume the form depends on). records.push({ step: "photo", label: "skipped the profile photo", value: "skipped" }); return { name, verbs, records }; } const spaceName = (v.spaceName || "").trim(); verbs.push({ verb: "setGroupName", args: [spaceName] }); records.push({ step: "group", label: `named this space “${spaceName}”`, value: spaceName }); const email = (v.email || "").trim(); if (email) { verbs.push({ verb: "setContactEmail", args: [email] }); records.push({ step: "email", label: "noted a contact email", value: email }); } else { records.push({ step: "email", label: "skipped the contact email", value: "skipped" }); } records.push({ step: "photo", label: "skipped the space photo", value: "skipped" }); const invites = v.invites !== false; verbs.push({ verb: "setUserInvites", args: [invites] }); records.push( invites ? { step: "invites", label: "decided members can invite people", value: "members" } : { step: "invites", label: "decided only the admin invites people", value: "admin only" }, ); // Bots + apps mirror the conversation's two paths: both on is the one-tap // record (defaults are already on server-side — writing nothing is the // point), anything off walks the granular records, writing each gate. const extras = [ ["bots", v.bots !== false, "bots"], ["apps", v.apps !== false, "apps"], ]; if (extras.every(([, on]) => on)) { records.push({ step: "extras", label: "kept bots and apps on", value: "all-on" }); } else { for (const [key, on, noun] of extras) { verbs.push({ verb: "setFeature", args: [key, on] }); records.push({ step: `extras-${key}`, label: on ? `enabled ${noun}` : `switched ${noun} off`, value: on ? "on" : "off", }); } } // AI is its own explicit opt-IN (docs/ai-provisioning.md): ALWAYS written — // unticked means off, matching the conversation's dedicated step — and // never bundled into the everything-on shortcut, because saying yes // provisions a real helper on a cloud space. const ai = v.ai === true; verbs.push({ verb: "setFeature", args: ["ai", ai] }); records.push({ step: "ai-optin", label: ai ? "invited Eleven's AI in" : "kept AI off", value: ai ? "on" : "off", }); return { name, verbs, records }; }