toga-ai 1.0.287 → 1.0.289
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.
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -0
- package/knowledge/2.0/apps/_underscore/features/component-model-namespace-registration.md +102 -0
- package/knowledge/2.0/apps/_underscore/features/forecast-sale-import.md +25 -2
- package/knowledge/2.0/apps/ai-bdr/INDEX.md +1 -0
- package/knowledge/2.0/apps/ai-bdr/features/bdr-web-funnel-plan.md +533 -0
- package/knowledge/INDEX.md +2 -2
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
| [Assortment Name Translation (AssortmentTranslations sidecar)](features/assortment-name-translation.md) | Serves Assortment (product-grouping) **names** in multiple languages by adding a per-language **sidecar** table `AssortmentTranslations`, reusing the platform's | _underscore/Model/Client/AssortmentTranslation.php, dbchanges2/Client/2026-06-26a - AssortmentTranslations.sql, dbchanges2/Core/2026-06-26a - AssortmentTranslationsRecord.sql, dbchanges2/Client/2026-06-26b - AssortmentTranslationsAcl.sql |
|
|
9
9
|
| [Asynchronous Query Execution (writes-only, via Worker)](features/async-query-execution.md) | `_Query` can run a **write** query asynchronously so a long/slow write does not hold a request-scoped DB connection open long enough to hit **"MySQL server has | _underscore/Query.php, worker2/Worker/Infrastructure/Database.php, worker2/Worker/Team/Transcripts.php |
|
|
10
10
|
| [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
|
|
11
|
+
| [_Component_*/_Model_* project-namespace registration (autoloader) & backslash-qualify traps](features/component-model-namespace-registration.md) | Every **project-local** `_Component_*` and `_Model_*` class in a 2.0 app **must declare the project namespace** at the top of the file: ```php namespace <NAMESP | _underscore/Loader.php, worker2/_.php, api2/_.php, worker2/Component/Forecast/Db/Db.php, worker2/Component/Forecast/SaleImport/SaleImport.php, api2/Component/Api/Netsuite/Netsuite.php |
|
|
11
12
|
| [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
|
|
12
13
|
| [Error Reporting — Issue/Event Aggregation (exceptionHandler)](features/error-reporting-issue-event.md) | `_underscore`'s global exception handler persists every uncaught exception into a two-table **Issue / Event** model in the **shared Core Logs DB** (`_underscore | _underscore/Error.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/Event.php, dbchanges2/Logs/2026-07-06 - Issue and Event tables.sql |
|
|
13
14
|
| [Record-Changed Event Publishing (_Event::publish to SQS)](features/event-publish-sqs.md) | `_Event::publish()` (in `_underscore/Event.php`) is the PHP side of the real-time event pipeline. | _underscore/Event.php |
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "_Component_*/_Model_* project-namespace registration (autoloader) & backslash-qualify traps"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-08
|
|
10
|
+
owners: [dfranks]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Loader.php
|
|
13
|
+
- worker2/_.php
|
|
14
|
+
- api2/_.php
|
|
15
|
+
- worker2/Component/Forecast/Db/Db.php
|
|
16
|
+
- worker2/Component/Forecast/SaleImport/SaleImport.php
|
|
17
|
+
- api2/Component/Api/Netsuite/Netsuite.php
|
|
18
|
+
related:
|
|
19
|
+
- ../architecture.md
|
|
20
|
+
- ./forecast-sale-import.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
Every **project-local** `_Component_*` and `_Model_*` class in a 2.0 app **must declare the
|
|
25
|
+
project namespace** at the top of the file:
|
|
26
|
+
|
|
27
|
+
```php
|
|
28
|
+
namespace <NAMESPACE>; // worker2 → 'worker', api2 → 'api'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
where `<NAMESPACE>` is the value of that project's `_.php` `const NAMESPACE` (worker2 =
|
|
32
|
+
`'worker'`, api2 = `'api'`). The `namespace` declaration **IS the "registration"** — there
|
|
33
|
+
is no config toggle. If the class is declared in the **global** namespace instead, the
|
|
34
|
+
`_underscore` autoloader throws at **class-load (runtime)**:
|
|
35
|
+
|
|
36
|
+
> The namespace for '`<Class>`' located in '`<path>`' has not been defined which is
|
|
37
|
+
> prohibited for all _Model and _Component classes. Define the namespace for this class and
|
|
38
|
+
> try again.
|
|
39
|
+
|
|
40
|
+
**This is NOT caught by `php -l`** (parse-only) — it only surfaces at autoload/runtime, so
|
|
41
|
+
lint-clean code can still fatally throw on the first request that touches the class.
|
|
42
|
+
|
|
43
|
+
## How it works — `_underscore/Loader.php` (lines ~71-83)
|
|
44
|
+
For a project-local `_Component_*` / `_Model_*` class the autoloader:
|
|
45
|
+
1. `require`s the local file.
|
|
46
|
+
2. Checks whether the class **also** exists under the project namespace
|
|
47
|
+
(`<NAMESPACE>\<Class>`).
|
|
48
|
+
3. **If yes** → `eval`s a bridge class in the global namespace
|
|
49
|
+
(`class <Class> extends <NAMESPACE>\<Class> {}`, Loader.php:79-80) so callers can use the
|
|
50
|
+
bare `_Component_Foo` name.
|
|
51
|
+
4. **If no** (class was declared globally, so `<NAMESPACE>\<Class>` doesn't exist) → **throws**
|
|
52
|
+
the "namespace … has not been defined" error above.
|
|
53
|
+
|
|
54
|
+
**Correct precedent to copy:** `api2/Component/Api/Netsuite/Netsuite.php` —
|
|
55
|
+
`namespace api;` + `class _Component_Api_Netsuite extends \_Component` + `\_Config::netsuite(...)`,
|
|
56
|
+
`new \_ApiRequest(...)`.
|
|
57
|
+
|
|
58
|
+
### Framework references inside a namespaced file — what resolves vs. what must be backslash-qualified
|
|
59
|
+
Once the file is in `namespace <NAMESPACE>;`, **static calls and constants** to framework
|
|
60
|
+
classes resolve transparently via the autoloader's namespace fallback (Loader.php ~94-120),
|
|
61
|
+
bridged as `<NAMESPACE>\_X` → the framework class — e.g. `_Time::convert(...)`,
|
|
62
|
+
`_underscore::DB_FORECAST`, `_Component_Api_Netsuite::send(...)`, `new _Query(...)` all work
|
|
63
|
+
unqualified.
|
|
64
|
+
|
|
65
|
+
**Three constructs do NOT get that treatment and MUST be backslash-qualified:**
|
|
66
|
+
- `catch (\Exception $e)` / `catch (\RuntimeException $e)` — unqualified `catch (Exception $e)`
|
|
67
|
+
resolves to the non-existent `<NAMESPACE>\Exception` and silently no-matches (or fatals).
|
|
68
|
+
**This silent-swallow `catch` is the most dangerous / easiest-to-miss line** — the block
|
|
69
|
+
never catches, so error handling quietly does nothing.
|
|
70
|
+
- `new \Exception(...)` / `throw new \RuntimeException(...)` — same reason.
|
|
71
|
+
- `extends \_Component` (and `extends \_Model` etc.) — the base class in the class declaration
|
|
72
|
+
must be backslash-qualified.
|
|
73
|
+
|
|
74
|
+
## Why this matters — the Forecast SALES drift root cause
|
|
75
|
+
This mechanism was the root cause of the Forecast **SALES** drift observed after a worker2
|
|
76
|
+
push: `_Component_Forecast_SaleImport` and `_Component_Forecast_Db` had been declared in the
|
|
77
|
+
**global** namespace, so every NetSuite sales/invoice/opportunity webhook **fatally threw at
|
|
78
|
+
autoload before writing to `Forecast.Sales`**. Because NetSuite replays edits across old
|
|
79
|
+
transactions, the symptom **presented as ~7 months of historical drift** rather than as a
|
|
80
|
+
"new code broken" failure — a misleading symptom that this mechanism explains.
|
|
81
|
+
|
|
82
|
+
(The engine ultimately lives under `_underscore` — see the
|
|
83
|
+
[Forecast.Sales import engine doc](./forecast-sale-import.md) — precisely because a
|
|
84
|
+
`_Component_*` class loaded from a project path is rejected; but even a legitimately
|
|
85
|
+
project-local `_Component_*`/`_Model_*` must still declare the project namespace.)
|
|
86
|
+
|
|
87
|
+
## Gotchas / known issues
|
|
88
|
+
- `php -l` will **not** catch a missing/global namespace or an unqualified `catch`/`throw`/
|
|
89
|
+
`extends` — boot the class (autoload it) to verify. A wrong autoload path is likewise a
|
|
90
|
+
runtime-only failure.
|
|
91
|
+
- A global-namespace `_Component_*`/`_Model_*` fails **loudly** (the throw above). An
|
|
92
|
+
unqualified `catch (Exception $e)` fails **silently** — the more insidious of the two.
|
|
93
|
+
|
|
94
|
+
## Change history
|
|
95
|
+
- 2026-07-08 — Documented the `_Component_*`/`_Model_*` project-namespace registration
|
|
96
|
+
requirement (Loader.php eval-bridge mechanism, ~71-83), the runtime-only failure mode (not
|
|
97
|
+
caught by `php -l`), and the three constructs that must be backslash-qualified inside a
|
|
98
|
+
namespaced file (`catch`/`throw`/`new \Exception`, `extends \_Component`). Root-caused the
|
|
99
|
+
Forecast SALES drift to `_Component_Forecast_SaleImport`/`_Component_Forecast_Db` declared
|
|
100
|
+
in the global namespace (webhooks threw at autoload before writing; presented as 7-month
|
|
101
|
+
historical drift because NetSuite replays edits). Correct precedent:
|
|
102
|
+
`api2/Component/Api/Netsuite/Netsuite.php`. (dfranks)
|
|
@@ -38,6 +38,7 @@ related:
|
|
|
38
38
|
- ../../worker2/features/netsuite-salesorder-open-orders-sync.md
|
|
39
39
|
- ../../worker2/features/netsuite-supporting-record-webhook-importer.md
|
|
40
40
|
- ./netsuite-rest-client.md
|
|
41
|
+
- ./component-model-namespace-registration.md
|
|
41
42
|
---
|
|
42
43
|
|
|
43
44
|
## Summary
|
|
@@ -135,7 +136,14 @@ This is the **opposite of the legacy cron**, which reads the pre-signed listSale
|
|
|
135
136
|
(already `-foreignamount`) and so applies the factor **only** to the synthetic shipping
|
|
136
137
|
line. **Do not copy the cron's sign handling into the webhook** — it reintroduces the
|
|
137
138
|
~$470K credit-memo sign bug (credits stored positive). The cron is not authoritative for
|
|
138
|
-
the raw-REST path.
|
|
139
|
+
the raw-REST path.
|
|
140
|
+
|
|
141
|
+
> **Do not confuse this line-amount/revenue sign with the `amountDue` "store raw positive"
|
|
142
|
+
> decision (TRUE-78923).** The "store raw positive / don't sign-flip credit memos & refunds"
|
|
143
|
+
> guidance applies **only to `amountDue`** (which this importer does not write) — it does
|
|
144
|
+
> **NOT** apply to the line `amount`/revenue sign. For revenue, credit memos + cash refunds
|
|
145
|
+
> **must** land NEGATIVE and invoices + cash sales POSITIVE in `Forecast.Sales`; that is the
|
|
146
|
+
> stored contract. Mis-carrying the amountDue rule onto revenue would store credits positive. Confirmed end-to-end against the local Forecast mirror (creditMemo
|
|
139
147
|
revenue stored -350..-600; cashRefund revenue -885 / profit -78; invoice positive).
|
|
140
148
|
|
|
141
149
|
> A reviewer may flag "profit double-signs the factor" — false positive:
|
|
@@ -481,10 +489,25 @@ record is deleted in NetSuite.)
|
|
|
481
489
|
(e.g. `./` under worker2) with "namespace … has not been defined" — at **class-load
|
|
482
490
|
(runtime), not `php -l`**. So this engine lives in `_underscore` next to
|
|
483
491
|
`_Component_Forecast_Db` even though only worker2 uses it; it references the worker2 class
|
|
484
|
-
`_Worker_Netsuite_Item`, resolved lazily at runtime in the worker2 context.
|
|
492
|
+
`_Worker_Netsuite_Item`, resolved lazily at runtime in the worker2 context. **A
|
|
493
|
+
legitimately project-local `_Component_*`/`_Model_*` (e.g. the worker2 copies of these
|
|
494
|
+
classes) must still declare `namespace worker;` or it throws the same error at autoload —
|
|
495
|
+
and this exact global-namespace mistake caused the SALES drift (webhooks fatally threw
|
|
496
|
+
before writing, presenting as 7-month historical drift). See the
|
|
497
|
+
[namespace-registration doc](./component-model-namespace-registration.md) for the
|
|
498
|
+
eval-bridge mechanism and the `catch`/`throw`/`extends` backslash-qualify traps.**
|
|
485
499
|
- The cron's sign handling is not portable here — see Sign convention.
|
|
486
500
|
|
|
487
501
|
## Change history
|
|
502
|
+
- 2026-07-08 — **Clarified the revenue sign convention is NOT the `amountDue` "store raw
|
|
503
|
+
positive" rule.** The TRUE-78923 "don't sign-flip credit memos/refunds" guidance applies
|
|
504
|
+
only to `amountDue` (which this importer does not write); the line `amount`/revenue sign
|
|
505
|
+
contract is unchanged — credit memos + cash refunds NEGATIVE, invoices + cash sales
|
|
506
|
+
POSITIVE. Also cross-linked the new
|
|
507
|
+
[`_Component_*`/`_Model_*` namespace-registration doc](./component-model-namespace-registration.md):
|
|
508
|
+
a global-namespace declaration of `_Component_Forecast_SaleImport`/`_Component_Forecast_Db`
|
|
509
|
+
was the root cause of the SALES drift (webhooks threw at autoload before writing; presented
|
|
510
|
+
as 7-month historical drift because NetSuite replays edits). (dfranks)
|
|
488
511
|
- 2026-07-07 — **Documented the AMQ enqueuer's line-field inline-edit blind spot** (Forecast2 SALES
|
|
489
512
|
profit-drift forensics; invoice 6715127 / 2026-02-20 / $3,392.70). Editing a **sublist LINE
|
|
490
513
|
field** (e.g. cost `TRANLINE.MCOSTESTIMATE`) inline in the NetSuite UI raises **no** record UE
|
|
@@ -3,6 +3,7 @@
|
|
|
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 |
|
|
8
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 |
|
|
@@ -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)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -17,7 +17,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
17
17
|
|
|
18
18
|
## 2.0 framework
|
|
19
19
|
|
|
20
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
20
|
+
- **_underscore** (_Underscore) _(framework core)_ — 26 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
21
21
|
- **worker2** (Worker) — 27 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
22
22
|
- **api2** (API) — 10 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
23
23
|
- **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
@@ -27,7 +27,7 @@ _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) —
|
|
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)
|
package/package.json
CHANGED