toga-ai 1.0.286 → 1.0.288

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.
@@ -3,6 +3,8 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [AI-BDR Architecture](architecture.md) | **AI-BDR** is TOGA's outbound AI sales-development representative. | ai-bdr/README.md, ai-bdr/requirements.txt, ai-bdr/.env.example, ai-bdr/docs/client-onboarding-sop.md, ai-bdr/docs/vapi-firstmessage-timing-fix.md, ai-bdr/docs/vapi-inbound-callback-assistant-request.md, ai-bdr/docs/vapi-voicemail-iphone-screening.md, ai-bdr/vapi/templates/system-prompt.template.md, ai-bdr/vapi/templates/assistant.template.json, ai-bdr/prompts/archive/prompt-may-15.txt, ai-bdr/prompts/campaigns/healthcare-2026-03-18.md, ai-bdr/prompts/campaigns/healthcare-v2-2026-03-25.md, ai-bdr/scripts/update_assistant.py, ai-bdr/scripts/update_system_prompt.py, ai-bdr/scripts/update_structured_output.py, ai-bdr/scripts/update_call_summary_context.py, ai-bdr/scripts/fix_booking_guardrails.py, ai-bdr/scripts/get_assistant.py |
6
+ | [BDR Web Funnel — Full Implementation Plan](features/bdr-web-funnel-plan.md) | > **Status:** First iteration (for Alex's review). | bdr/PLAN.md, bdr/mockup/app.jsx, bdr/mockup/screens.jsx, bdr/mockup/components.jsx |
6
7
  | [Call Orchestration — PHP Worker ↔ Vapi (the integration seam)](features/call-orchestration.md) | The **PHP worker** is the orchestrator; **Vapi** is the actor. | ai-bdr/docs/client-onboarding-sop.md, ai-bdr/docs/vapi-firstmessage-timing-fix.md, ai-bdr/docs/vapi-inbound-callback-assistant-request.md, ai-bdr/docs/vapi-voicemail-iphone-screening.md, ai-bdr/scripts/update_structured_output.py |
7
8
  | [Vapi Integration — Assistants, Tools, Structured Output](features/vapi-integration.md) | Everything inside Vapi: the three assistants, the three shared tools, the shared structured-output schema, the Liquid-templated system prompt, and the Python sc | ai-bdr/vapi/templates/assistant.template.json, ai-bdr/vapi/templates/system-prompt.template.md, ai-bdr/prompts/archive/prompt-may-15.txt, ai-bdr/prompts/campaigns/healthcare-2026-03-18.md, ai-bdr/prompts/campaigns/healthcare-v2-2026-03-25.md, ai-bdr/prompts/templates/campaign-template.md, ai-bdr/scripts/update_assistant.py, ai-bdr/scripts/update_system_prompt.py, ai-bdr/scripts/update_structured_output.py, ai-bdr/scripts/update_call_summary_context.py, ai-bdr/scripts/fix_booking_guardrails.py, ai-bdr/scripts/get_assistant.py, ai-bdr/docs/vapi-firstmessage-timing-fix.md, ai-bdr/docs/vapi-voicemail-iphone-screening.md |
9
+ | [Web Funnel — Next.js UI + Config-Driven Campaign Content](features/web-funnel-content-model.md) | The **public-facing web funnel** is the new UI / lead-capture front door of the **same AI-BDR product** documented in `../architecture.md`. | bdr/src/content/schema.ts, bdr/src/content/hubspotProvider.ts, bdr/src/content/useCampaign.ts, bdr/src/content/default.ts, bdr/src/server/leadSink.ts, bdr/src/server/callbackService.ts, bdr/src/app/api/call-now/route.ts, bdr/src/app/api/call-later/route.ts |
8
10
  | [New-Campaign Onboarding (6-phase runbook)](workflows/new-campaign-onboarding.md) | The end-to-end procedure for launching a new outbound BDR campaign (industry, offer, target persona, geography). | ai-bdr/docs/client-onboarding-sop.md, ai-bdr/docs/calcom-new-campaign-event-guide.md, ai-bdr/prompts/templates/campaign-template.md, ai-bdr/scripts/update_assistant.py, ai-bdr/scripts/update_system_prompt.py |
@@ -6,8 +6,8 @@ project: AI-BDR
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-06-16
10
- owners: [akhokhani]
9
+ updated: 2026-07-08
10
+ owners: [akhokhani, tcox]
11
11
  files:
12
12
  - ai-bdr/README.md
13
13
  - ai-bdr/requirements.txt
@@ -30,6 +30,7 @@ files:
30
30
  related:
31
31
  - features/vapi-integration.md
32
32
  - features/call-orchestration.md
33
+ - features/web-funnel-content-model.md
33
34
  - workflows/new-campaign-onboarding.md
34
35
  ---
35
36
 
@@ -175,6 +176,19 @@ authored here, then pushed to Vapi by `scripts/update_system_prompt.py`.
175
176
  - **No Langfuse / OTEL** — there is no TOGA-side LLM call to trace; the
176
177
  LLM call happens inside Vapi.
177
178
 
179
+ ## Public web funnel (UI / lead-capture front door)
180
+
181
+ The AI-BDR product now has a **public-facing web funnel** — a **Next.js (App Router)**
182
+ app in its own code repo, **`bdr`** — as its UI and lead-capture front door. It **reuses
183
+ this system's existing calls** (HubSpot contact fetch, Toga upsert + campaign link,
184
+ `requestCall` callback — see *End-to-end call flow* and `features/call-orchestration.md`),
185
+ ported from the external `info` repo behind `LeadSink` / `CallbackService` adapters so all
186
+ secret-bearing calls stay server-side. On top of that reuse it adds a **HubSpot-owned,
187
+ config-driven campaign content layer**: marketing authors campaign content in HubSpot,
188
+ fetched by slug at runtime and mapped to a serializable `CampaignBundle` (config-driven
189
+ components, `DEFAULT` fallback — no `if(campaign===x)`; a copy change ships with no
190
+ redeploy). Full detail: **`features/web-funnel-content-model.md`**.
191
+
178
192
  ## Key decisions
179
193
 
180
194
  - **Single shared structured-output schema** across all assistants — keeps
@@ -195,3 +209,7 @@ authored here, then pushed to Vapi by `scripts/update_system_prompt.py`.
195
209
 
196
210
  ## Change history
197
211
  - 2026-06-16 — Initial architecture doc for AI-BDR. (akhokhani)
212
+ - 2026-07-08 — Additive: recorded the public Next.js web funnel (code repo `bdr`) as the
213
+ product's UI / lead-capture front door — reuses this system's existing HubSpot/Toga/callback
214
+ calls and adds a HubSpot-owned, config-driven campaign content layer. See
215
+ `features/web-funnel-content-model.md`. (tcox)
@@ -0,0 +1,533 @@
1
+ ---
2
+ title: "BDR Web Funnel — Full Implementation Plan"
3
+ framework: "2.0"
4
+ repo: ai-bdr
5
+ project: "AI-BDR"
6
+ client: shared
7
+ type: feature
8
+ status: draft
9
+ updated: 2026-07-08
10
+ owners: [tcox]
11
+ files:
12
+ - bdr/PLAN.md
13
+ - bdr/mockup/app.jsx
14
+ - bdr/mockup/screens.jsx
15
+ - bdr/mockup/components.jsx
16
+ related:
17
+ - architecture.md
18
+ - web-funnel-content-model.md
19
+ ---
20
+
21
+ # BDR: Multi-Campaign "AI BDR / Agent Studio" Web App — Implementation Plan
22
+
23
+ > **Status:** First iteration (for Alex's review). Draft, pending sign-off on the open questions
24
+ > in §9 and final campaign direction / verbiage from leadership.
25
+ > **Author:** tcox · **Stack:** TOGA 2.0 front-end · Next.js (App Router) · **Client scope:** shared / internal (TOGA)
26
+ > **Repo (new):** `bdr` · **Two source inputs:** the **mockup** (real UI) + **`info`** (backend to reuse).
27
+ >
28
+ > Product-copy rules the built site must follow (from the mockup's `CLAUDE.md`): **no em dashes** in any
29
+ > shipped string; **one accent source** drives every accent. (This internal planning doc itself uses em
30
+ > dashes for readability; the rule governs what the site ships, not this file.)
31
+
32
+ ---
33
+
34
+ ## 0. Provenance & required inputs
35
+
36
+ This plan comes from the **AI BDR Development Handoff** meeting plus a full read of two codebases.
37
+ `bdr` is the new public **UI / lead-capture front door of TOGA's existing AI-BDR product** (that
38
+ product's backend is documented at `2.0/apps/ai-bdr/`) — only the UI is new. It is an **entirely new
39
+ code repo**, built by combining:
40
+
41
+ 1. **The mockup (the real target UI)** in the `bdr` repo's `mockup/` folder — a finished, high-fidelity
42
+ prototype of an "AI BDR / Agent Studio" single-page flow. This is the pixel-perfect blueprint.
43
+ - Source of truth: `app.jsx` (shell/routing/state), `screens.jsx` (the screens), `components.jsx`
44
+ (shared component + data library), `styles.css` (~97 KB of styling), `CLAUDE.md` (copy rules).
45
+ - Prototype tech: React 18 via UMD + in-browser Babel (no build step). Assets under
46
+ `media/ uploads/ agents/ voices/ screens/`. There is also a 14.5 MB single-file
47
+ `AI BDR (standalone).html` export (do not hand-edit; it is a build artifact).
48
+ 2. **`info`** — the external `info` repo (github.com/agilantsolutions/info) — a Next.js app whose `/bdr`
49
+ route already implements the **production backend calls we will reuse**: HubSpot contact fetch, Toga
50
+ contact upsert + campaign linking, and callback (call-now / call-later) requests. (BDR is **also
51
+ Next.js**, so we reuse `info`'s framework patterns and its server-side call logic directly; see §5.6.)
52
+ This is where the real integrations live; its marketing UI is a *different, older* funnel we are
53
+ **not** copying.
54
+
55
+ **Attach when handing this plan to Claude for execution:** this file, the meeting transcript, the
56
+ mockup folder, and the `info` repo.
57
+
58
+ **Still pending (does not block the build):** final campaign direction and copy from leadership.
59
+
60
+ ---
61
+
62
+ ## 1. Goal & done-condition
63
+
64
+ **Goal:** Rebuild the mockup's Agent Studio flow as a production **Next.js app** (App Router +
65
+ TypeScript, matching `info`). Next.js is the chosen framework because the site is **highly public**,
66
+ so server-side rendering + SEO matter, and its server (server components + route handlers) is where
67
+ the secret-bearing HubSpot/Toga/content calls run natively (§5.6). Every campaign is defined by
68
+ content authored in HubSpot, so a new campaign launches with **no code changes**, multiple campaigns
69
+ run side by side, and the real HubSpot / Toga / callback backend from `info` is reused directly.
70
+
71
+ **Done when:**
72
+ 1. The full flow (Welcome, Build, Capabilities, Call Now, Schedule, Success) renders in Next.js
73
+ with visual parity to the mockup (pixel-perfect pass against the mockup screens).
74
+ 2. Two distinct campaigns (for example a generic **BDR-as-a-service** and a **healthcare** variant),
75
+ **each authored in HubSpot**, render differing only in data (copy, Q&A proof points, agent identity,
76
+ accent), with zero component-code differences. Adding a campaign requires **only HubSpot edits** (no
77
+ code, no redeploy).
78
+ 3. No user-facing string is hardcoded in a component (grep-verified); all copy comes from the HubSpot
79
+ campaign content, fetched by slug at runtime and resolved through one hook.
80
+ 4. The simulated call/schedule actions are wired to the **real backend** (Toga `requestCall`,
81
+ HubSpot/Toga contact upsert) behind a `CallbackService` / `LeadSink` adapter ported from `info`.
82
+ 5. Campaign content is fetched from **HubSpot at runtime** via the backend, behind the
83
+ `CampaignContentProvider` seam (§5.2), so marketing edits copy without a developer or a deploy.
84
+ 6. Copy honors the mockup rules: no em dashes; a single `--accent` source drives all accents.
85
+ 7. No PII in logs; the `info` debug logging is not carried over.
86
+ 8. Deploys (AWS Amplify) with documented env vars.
87
+
88
+ ---
89
+
90
+ ## 2. What the mockup actually is (the real UI to rebuild)
91
+
92
+ ### 2.1 Screen / state map
93
+ The shell (`app.jsx`) is a screen router with a 3-step rail (Create · Abilities · Connect) plus
94
+ modals. Default flow in **bold**; parked screens are reachable only via hidden dev triggers today.
95
+
96
+ | Screen (`screen` key) | Step | Role | Notes |
97
+ |---|---|---|---|
98
+ | `landing` | (rail hidden) | Service selection ("AI BDR" featured + "Talos" coming soon) | Parked; reached via a hidden corner door. |
99
+ | `creator` | 0 Create | Manual agent builder (form tabs, 9-slot grid, live preview, name, color, voice) | **Parked / dev-only** ("Build it myself" is "coming soon"). |
100
+ | **`building`** | 0 Create | **AutoBuild**: theatrical assembly, always lands on Alex / Human / Cobalt | ~5.5 s, then advances to capabilities. |
101
+ | **`capabilities`** | 1 Abilities | **"Meet your agent"**: avatar, 3 skill cards, Q&A pill bank, CTAs | The content-heavy screen. |
102
+ | **`call`** | 2 Connect | **Call Now**: agent recap + phone entry | Submits phone. |
103
+ | **`schedule`** | 2 Connect | **Schedule**: calendar + 30-min slots + phone | 24/7, 12-month window. |
104
+ | **`calling`** | 2 Connect | **Success (calling)**: connecting → connected → ended + call summary + share | Currently a timed simulation. |
105
+ | **`scheduled`** | 2 Connect | **Success (scheduled)**: confirmation | Calendar-invite copy. |
106
+
107
+ **Modals (`app.jsx`):** `Welcome` (first load: "Meet the rep who never sleeps.", primary
108
+ "Build my agent for me"), `WelcomeStep2` (how-it-works, 3 tiles, shown once on first reaching
109
+ capabilities), `EarlyAccess` ("Build it myself" / customization, coming-soon, with walkthrough video).
110
+
111
+ **Default happy path:** `Welcome → (Build my agent) → building → capabilities → call | schedule → calling | scheduled`.
112
+
113
+ ### 2.2 Agent identity model (core state)
114
+ `state = { form, sel, color, voice, voiceGender, name }`:
115
+ - **form**: `Human | Owl | Robot`; **sel**: chosen slot index per form (0..8, 9 slots each).
116
+ - **color**: one of 5 TOGA accents (`#FF6AB0` Fuchsia, `#CF9AFF` Lilac, `#4571DD` Cobalt,
117
+ `#FFA071` Peach, `#A2AAE1` Indigo); drives the global `--accent` (+ derived `--accent-fill`).
118
+ - **voice**: `voiceGender` (Female/Male) + index into `VOICE_GROUPS`.
119
+ - **name**: free text, falls back to a per-form sample name (`AGENT_NAMES`, for example Human ->
120
+ Maya/Alex/Zoe...). Default auto-build result is **Alex** (Human slot 1, Cobalt, Female voice 0).
121
+ - Persisted to `localStorage` (`aibdr.v2`); only voice survives a refresh, flow restarts at Welcome.
122
+
123
+ > Design implication: in the default flow the user does **not** pick the agent (the builder is
124
+ > parked); AutoBuild fixes it to Alex/Cobalt. So **agent identity is effectively campaign config**
125
+ > today (which art, accent, voice, name a campaign presents). See §6.
126
+
127
+ ### 2.3 Shared component + data library (`components.jsx`)
128
+ Reusable pieces to port (heavy reuse is exactly why "build one screen cleanly, the rest are fast"):
129
+ - **Structural/UI:** `Wordmark`, `StepRail`, `BackBtn`, `SynapseButton`, `CtlHead`, `IqMark`, `Ico`
130
+ (full inline SVG icon set), `Silhouette`.
131
+ - **Agent visuals:** `AgentArt` / `SmoothImg` / `agentArt` + `AGENT_ART`, `TalkingPreview` +
132
+ `AGENT_TALK` (per-form talking video), `AgentSlot`, `ColorSelector` + `AGENT_COLORS`,
133
+ `VoiceDial` + `VOICE_GROUPS` + `VoiceAudio` + `voiceName`, `AGENT_NAMES` + `sampleName`.
134
+ - **Motion/system:** `SwapFade` (the one page/tab transition), `Particles` (ambient motes),
135
+ plus app-level cursor-hover tracking and scroll-state tinting (in `app.jsx`).
136
+ - **Asset resolution:** `ASSET(path)` maps a logical path to `window.__resources` (embedded assets
137
+ in the standalone export) or to a real URL. `preloadAgentAssets(form, sel)` warms art + video.
138
+ - **Dev tuning:** `TweaksPanel` / `TweakSection` / `TweakColor` / `TweakSlider` (synapse glow,
139
+ radius). Likely dev-only; decide whether to keep in production (§9).
140
+
141
+ ### 2.4 Content inventory (everything that must become campaign config)
142
+ This is the "verbiage" the meeting wants swappable. Current hardcoded copy lives in these surfaces:
143
+ - **Welcome / WelcomeStep2 / EarlyAccess** (headlines, value bullets, how-it-works tiles, CTA labels).
144
+ - **Landing** (headline, lede, the two service tiles).
145
+ - **Capabilities**: `CAPS` (3 skill cards), `Q_ANCHOR` + `Q_POOL` (anchor + a 10-entry Q&A pool, 11
146
+ total) and `Q_SETS` (rotation A/B/C), lede, CTA labels, "avg connect time" line.
147
+ - **Call Now / Schedule**: prompts, phone-field labels, privacy footnotes, durations.
148
+ - **Success**: `CALL_SUMMARIES` (short/medium/large recaps), status strings, calendar-invite copy,
149
+ `AmbientQuestions` (reuses the Q pool).
150
+ - **AutoBuild**: build-step labels ("Selecting your rep / Tuning the voice / Setting the style").
151
+
152
+ > Note: the `Q_POOL` answers already contain **healthcare/pharma proof points** ("cut inventory
153
+ > costs 32%", "recovered $167K in 90 days", "response times dropped 44%") alongside generic BDR
154
+ > answers. This is direct evidence that campaign copy (BDR-as-a-service vs. healthcare) must be
155
+ > bundle-swappable, and gives us real content for the two example bundles in the done-condition.
156
+
157
+ ---
158
+
159
+ ## 3. What `info` provides (backend to reuse, not its UI)
160
+ **Many of the API calls the new BDR needs already exist and work in `info` — reuse them, do not
161
+ rebuild.** The `info/bdr` route is a live implementation of the lead + callback backend; port these
162
+ modules into `bdr` (behind the §5.6 adapters, minus the debug logging) rather than writing them from
163
+ scratch. From the external `info` repo's `src` folder:
164
+ - **HubSpot** (`lib/hubspot.ts`): `getContact(hsContactId)`, env `HUBSPOT_ACCESS_TOKEN`.
165
+ - **Toga / api2** (`lib/toga.ts`, envelope `{isSuccess,status,error,messages,data}`):
166
+ cached-token auth; `upsertContactByEmail` (find/create/update) + `addContactToCampaign`;
167
+ `requestCall(uuid, dtNextContactRequested, phone, IMMEDIATE|SCHEDULED)` which drives the actual
168
+ voice callback; `CONTACT_CALL_TYPE_UUID` immediate/scheduled; CST datetime formatting.
169
+ Env: `TOGA_API_BASE_URL`, `TOGA_CLIENT_ID`, `TOGA_CLIENT_API_UUID`, `TOGA_CLIENT_API_SECRET`.
170
+ - **Entry params**: `?hsContactId=…&hsCampaignId=…` (a server component prefetches the contact +
171
+ upserts on request; BDR keeps this same Next.js server-component pattern).
172
+ - **API routes**: `call-now`, `call-later`, `contact` (and a `visit` 501 stub).
173
+ - **GA4** (`lib/analytics.ts`): typed `EventMap`, env `NEXT_PUBLIC_GA_MEASUREMENT_ID`.
174
+ - **Do not port**: the extensive debug `console.log`s (several log PII: email/phone), and `info`'s
175
+ hero/curiosity marketing UI.
176
+
177
+ **Reconciliation:** the mockup's Call Now / Schedule screens are the front-end; `info`'s
178
+ `requestCall` / upsert are the backend those screens should call (replacing the mockup's timed
179
+ simulation in `Success`). The mockup already models immediate vs. scheduled, matching
180
+ `CONTACT_CALL_TYPE_UUID`.
181
+
182
+ **How `info` uses "campaign" (verified by reading the source):** it is **attribution-only**. HubSpot is
183
+ used solely to fetch the *contact* (`getContact(hsContactId)`); the `hsCampaignId` from the URL is
184
+ passed to **Toga** as a campaign UUID (`addContactToCampaign` → `POST /campaigns-contacts`), linking
185
+ the contact to a campaign on the Toga/api2 side. **`info` does not fetch any marketing content from
186
+ HubSpot** — its copy is hardcoded. So we reuse `info`'s contact fetch + Toga campaign-link + callback,
187
+ but **fetching campaign *content* by slug from HubSpot is net-new** (no `info` pattern to copy; see
188
+ §9.10). Note the slug (content key) and the campaign UUID (attribution key) are related but distinct;
189
+ the bundle carries the UUID so a slug maps to the right campaign for logging.
190
+
191
+ ---
192
+
193
+ ## 4. Decisions locked at kickoff
194
+ | Decision | Choice | Consequence |
195
+ |---|---|---|
196
+ | Repo name | **`bdr`** | New greenfield repo. Not yet in the TOGA registry. |
197
+ | Framework | **Next.js (App Router) + TypeScript** | Chosen because the site is highly public: SSR + SEO, and a built-in server for the secret-bearing HubSpot/Toga/content calls. Matches `info`, so its patterns port directly. |
198
+ | UI source | **The mockup** | Rebuild its Agent Studio flow faithfully (pixel-perfect). The mockup is React, so components port cleanly into Next.js client components. |
199
+ | Backend source | **`info`** | Reuse HubSpot/Toga/callback logic directly (same framework). |
200
+ | Content model | **Runtime, HubSpot-owned** | Campaign content is authored by the **marketing team in HubSpot** and fetched **by slug at runtime** via the backend. **No campaign content in the repo; no developer or redeploy to change copy.** (Supersedes the earlier "static bundles now" idea.) See §5.2 / §5.5. |
201
+ | Backend relationship | **Same product; reuses the AI-BDR backend** | This funnel is the new UI / front door of TOGA's existing **AI-BDR** product; it reuses that system's HubSpot/Toga/callback calls (documented at `2.0/apps/ai-bdr/`). Lead/callback handoff still goes behind `LeadSink`/`CallbackService` adapters for a clean seam. |
202
+ | Campaign entry | **Plan recommends** | See §5.5. |
203
+ | Copy rules | **No em dashes; one accent source** | Accent enforced in code. No-em-dash cannot be linted on HubSpot-authored copy: enforce via author guidance + a normalization pass in the backend mapping. |
204
+ | Client | Shared / internal | No per-client theming/entitlement logic. |
205
+
206
+ ---
207
+
208
+ ## 5. Target architecture
209
+
210
+ ### 5.1 Prototype → production porting strategy
211
+ The mockup is already **React** (React 18 via UMD + in-browser Babel, no build step). Production is a
212
+ **Next.js (App Router) + TypeScript** app, matching `info` (note `info/AGENTS.md`: Next has breaking
213
+ changes vs. older versions; read the bundled docs in `node_modules/next/dist/docs/` before coding).
214
+ Port approach:
215
+ - Move `styles.css` in as the base stylesheet (Tailwind optional; the mockup is hand-authored CSS
216
+ with CSS variables, so keep it and layer Tailwind only if desired). Preserve the CSS-variable
217
+ accent system verbatim.
218
+ - Convert each `components.jsx` / `screens.jsx` unit into a real module. The interactive Agent Studio
219
+ screens are **client components** (`"use client"`, they use state/effects/animation); the top-level
220
+ page is a **server component** that resolves the campaign + primes the lead server-side, then hands
221
+ data to the client flow. Replace UMD globals / `window.*` with imports. Keep `SwapFade` / `Particles`.
222
+ - Replace `ASSET`/`window.__resources` with the Next asset pipeline (files in `public/`, imported URLs).
223
+ Migrate agent art / talking videos / walkthrough video / particles.
224
+ - Replace `localStorage` boot logic with the same behavior in a client provider/hook.
225
+
226
+ > **Server = built in (this is why Next.js helps here).** `info`'s HubSpot/Toga calls use **secrets**
227
+ > (`HUBSPOT_ACCESS_TOKEN`, `TOGA_CLIENT_API_SECRET`, ...) that must never ship to the browser. In
228
+ > Next.js those calls live in **server components + route handlers**, so there is no separate backend
229
+ > or service to stand up. Keep secret-bearing code out of client components and never expose it via
230
+ > `NEXT_PUBLIC_` env.
231
+
232
+ ### 5.2 Content provider — HubSpot is the source of truth
233
+ **Campaign content lives in HubSpot, authored by marketing, and is fetched by slug at runtime.**
234
+ It is **not** stored in the repo, and changing copy requires **no developer and no redeploy**. The
235
+ provider is the seam that makes this true:
236
+ ```ts
237
+ interface CampaignContentProvider {
238
+ resolve(slug: string | undefined): Promise<CampaignBundle>; // returns the safety-net fallback if unknown/unreachable
239
+ list?(): Promise<CampaignSummary[]>;
240
+ }
241
+ // Primary: HubSpotContentProvider — runs server-side (in the page's server component / a route
242
+ // handler). Reads the campaign content from HubSpot (see §9.10 for the exact HubSpot
243
+ // mechanism), maps it to a CampaignBundle, and caches it. Secrets stay server-side.
244
+ // Fallback: a minimal built-in DEFAULT bundle (engineering safety net only, NOT authored marketing
245
+ // copy) so the funnel still renders if a slug is unknown or HubSpot is briefly unreachable.
246
+ ```
247
+ `resolve(slug)` runs on the server during render (SSR), so the HubSpot fetch + field mapping to the
248
+ `CampaignBundle` shape (§6) happens before the page is sent, with Next caching / revalidation so
249
+ marketing edits appear quickly without a deploy (and the public site stays SEO-friendly). The client
250
+ components receive the resolved bundle as props / via `useCampaign()`; they never see HubSpot.
251
+
252
+ > **Key sub-decision (open, §9.10):** *how* campaign content is represented in HubSpot. HubSpot's
253
+ > "Campaigns" tool is built for attribution, not rich structured copy, so this depth of content
254
+ > (headlines, a ~10-entry Q&A pool, agent identity, call summaries) needs a deliberate home. Likely
255
+ > **HubDB** (a CMS-Hub table: one row per campaign, a `slug` column + content columns, API-fetchable)
256
+ > or a **custom object**. This choice drives the fetch + the field mapping and needs confirmation with
257
+ > whoever owns the HubSpot instance.
258
+
259
+ ### 5.3 Campaign as data, components as renderers
260
+ Follow the TOGA config-driven precedents (`toga25-supply` `useClientFields`; `toga2-commerce` Cart
261
+ C1..C7): resolve a bundle keyed by campaign, always with a `DEFAULT` fallback; render through the
262
+ components; never branch `if (campaign === 'x')` in JSX. Non-serializable behavior (a CTA action,
263
+ an icon) is referenced by an **enum key** hydrated via a `Record<key, fn|Component>` registry at the
264
+ view-model layer, so bundles stay pure data (so HubSpot only ever stores plain strings/values).
265
+
266
+ ### 5.4 Flow state machine
267
+ A `useAgentFlow(campaign)` hook owns `screen` + transitions (the `go()` / `back` map from `app.jsx`),
268
+ the modal gates (`welcome`, `welcome2`, `earlyAccess`), and step derivation (`stepFor`). Screens are
269
+ presentational and receive copy from the bundle + handlers from the hook. This mirrors the existing
270
+ shell but makes the copy and agent identity injected rather than inlined.
271
+
272
+ ### 5.5 How the front end knows which campaign to display (URL-driven, slug-keyed)
273
+ **Short answer: yes, it is URL-driven. Every campaign has a stable `slug`; the server reads the slug
274
+ from the request URL and loads that campaign's data through the provider (at render time).** End to end:
275
+
276
+ **1. Each campaign has a slug.** A short, stable identifier, for example `bdr-service`, `healthcare`.
277
+ The slug is the key marketing's HubSpot campaign is looked up by. It lives **in HubSpot, not the
278
+ repo** (§5.2); the repo only holds the type/schema, the HubSpot->bundle mapping, and a minimal
279
+ safety-net `DEFAULT`.
280
+
281
+ **2. The URL carries the slug.** Two supported shapes (both resolved by one `resolveCampaignId()`):
282
+ - **Query param (recommended to ship first):** `bdr.togatech.com/?campaign=bdr-service`. Matches how
283
+ leads actually arrive: HubSpot outreach links are query-based, and `info` already reads a
284
+ `?hsCampaignId=` param, so this slots into existing outreach with no new infrastructure.
285
+ - **Path (add later, optional):** `bdr.togatech.com/c/bdr-service`. Cleaner shareable URLs; added via
286
+ the client router without touching components once the query path exists.
287
+
288
+ **3. Resolution order** (first match wins): explicit `?campaign=<slug>` -> mapped `?hsCampaignId=<id>`
289
+ (id -> slug lookup, so CRM links keep working) -> `DEFAULT`. An unknown or missing slug always falls
290
+ back to the `DEFAULT` bundle, so the funnel never fails to render.
291
+
292
+ **4. The slug drives a runtime fetch of the content from HubSpot** (this is the confirmed requirement):
293
+ the page's server component calls `provider.resolve(slug)`, which reads that campaign's content from
294
+ **HubSpot** (§9.10), maps it to a `CampaignBundle`, and caches it (Next revalidation). Because this is
295
+ server-side, the fully-populated page is rendered and sent (good for the public site's SEO). Marketing
296
+ edits in HubSpot show up on the next revalidation. **No repo content, no redeploy, no developer** to
297
+ change copy. If the slug is unknown or HubSpot is briefly unreachable, the minimal `DEFAULT` safety net
298
+ renders so the funnel never breaks.
299
+
300
+ **5. Multiple campaigns run simultaneously by construction.** Selection is per-visitor, derived from
301
+ their URL each load; there is no global "current campaign" state. Any number of campaigns are live at
302
+ once, each just a different slug.
303
+
304
+ **6. CRM/analytics linkage travels in the bundle.** The resolved bundle carries `hsCampaignId` /
305
+ `togaCampaignUuid`, used when the lead is upserted/linked (§5.6) and for per-campaign GA4, so every
306
+ lead and event is attributed to the right campaign.
307
+
308
+ **Recommendation:** ship **query-param entry + the HubSpot content provider** from the start (that is
309
+ the confirmed requirement); add **path-based routing** as a nicety later. Keep all of this behind
310
+ `resolveCampaignId()` + `useCampaign()` so none of it is visible to the screens. The one thing that
311
+ must be pinned down before Phase 1 is **how the content is modeled in HubSpot** (§9.10).
312
+
313
+ ### 5.6 Backend adapters (run in the Next.js server)
314
+ The client screens call two interfaces; both run **server-side** (in route handlers / server actions),
315
+ so secrets never reach the browser:
316
+ ```ts
317
+ interface LeadSink { upsertLead(input): Promise<LeadRef>; } // today: Toga upsert + HubSpot
318
+ interface CallbackService { requestCall(ref, whenOrNow, phone): Promise<void>; } // today: Toga requestCall
319
+ ```
320
+ Because BDR is Next.js, these live in the app's own server layer (route handlers such as
321
+ `app/api/call-now/route.ts`, mirroring `info`), so **there is no separate backend to host**.
322
+ Implementation is `info`'s existing logic (`lib/toga.ts`,
323
+ `lib/hubspot.ts`, and its `call-now` / `call-later` route handlers): it **already exists and works**,
324
+ we port it directly, minus the PII logging. The mockup's `Success` simulation is replaced by a real
325
+ `CallbackService` call (immediate vs. scheduled maps to `CONTACT_CALL_TYPE_UUID`).
326
+
327
+ This funnel is the front door of the existing **AI-BDR** product, so `CallbackService` is implemented
328
+ with that system's callback path (the `info`-ported Toga `requestCall`). The adapter seam keeps the
329
+ client and interfaces stable if that backend path changes.
330
+
331
+ ### 5.7 `togatech` website integration (eventual)
332
+ The site will ultimately be surfaced through the TOGA Technology website (`togatech` repo). Design for
333
+ it now: keep the funnel self-contained and embeddable, prefer a linkable per-campaign URL so
334
+ `togatech` CTAs deep-link into a campaign, and express shared branding as tokens. Integration
335
+ mechanism (subdomain / linked Amplify app vs. iframe vs. absorption into `togatech`) is an open
336
+ question (§9), needed before deploy, not before building.
337
+
338
+ ### 5.8 Asset pipeline
339
+ Migrate agent art (Human/Owl/Robot × up-to-9 slots), talking videos, the customization walkthrough
340
+ video, and particle/Lottie assets into `public/`. Keep `preloadAgentAssets` behavior so screen swaps
341
+ never blank. Prune unused slots per the campaigns actually shipped (the default flow uses one agent).
342
+
343
+ ---
344
+
345
+ ## 6. Campaign bundle schema (concrete sketch)
346
+ This is the shape the app consumes. It is **produced server-side by mapping the HubSpot campaign
347
+ content (§5.2) into this structure** — it is the contract between "how marketing's content is stored
348
+ in HubSpot" and "what the components render," and the mapping layer is where HubSpot fields become
349
+ these fields (and where the no-em-dash normalization runs). Serializable data only; behavior/icons by
350
+ key. Covers the real content surfaces from §2.4.
351
+
352
+ > The richer fields here (the nested `pool: QA[]` + `sets`, `callSummaries`, `RichLine` accent spans)
353
+ > are the parts hardest to represent in a marketing-friendly way in HubSpot — a direct input to the
354
+ > §9.10 modeling decision. Flat columns handle the simple strings; nested arrays likely need child
355
+ > rows or a structured (JSON) field.
356
+ ```ts
357
+ interface AgentIdentity {
358
+ form: "Human" | "Owl" | "Robot";
359
+ slot: number; // 0..8
360
+ accent: string; // one of the 5 TOGA accents -> --accent
361
+ voiceGender: "Female" | "Male";
362
+ voiceIndex: number;
363
+ name: string; // e.g. "Alex"
364
+ }
365
+
366
+ interface QA { id: string; q: [string, string, string]; a: string; } // a: first-person, no em dashes
367
+
368
+ interface CampaignBundle {
369
+ id: string; // slug
370
+ hsCampaignId?: string; // HubSpot linkage
371
+ togaCampaignUuid?: string; // Toga addContactToCampaign linkage
372
+ agent: AgentIdentity; // who the campaign presents (default flow does not let user pick)
373
+ welcome: { eyebrow: string; heading: string; body: string; values: string[]; primaryCta: string; };
374
+ landing?: { heading: RichLine; lede: string; tiles: ServiceTile[]; }; // if the service picker is used
375
+ capabilities: {
376
+ step2Intro: { heading: string; body: string; values: string[]; tiles: HowTile[]; };
377
+ heading: RichLine; lede: string;
378
+ skills: { icon: string; title: string; desc: string }[]; // CAPS
379
+ anchor: QA; pool: QA[]; sets: string[][]; // Q&A bank + rotation
380
+ ctas: { callNow: string; schedule: string; connectNote: string; };
381
+ };
382
+ call: { heading: string; body: string; footnote: string; };
383
+ schedule: { heading: string; footnote: string; durationLabel: string; };
384
+ success: { callSummaries: { key: string[]; next: string[] }[]; scheduledNote: string; };
385
+ build?: { steps: { label: string; value?: string }[]; }; // AutoBuild beats
386
+ analytics?: { measurementId?: string; eventPrefix?: string };
387
+ features?: string[]; // capability flags (e.g. "showServicePicker", "allowManualBuilder")
388
+ }
389
+ ```
390
+ `RichLine` supports the mockup's accent-inked span (for example `"<agent> is ready to work."`).
391
+ The provider must always return a valid bundle: on an unknown slug or a HubSpot hiccup it falls back
392
+ to a minimal built-in `DEFAULT` (a safety net, not authored marketing copy) so the funnel still renders.
393
+
394
+ ---
395
+
396
+ ## 7. Proposed repo structure (Next.js App Router + TypeScript)
397
+ ```
398
+ bdr/
399
+ src/
400
+ app/
401
+ layout.tsx globals.css # shell + ported styles.css / CSS variables
402
+ page.tsx # SERVER component: resolve slug -> campaign, prime lead, render flow
403
+ c/[slug]/page.tsx # optional path-based campaign entry (later)
404
+ api/{lead,call-now,call-later}/route.ts # SERVER: secret-bearing Toga/HubSpot calls (ported from info)
405
+ content/
406
+ schema.ts # CampaignBundle types (the contract)
407
+ hubspotProvider.ts # SERVER: reads campaign content from HubSpot + maps to bundle (§5.2/§6)
408
+ useCampaign.ts # client hook to read the resolved bundle
409
+ default.ts # minimal safety-net fallback ONLY (not authored copy)
410
+ # NOTE: no per-campaign content files here — campaign copy lives in HubSpot, not the repo
411
+ flow/
412
+ useAgentFlow.ts # screen state machine + modal gates + step rail (client)
413
+ screens/{Landing,AutoBuild,Capabilities,CallNow,Schedule,Success}.tsx # "use client"
414
+ modals/{Welcome,WelcomeStep2,EarlyAccess}.tsx
415
+ components/ # ported shared lib (SwapFade, Particles, AgentArt, VoiceDial, ColorSelector, SynapseButton, StepRail, Wordmark, Ico, ...)
416
+ server/ # secret-bearing modules, ported from info (PII logging removed)
417
+ toga.ts hubspot.ts leadSink.ts callbackService.ts
418
+ lib/ analytics.ts assetPath.ts formatPhone.ts agentData.ts # AGENT_ART/NAMES/COLORS/VOICES
419
+ public/ # migrated agent art, talking videos, walkthrough, particles
420
+ amplify.yml next.config.ts tsconfig.json eslint (no-em-dash copy rule on in-repo strings)
421
+ ```
422
+
423
+ ---
424
+
425
+ ## 8. Phased execution plan
426
+ Sizing note (from the meeting): once one screen is built cleanly the rest go fast (heavy component
427
+ reuse); the long pole is pixel-perfect UI parity, not logic. We hand Claude the actual mockup source,
428
+ so fidelity should be near-exact.
429
+
430
+ - **Phase 0 — Bootstrap `bdr`.** New Next.js (App Router) + TypeScript app (mirror `info`'s config;
431
+ Amplify SSR hosting). Port `styles.css` and the CSS-variable accent system. Set up campaign routing
432
+ (query param now, `/c/[slug]` later) and ESLint incl. a no-em-dash check on in-repo strings. Read
433
+ `node_modules/next/dist/docs/` first (Next breaking changes per `info/AGENTS.md`).
434
+ - **Phase 1 — Content layer.** Depends on the §9.10 HubSpot-modeling decision. Define `schema.ts`
435
+ (the `CampaignBundle` contract), the server-side `hubspotProvider` (`resolve(slug)` reads HubSpot +
436
+ maps to a bundle, with caching/revalidation + no-em-dash normalization), the `useCampaign` client
437
+ hook, and a minimal safety-net `DEFAULT`. Seed HubSpot with one real campaign (extract the mockup's
438
+ current copy into HubSpot, not the repo) to develop against. Unit-test mapping + fallback.
439
+ - **Phase 2 — Port the component library.** Bring `components.jsx` across as real modules (SwapFade,
440
+ Particles, AgentArt + data, VoiceDial, ColorSelector, SynapseButton, StepRail, Wordmark, Ico,
441
+ asset/preload). Migrate assets to `public/`. This unblocks every screen.
442
+ - **Phase 3 — Build one screen end to end (Capabilities), config-driven.** It is the richest
443
+ (skills + Q&A bank + CTAs), so building it first calibrates the real estimate and proves the bundle
444
+ schema before the remaining screens.
445
+ - **Phase 4 — Remaining screens + modals from config.** AutoBuild, Landing (behind a feature flag),
446
+ Call Now, Schedule, Success, Welcome/WelcomeStep2/EarlyAccess, StepRail. Remove all hardcoded copy.
447
+ - **Phase 5 — Wire the real backend.** Port `info`'s `lib/toga.ts` / `lib/hubspot.ts` + its call
448
+ route handlers into BDR's `app/api/*` (server-side); wire the `LeadSink` / `CallbackService`
449
+ adapters; replace the `Success` simulation with real immediate/scheduled `requestCall`; remove PII
450
+ logging; resolve the `visit` route (implement or drop).
451
+ - **Phase 6 — Second campaign + multi-campaign.** Author a second campaign **in HubSpot** (for example
452
+ a `healthcare` variant using the healthcare proof points) alongside `bdr-service`; prove that adding a
453
+ campaign needs only HubSpot edits (no code, no deploy) and zero component diffs; validate slug
454
+ resolution and simultaneous campaigns.
455
+ - **Phase 7 — Analytics + polish.** Per-campaign GA4, pixel-perfect pass vs. the mockup, motion/timing
456
+ parity, accessibility check.
457
+ - **Phase 8 — Deploy.** Amplify env vars, README, "how to add a campaign" doc; confirm `togatech`
458
+ integration mechanism.
459
+
460
+ ---
461
+
462
+ ## 9. Open questions (leadership + HubSpot owner). Most do not block early work; **§9.10 gates Phase 1**.
463
+ 1. **Backend relationship:** RESOLVED — `bdr` is the new UI / front door of TOGA's existing **AI-BDR**
464
+ product (not standalone); it reuses that system's backend calls, and its knowledge is filed under
465
+ `2.0/apps/ai-bdr/`. `CallbackService` uses that system's callback path.
466
+ 2. **Runtime editability:** RESOLVED — content is authored by marketing **in HubSpot** and fetched by
467
+ slug at runtime; no repo content, no redeploy. The remaining question is the HubSpot mechanism (§9.10).
468
+ 3. **Agent choice:** Stay with a campaign-fixed agent (auto-build to a preset), or ship the manual
469
+ Creator (currently parked / "coming soon")? Determines whether `Creator` + full slot art ship now.
470
+ 4. **Service picker:** Is the `Landing` service-selection screen (AI BDR + "Talos coming soon") in
471
+ scope for launch, or parked like in the mockup?
472
+ 5. **Campaign catalog:** How many campaigns at launch and who authors copy? Confirms entry UX (§5.5).
473
+ 6. **`togatech` integration mechanism:** subdomain / linked Amplify app vs. iframe/embed vs. absorption
474
+ into `togatech`. Needed before Phase 8.
475
+ 7. **Tweaks panel:** keep the dev tuning panel (synapse/radius/accent) in production or strip it?
476
+ 8. **First campaign direction + verbiage** (BDR-as-a-service vs. healthcare): needed only to fill the
477
+ first bundle, not to build the engine.
478
+ 9. **Backend hosting:** RESOLVED by the Next.js choice — the secret-bearing HubSpot/Toga calls run in
479
+ BDR's own server components / route handlers; no separate backend to host. Only remaining item is
480
+ confirming the Amplify **SSR** hosting target (not static) at deploy.
481
+ 10. **HubSpot content mechanism (blocks Phase 1) — the big one.** *How* is campaign content stored in
482
+ HubSpot so it is marketing-editable and fetchable by slug? `info` gives no precedent (it only reads
483
+ contacts). Options: **HubDB** (CMS-Hub table, one row per campaign, `slug` column + content columns,
484
+ API-fetchable — the likely fit) vs. a **custom object** vs. properties on the Campaigns object. This
485
+ determines the fetch + the HubSpot->`CampaignBundle` mapping. Sub-parts to settle with the HubSpot
486
+ owner: which mechanism; how the **nested content** (Q&A pool + rotation sets, call summaries, accent
487
+ `RichLine`s) is represented (flat columns can't hold arrays cleanly — child rows or a JSON field);
488
+ who authors/owns the schema; and how the slug relates to the existing `hsCampaignId`/Toga campaign
489
+ UUID used for attribution.
490
+
491
+ ---
492
+
493
+ ## 10. Risks & gotchas
494
+ - **Next breaking changes** (per `info/AGENTS.md`): read the bundled `node_modules/next/dist/docs/`
495
+ before coding; do not assume older-Next APIs.
496
+ - **Keep secrets in server code.** `HUBSPOT_ACCESS_TOKEN` / `TOGA_CLIENT_API_SECRET` and the
497
+ token-exchange must live in server components / route handlers, never in a `"use client"` component
498
+ or a `NEXT_PUBLIC_` env var (those ship to the browser). The interactive screens are client
499
+ components, so the boundary matters: secret-bearing calls stay in `app/api/*` + server modules.
500
+ - **No `2.0/standards/frontend.md`** exists; `toga2-view` conventions are the de-facto standard
501
+ (single axios/api client, `use*ViewModel` hooks, presentational views). Keep the viewmodel-vs-view
502
+ split in spirit.
503
+ - **PII in logs** (existing `info` debt): log ids only, never email/phone/tokens.
504
+ - **HubSpot as content store is net-new** (no `info` precedent). Biggest unknown is fitting the mockup's
505
+ rich, nested content (Q&A pool + sets, call summaries, accent `RichLine`s) into something marketing can
506
+ edit (§9.10). Flat strings are easy; nested arrays are the hard part.
507
+ - **Runtime fetch = latency + availability**: the content now depends on a HubSpot call on load. Cache
508
+ in the backend with short revalidation, and keep the `DEFAULT` safety net so a HubSpot hiccup or an
509
+ unknown slug never breaks the funnel. Mind HubSpot API rate limits.
510
+ - **Serializable content only**: the bundle stays pure data (no JSX/functions); behavior/icons are enum
511
+ keys hydrated in code, so HubSpot only ever needs to store strings/values.
512
+ - **No-em-dash cannot be linted on HubSpot copy**: enforce via marketing author guidance + a
513
+ normalization pass in the backend mapping.
514
+ - **DEFAULT must always render**: an unknown/absent slug (or HubSpot down) falls back cleanly, never a
515
+ 404 funnel.
516
+ - **Single accent source**: every accent-tinted element routes through `var(--accent)` /
517
+ `color-mix`; never hardcode an accent (mockup `CLAUDE.md` rule).
518
+ - **Motion fidelity**: the mockup hardens transitions against stalled animation clocks (settle guards,
519
+ effect-owned timers). Preserve these when porting; do not "simplify" them away.
520
+ - **Asset weight**: full slot art/video for 3 forms × 9 slots is large; ship only what the launched
521
+ campaigns use.
522
+
523
+ ## 11. Cleanup / fidelity checklist
524
+ - [ ] No hardcoded marketing copy in any component (grep-verified).
525
+ - [ ] No `if (campaign === …)` branches in components.
526
+ - [ ] No em dashes in any shipped string; single `--accent` source respected.
527
+ - [ ] `info` debug logging dropped; no PII in logs.
528
+ - [ ] `visit` route resolved (implemented or removed).
529
+ - [ ] Env vars documented; no secrets committed.
530
+ - [ ] Pixel-perfect parity pass against the mockup screens + screenshots.
531
+
532
+ ## Change history
533
+ - 2026-07-08 — Captured the full BDR web-funnel implementation plan verbatim into the knowledge repo as a companion to the distilled `web-funnel-content-model.md` feature doc (redundancy intentional). (tcox)
@@ -0,0 +1,148 @@
1
+ ---
2
+ title: Web Funnel — Next.js UI + Config-Driven Campaign Content
3
+ framework: "2.0"
4
+ repo: ai-bdr
5
+ project: AI-BDR
6
+ client: shared
7
+ type: feature
8
+ status: draft
9
+ updated: 2026-07-08
10
+ owners: [tcox]
11
+ files:
12
+ - bdr/src/content/schema.ts
13
+ - bdr/src/content/hubspotProvider.ts
14
+ - bdr/src/content/useCampaign.ts
15
+ - bdr/src/content/default.ts
16
+ - bdr/src/server/leadSink.ts
17
+ - bdr/src/server/callbackService.ts
18
+ - bdr/src/app/api/call-now/route.ts
19
+ - bdr/src/app/api/call-later/route.ts
20
+ related:
21
+ - ../architecture.md
22
+ - call-orchestration.md
23
+ ---
24
+
25
+ ## What this is
26
+
27
+ The **public-facing web funnel** is the new UI / lead-capture front door of the **same
28
+ AI-BDR product** documented in `../architecture.md`. It is a **Next.js (App Router) +
29
+ TypeScript** app living in its own code repo, `bdr` (registered separately because it is a
30
+ distinct codebase). Only the UI/content layer is new — the funnel **reuses this AI-BDR
31
+ system's existing backend calls** (see "Backend reuse" below); the API/backend contracts
32
+ are already documented and are **not** restated here.
33
+
34
+ The UI is built pixel-perfect from the mockup at `BDR/mockup`: a **7-screen "Agent Studio"
35
+ flow** — Welcome → AutoBuild → Capabilities → Call Now / Schedule → Success. The
36
+ agent-identity model is **form × slot × accent × voice × name** (form Human/Owl/Robot ×
37
+ slot 0–8 × one of 5 TOGA accents → `--accent` × voice × name). Two shipped-copy rules from
38
+ the mockup are load-bearing: a **single `--accent` source** (every accent-tinted element
39
+ routes through `var(--accent)` + derived `--accent-fill`; never hardcode an accent) and
40
+ **no em dashes** in any shipped copy.
41
+
42
+ Full implementation plan: `BDR/PLAN.md`.
43
+
44
+ ## Config-driven campaign content (HubSpot-owned, fetched by slug at runtime)
45
+
46
+ Every campaign is a **serializable data bundle** (`CampaignBundle`) keyed by a stable
47
+ `slug`. **Campaign content is authored by the marketing team in HubSpot** and fetched **by
48
+ slug at runtime, server-side**, then mapped to a `CampaignBundle`. Content is **not** stored
49
+ in the repo — a new campaign or a copy edit ships with **no developer and no redeploy**,
50
+ only a HubSpot edit.
51
+
52
+ Config-driven, per TOGA precedent (`toga25-supply`'s `useClientFields`,
53
+ `toga2-commerce`'s Cart C1–C7): components are **pure renderers of a bundle**; there is
54
+ **no `if (campaign === 'x')` branching** anywhere in the screens. The repo holds only the
55
+ **schema** (`CampaignBundle` types), the **HubSpot→bundle mapping** (provider), and a
56
+ minimal safety-net **`DEFAULT`** bundle (an engineering fallback, not authored copy).
57
+
58
+ - **Provider seam.** `CampaignContentProvider.resolve(slug)` returns a `CampaignBundle` (or
59
+ `DEFAULT` on an unknown/unreachable slug). `HubSpotContentProvider` runs **server-side**
60
+ (page server component / route handler), maps HubSpot content to a `CampaignBundle`,
61
+ caches it (Next revalidation), and applies the no-em-dash normalization pass. Client
62
+ components receive the resolved bundle as props / via `useCampaign()` and never see
63
+ HubSpot.
64
+ - **Serializable-only bundles.** Non-serializable behavior (a CTA action, an icon) is
65
+ referenced by an **enum key** and hydrated at the view-model layer through a
66
+ `Record<key, fn|Component>` registry — HubSpot only ever stores plain strings/values.
67
+ - **Campaign selection is URL-driven, per-request.** `resolveCampaignId()`, first match
68
+ wins: `?campaign=<slug>` → mapped `?hsCampaignId=<id>` (id→slug lookup so existing
69
+ outreach/CRM links keep working) → `DEFAULT`. Query-param entry ships first (matches how
70
+ leads arrive and how the ported backend already reads `?hsCampaignId=`); optional
71
+ `/c/[slug]` path later. No global "current campaign" state — selection is derived from
72
+ each visitor's URL, so any number of campaigns run **simultaneously**.
73
+ - **Attribution travels in the bundle.** The resolved bundle carries `hsCampaignId` and
74
+ `togaCampaignUuid`, used when the lead is upserted/linked and for per-campaign GA4.
75
+
76
+ ### CampaignBundle shape (contract)
77
+
78
+ Keyed by `slug` (`id`). Carries `agent: AgentIdentity` (form × slot × accent → `--accent` ×
79
+ voice × name; the default flow does not let the user pick, so agent identity is effectively
80
+ campaign config), plus `welcome`, optional `landing`, `capabilities` (skill cards +
81
+ `anchor`/`pool`/`sets` Q&A bank + CTAs), `call`, `schedule`, `success` (`callSummaries`),
82
+ optional `build` (AutoBuild beats), `analytics`, and `features[]` capability flags.
83
+ `RichLine` supports the accent-inked span the mockup uses (e.g. `"<agent> is ready to
84
+ work."`). The mapping layer is the seam between HubSpot storage and this contract.
85
+
86
+ ## Backend reuse (from the `info` repo; existing AI-BDR calls)
87
+
88
+ The funnel's Call Now / Schedule screens submit to a real backend **ported from the
89
+ external `info` repo** (github.com/agilantsolutions/info — a Next.js app whose `/bdr` route
90
+ already runs these calls in production). `info` is an **external reference repo, not a TOGA
91
+ registry repo** — port its logic, do not register or rebuild it. The ported logic drives
92
+ **this AI-BDR system's existing backend calls** (HubSpot contact fetch, Toga upsert +
93
+ campaign link, `requestCall` callback) — those contracts are already documented in
94
+ `call-orchestration.md` and `../architecture.md`; do **not** restate them.
95
+
96
+ Because BDR is Next.js there is no separate backend to host — all secret-bearing calls run
97
+ server-side (route handlers / server actions in `app/api/*`), wrapped behind two adapters
98
+ so only the impl changes later:
99
+
100
+ - `LeadSink.upsertLead(input)` → Toga upsert-by-email + campaign link + HubSpot.
101
+ - `CallbackService.requestCall(ref, whenOrNow, phone)` → Toga `requestCall`, immediate vs.
102
+ scheduled mapped to `CONTACT_CALL_TYPE_UUID`.
103
+
104
+ Note on "campaign": `info` uses HubSpot **only to fetch the contact**; the URL's
105
+ `hsCampaignId` is passed to Toga as the attribution campaign UUID
106
+ (`addContactToCampaign`) — attribution-only in `info`. **Fetching marketing content by
107
+ slug from HubSpot is net-new** to this funnel (above). The content slug and the attribution
108
+ UUID are related but distinct; the `CampaignBundle` carries the UUID so a slug maps to the
109
+ right campaign for logging.
110
+
111
+ ### Environment (values live in env / Amplify config, never in the repo)
112
+
113
+ - `HUBSPOT_ACCESS_TOKEN`
114
+ - `TOGA_API_BASE_URL`, `TOGA_CLIENT_ID`, `TOGA_CLIENT_API_UUID`, `TOGA_CLIENT_API_SECRET`
115
+ - `NEXT_PUBLIC_GA_MEASUREMENT_ID`
116
+
117
+ ## Gotchas
118
+
119
+ - **HubSpot content mechanism is an OPEN question that gates the content-layer phase.**
120
+ `info` gives no precedent (it only *reads contacts*). Candidate homes: **HubDB** (CMS-Hub
121
+ table — one row per campaign, `slug` column + content columns, API-fetchable; likely fit)
122
+ vs. a **custom object** vs. properties on the Campaigns object. The hard part is the
123
+ **nested content** — Q&A pool + rotation sets, call summaries, accent `RichLine`s — which
124
+ flat columns can't hold cleanly, so it needs child rows or a structured (JSON) field.
125
+ Settle with the HubSpot owner before building the content layer.
126
+ - **Keep secrets server-side.** `TOGA_CLIENT_API_SECRET` / `HUBSPOT_ACCESS_TOKEN` and the
127
+ token exchange must live in server components / route handlers, never in a `"use client"`
128
+ component or a `NEXT_PUBLIC_` env var (those ship to the browser). The interactive Agent
129
+ Studio screens are client components, so this boundary is load-bearing.
130
+ - **Do NOT port `info`'s debug `console.log`s** — several log PII (email/phone). Log
131
+ identifiers only.
132
+ - **Do not port `info`'s hero/curiosity marketing UI** — it is a different, older funnel.
133
+ - **`DEFAULT` must always render.** An unknown/absent slug or a brief HubSpot outage falls
134
+ back to the built-in safety net — never a 404 funnel.
135
+ - **Runtime fetch adds latency + an availability dependency.** Cache server-side with short
136
+ revalidation; mind HubSpot API rate limits.
137
+ - **No em dashes in any shipped copy** (mockup rule) — can't be linted on HubSpot-authored
138
+ copy, so enforce via marketing author guidance + the normalization pass; in-repo strings
139
+ get an ESLint no-em-dash check.
140
+ - **Single accent source** (mockup rule): every accent-tinted element routes through
141
+ `var(--accent)` (+ derived `--accent-fill`); never hardcode an accent value.
142
+
143
+ ## Change history
144
+ - 2026-07-08 — Initial doc: Next.js App Router web funnel as the AI-BDR product's UI /
145
+ lead-capture front door; config-driven campaign bundle authored in HubSpot and fetched by
146
+ slug server-side at runtime (`DEFAULT` fallback, URL-driven per-request selection); backend
147
+ ported from the external `info` repo behind LeadSink/CallbackService adapters, reusing this
148
+ AI-BDR system's existing calls. HubSpot content mechanism still open. (tcox)
@@ -27,10 +27,11 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
27
27
  - **toga2-hub** (TOGa Hub) — 2 doc(s) → [2.0/apps/toga2-hub/INDEX.md](2.0/apps/toga2-hub/INDEX.md)
28
28
  - **talos** (TOGa IQ) — 7 doc(s) → [2.0/apps/talos/INDEX.md](2.0/apps/talos/INDEX.md)
29
29
  - **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
30
- - **ai-bdr** (AI-BDR) — 4 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
30
+ - **ai-bdr** (AI-BDR) — 6 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
31
31
  - **toga2-commerce** (TOGa Commerce) — 7 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
32
32
  - **toga25-supply** (TOGa 2.5 Supply) — 8 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
33
33
  - **toga-blox** (TOGa Blox) — 7 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
34
+ - **bdr** (BDR) — 0 doc(s) → [2.0/apps/bdr/INDEX.md](2.0/apps/bdr/INDEX.md)
34
35
 
35
36
  ## standalone framework
36
37
 
@@ -210,5 +210,14 @@
210
210
  "dependsOn": [
211
211
  "library"
212
212
  ]
213
+ },
214
+ {
215
+ "repo": "bdr",
216
+ "project": "BDR",
217
+ "framework": "2.0",
218
+ "role": "app",
219
+ "dependsOn": [
220
+ "api2"
221
+ ]
213
222
  }
214
223
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.286",
3
+ "version": "1.0.288",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",