import { useEffect, useState } from "react"; import { cloudLogin, connectManaged, getCloudStatus, getConnectors, setOnboarded, type CloudStatus, type Connector, } from "../api"; import { ConnectorBadge } from "../connectors/ConnectorIcon"; import { ProviderCards, ProviderForm, useProviderSetup } from "../providers/ProviderSetup"; import { Spinner } from "./AutomationQuickstart"; // First-run onboarding (UX-DECISIONS §24 → §29 → §39): model → your tools → go. // §39 (owner design, 2026-07-18): step 1 is a PROVIDER GALLERY — 13 real brand // marks, two per row, each card wearing its own state — and step 2 is a // two-state tools page whose post-sign-in body is a mini connector gallery with // live one-click connects. Both steps share one frame rule: the header and // footer never move; only the middle region swaps, at a fixed height. // The gallery/form themselves live in providers/ProviderSetup.tsx, shared with // Settings ▸ Models (UX-021) so the two surfaces can't drift. // Replayable from Settings ▸ General ▸ "Run setup again". // Step 2's benefit rows (§41): managed connectors with LIVE prod OAuth apps only, // each framed by the job it does (detail copy stays ONE line even with a Connect // pill — wrap made rows jump between states). gmail + google_calendar ship as one // combined grayed "Coming soon" row — both ride the same Google app, gated on // Google verification/CASA; give them rows when it lands. const TOOL_ROWS = [ { name: "outlook", benefit: "Stay on top of email", detail: "Outlook — triage mail, draft replies, run your calendar." }, { name: "slack", benefit: "Keep up with Slack", detail: "Slack — catch up, answer mentions, post updates." }, { name: "github", benefit: "Ship code", detail: "GitHub — review PRs, watch issues, reply to @mentions." }, { name: "notion", benefit: "Keep your notes in reach", detail: "Notion — search pages, query databases, draft docs." }, { name: "hubspot", benefit: "Keep the CRM current", detail: "HubSpot — update deals, log notes, prep calls." }, { name: "attio", benefit: "Track every relationship", detail: "Attio — search records, read timelines, log notes." }, ]; const TOOLS_SOON = ["gmail", "google_calendar"]; export function Onboarding({ onDone }: { onDone: (next?: "work" | "gallery" | "automations") => void }) { const [step, setStep] = useState(0); // -- step 1: model (provider gallery ⇄ key form, shared machinery) --------------- const ps = useProviderSetup(); const [skipConfirm, setSkipConfirm] = useState(false); const anyReady = ps.providers.some((p) => p.configured && p.needs_key) || ps.keylessOk.size > 0; // In the form with typed-but-untested input, Next verifies+saves first (tester // catch 2026-07-12: a manual Test-then-Continue two-step reads as a puzzle). const nextFromForm = !!ps.sel && ps.dirty && ps.secretFilled; const canNext = anyReady || nextFromForm; const advance = async () => { if (nextFromForm && !ps.credentialed) { ps.cancelBackTimer(); if (!(await ps.runTestAndSave())) return; } setStep(1); }; // -- step 2: connect your everyday tools (§39 two-state page) ------------------- const [connectors, setConnectors] = useState([]); const [cloud, setCloud] = useState(null); const [signinPhase, setSigninPhase] = useState<"opening" | "waiting" | null>(null); // One in-flight connect at a time; clicking another card quietly resets the first. const [pendingTool, setPendingTool] = useState(null); // Poll while on the tools page: sign-in AND vendor consents land out-of-band in // the system browser. Tighten while either is actually in flight. useEffect(() => { if (step !== 1) return; const load = () => { getConnectors().then(setConnectors).catch(() => {}); getCloudStatus().then(setCloud).catch(() => {}); }; load(); const fast = signinPhase === "waiting" || pendingTool !== null; const t = setInterval(load, fast ? 750 : 3000); return () => clearInterval(t); }, [step, signinPhase, pendingTool]); // The poll flips the card to ✓ when the consent lands. useEffect(() => { if (pendingTool && connectors.find((c) => c.name === pendingTool)?.connected) setPendingTool(null); }, [connectors, pendingTool]); const startTool = async (name: string) => { setPendingTool(name); // replaces any previous pending connect const res = await connectManaged( name, name === "hubspot" ? { access: "read" } : undefined, // least privilege in onboarding ).catch(() => ({ ok: false })); if (!res.ok) setPendingTool((cur) => (cur === name ? null : cur)); // silent reset — no error walls here }; const finish = async (next?: "work" | "gallery" | "automations") => { await setOnboarded(true).catch(() => {}); onDone(next); }; // -- shared bits ---------------------------------------------------------------- const dots = (
{[0, 1, 2].map((i) => ( ))}
); return (
{/* FIXED height across all three steps (owner call 2026-07-12, reaffirmed §39: the modal must never resize — the gallery⇄form swap happens inside this box). */}
{dots} {step === 0 && (
{/* Persistent header — stays put while the region below swaps (§39). */}

Welcome to OpenWorkerBETA

Pick a model provider to get started — OpenWorker runs on your own key, and your key and your data stay on this computer.

{!ps.sel ? ( /* ---- the provider GALLERY ---- */
) : ( /* ---- one provider's key form, same box ---- */
)} {/* Persistent footer (§39). */}
{!skipConfirm ? ( ) : ( Nothing works without a model —{" "} )}

Models can be enabled or hidden anytime in Settings ▸ Models.

)} {step === 1 && ( /* §41 (owner design, 2026-07-19, supersedes §39's card gallery): BENEFIT ROWS are the connect surface — one row set, two states, ZERO layout shift. Pre-sign-in the rows make the case and a pinned band asks for sign-in; after sign-in the band's slot keeps its place but flips to a green congrats, and every row grows a quiet Connect pill. The gated Google pair is ONE combined grayed row. */

Connect your everyday tools

Chat can only advise. Connected, your coworker does the actual work:

{TOOL_ROWS.map(({ name, benefit, detail }) => { const c = connectors.find((x) => x.name === name); if (!c) return null; return (
{benefit} {detail} {cloud?.signed_in && (c.connected ? ( ✓ Connected ) : pendingTool === name ? ( Check your browser… ) : ( ))}
); })} {/* The gated Google pair: one combined grayed row, both states (§41). */}
{TOOLS_SOON.map((n) => { const c = connectors.find((x) => x.name === n); return c ? : null; })} Gmail & Google Calendar Coming soon — pending Google’s app verification. {cloud?.signed_in && Coming soon}
{/* The band is PINNED outside the scroll area and its slot never moves: the ask pre-sign-in, a green congrats after — zero layout shift at the moment the user returns from the browser (§41). */} {!cloud?.signed_in ? (
Sign in for one-click connections OpenWorker handles the OAuth for 20+ tools — no dev consoles, no pasted keys. Tokens stay on this computer. {signinPhase ? ( {signinPhase === "opening" ? ( "Opening browser…" ) : ( <> Waiting…{" "} )} ) : ( )}
) : (
🎉 You’re signed in{cloud.account ? ` as ${cloud.account}` : ""} Connect a tool above with one click — or add them anytime later from the Connectors page.
)} {/* One footer button, one slot: quiet skip pre-sign-in, black Next after. */}
{cloud?.signed_in ? ( ) : ( )}

30+ more tools on the Connectors page — add or remove anytime. Tokens stay on this computer.

)} {step === 2 && (

You're set up

Two good ways to start:

{/* The Specialist-coworkers gallery card and the per-session-scope line stay HIDDEN (owner call 2026-07-12); the finish("gallery") plumbing remains for their return. */}

Replay this setup anytime: Settings ▸ Appearance ▸ Run setup again.

)}
); }