The intent-driven hero for invergent.ai. A visitor types a use case in plain language, the agent comes back with a handful of refining chip questions to sharpen the brief, then composes a tailored plan (product mix, modules, deployment, timeline, scenario) and streams it back into a wizard + downloadable PDF.
Three phases, one session: Brief → Refine → Plan.
npm install
npm run dev # http://127.0.0.1:5175/?demo=1Without env vars the canned demo plays. To talk to a real backend, drop a .env.local:
VITE_HERO_API_URL=/agent
VITE_HERO_PUBLISHABLE_KEY=surg_wk_…The dev server proxies /agent/* to the configured Surogates host (TLS-skipped, Origin injected) so the browser sees same-origin and CORS stays out of the way.
Three trust layers between a visitor and the model:
Bundle (this repo, browser)
↓ bootstrap with Bearer surg_wk_… + Origin
↓ thereafter: HttpOnly cookie + X-CSRF-Token
Website channel (Surogates API)
↓ /v1/website/{sessions,messages,events,end}
Harness + model (Surogates worker)
↓ Turn 1: /preflight <brief> → verdict + slot questions
↓ Turn 2: /invergent <refined> → full plan
↓ events stream via SSE → AG-UI → Refine panel / Wizard
Two turns ride on the same WebsiteAgent instance (one bootstrap, one cookie, one CSRF token; the SDK tracks the SSE cursor across runs). The surg_wk_… key ships in the bundle but is origin-bound — stolen and used from another origin → 403. Cost is bounded by session_message_cap + the platform's idle policy + an edge rate limit.
Key files:
| File | Role |
|---|---|
src/agent/heroAgentClient.ts |
WebsiteAgent wrapper — preflight() + plan() over one session, fenced-JSONL parser, paced replay |
src/agent/AgentProvider.tsx |
React context: phase (Brief/Refine/Plan), run state, refine slots+answers, transcript, plan, lifecycle |
src/components/Hero/Composer.tsx |
Token-pill content-editable; CTA fires /preflight; phase pills double as the flow stepper |
src/components/Refine/ |
RefinePanel + SlotChips + Transcript — between Brief and Plan |
src/components/Wizard/Wizard.tsx |
Plan panel; gated on phase === 'planning' || 'done'; progressive card paint; PDF download |
src/lib/pdfPlan.tsx |
@react-pdf/renderer rendering of the same PlanState (no screen/PDF drift) |
src/embed.tsx |
Library entry — mount(target, opts) → shadow root + Tailwind + React tree |
src/demo/cannedRun.ts |
Canned event streams for /preflight (chips / sufficient / nonsense) and /invergent |
The client reads only the assistant turn (agent.messages after RUN_FINISHED) and extracts a fenced ```jsonl ``` block per turn, then re-paces the events on local timers so the panels paint progressively. The /preflight skill is expected to return one of three verdicts: chips (2–4 slot questions), sufficient (brief is detailed enough — auto-advance to plan), or nonsense (polite gate back to Brief). If the deployed agent doesn't produce a fenced block, the client synthesises a soft nonsense verdict so the user gets a retry path instead of a raw error.
Two URLs are in play and they do different jobs:
| URL | Serves | Who hosts it |
|---|---|---|
The agent URL (hero.invergent.ai) |
The chat API: bootstrap, messages, SSE | Surogates (Helm release in surogate-ops) |
The bundle URL (your <script src>) |
The compiled invergent-hero.umd.js file |
jsDelivr — served straight off this repo's hosted/ folder |
The repo is the CDN. Push to main and jsDelivr serves the new bundle within seconds. No Webflow Assets dance, no manual uploads.
npm run build:lib
cp -r dist/lib/. hosted/
git add hosted/ && git commit -m "build: bump bundle"
git pushhosted/ is git-tracked on purpose: jsDelivr reads it directly from GitHub. The folder contains the UMD bundle, the ES variant (unused by Webflow but kept for bundler consumers), and fonts/*.ttf (resolved at runtime as siblings of the script URL).
Always-latest — rebuilds on every push to main:
https://cdn.jsdelivr.net/gh/invergent-ai/website-hero-agent@main/hosted/invergent-hero.umd.js
Pinned to a specific commit (recommended for production — Webflow won't pick up rebuilds until you change the URL):
https://cdn.jsdelivr.net/gh/invergent-ai/website-hero-agent@<commit-sha>/hosted/invergent-hero.umd.js
Or pinned to a tag:
git tag v0.2.0 && git push --follow-tagshttps://cdn.jsdelivr.net/gh/invergent-ai/website-hero-agent@0.2.0/hosted/invergent-hero.umd.js
jsDelivr strips the leading
vfrom tag names in URLs. Tag isv0.2.0locally, but the URL uses@0.2.0. New tags take ~10 min for jsDelivr's metadata cache to refresh — until then use the SHA form.
Page → Page Settings → Custom Code → Before </body>:
<div id="invergent-hero"></div>
<script src="https://cdn.jsdelivr.net/gh/invergent-ai/website-hero-agent@main/hosted/invergent-hero.umd.js"></script>
<script>
InvergentHero.mount('#invergent-hero', {
apiUrl: 'https://hero-agent.cloud.surogate.ai',
publishableKey: 'surg_wk_…',
});
</script>Publish.
| Option | Type | Notes |
|---|---|---|
apiUrl |
string |
The agent's URL. Absolute; relative URLs trigger a console.warn. Must be publicly resolvable HTTPS for cross-origin cookies. |
publishableKey |
string? |
surg_wk_… from Studio. Required for the live transport. |
demo |
boolean? |
Force the canned demo even with a key set. |
Returns { unmount() }. window.InvergentHero.version exposes the loaded build for DevTools sanity-checks.
The mount attaches an isolated shadow root and a HeroErrorBoundary so a runtime exception never blanks the host page. Re-calling mount() on the same host (Webflow soft-nav) recycles the existing shadow root instead of throwing. Polyfills for crypto.randomUUID + structuredClone ship in the bundle so Safari < 15.4 doesn't crash.
npm run build:lib
mkdir -p public/lib && cp -r dist/lib/* public/lib/
cp tests/embed-host.html public/embed-test.html # hostile-CSS isolation test
cp tests/invergent-host.html public/invergent-host.html # invergent.ai-style mock
npm run dev
# http://127.0.0.1:5175/invergent-host.html?demo=1Swap ?demo=1 for ?key=surg_wk_… once an agent is provisioned.
When you push a new bundle and Webflow keeps serving the old one, force-refresh jsDelivr:
https://purge.jsdelivr.net/gh/invergent-ai/website-hero-agent@main/hosted/invergent-hero.umd.js
Open the URL in a browser; jsDelivr replies with a JSON confirmation.
If jsDelivr doesn't fit (rate limits, custom domain, aggressive cache headers, private bundle), upload hosted/ to Cloudflare R2, AWS S3, your own nginx, or the Garage cluster already running inside surogates. Change the <script src> URL — nothing else.
The repo must stay public for jsDelivr's
gh/shorthand to work. If you flip it private, switch to GitHub Pages (org-plan only) or one of the alternative hosts above.
The website channel is configured per-agent through Studio (surogate-ops → Agents → Channels → Website). There's no DB row, no Python recipe, no kubectl: Studio writes SUROGATES_WEBSITE_* into the agent's env_vars JSON column and the agent monitor rolls the deployment.
To provision a new agent:
- Studio → Agents → pick the agent → Channels tab → Website.
- Toggle ON. Studio mints a
surg_wk_…key client-side. - Add allowed origins (exact-match, no wildcards) — one per hosting environment, e.g.
https://invergent.ai,http://localhost:5175. - Optional:
session_message_cap(0 = no cap). - Save.
- Note the publishable key + the agent's
apiUrl— plug them into the Webflow snippet.
| Operation | Where |
|---|---|
| Rotate key | Studio → Rotate → Save. New key works once the rollout finishes; old key 401s from the same boundary. |
| Update origins / cap | Studio → edit field → Save. Rollout propagates. |
| Kill switch | Studio → toggle OFF → Save. After rollout every /v1/website/* request returns 404 (including in-flight sessions). |
WebsiteSettings exposes only four fields — anything not on this list is configured at the agent level, not the channel:
| Field | Studio name | Purpose |
|---|---|---|
enabled |
Channel toggle | When false, every /v1/website/* returns 404. |
publishable_key |
Publishable Key | surg_wk_… bearer at bootstrap. Constant-time compared; no hashing — config is the secret store. |
allowed_origins |
Allowed Origins | Exact-match list (scheme + host + port). Wildcards not supported. |
session_message_cap |
Session Message Cap | Per-session ceiling. 0 = no cap. Frozen onto session.config at bootstrap. |
| Symptom | Likely cause | Fix |
|---|---|---|
| Bootstrap → 401 | Wrong / rotated key | Re-fetch from Studio. |
| Bootstrap → 403 | Origin not in allow-list | Add the exact scheme://host[:port] in Studio. |
| Bootstrap → 404 | Channel disabled, rollout pending, or wrong apiUrl |
Check Studio toggle; wait for rollout; verify apiUrl. |
| Refine step keeps showing "I couldn't read that response" | /preflight skill not deployed on the agent |
Until the dedicated /preflight per-agent skill ships on the surogate side, this is the expected graceful-degradation path. Use demo mode in the meantime. |
| Wizard never paints after MAKE MY PLAN | Agent returned prose without a fenced JSONL block (or /invergent skill not deployed) |
Expected until both skills are provisioned. Inspect agent.messages in DevTools via window.__invergentDebug. |
http://127.0.0.1:5175andhttp://localhost:5175are different origins. Add both if you flip between them.
Wire-protocol reference: docs/channels/website.md in the Surogates repo.
src/
agent/
AgentProvider.tsx React context: phase, run state, refine slots+answers, transcript, plan, mascot mood.
heroAgentClient.ts WebsiteAgent wrapper — preflight() + plan() over one session, JSONL parser, paced replay.
thinkingPhrases.ts Phrase pool for the rotating mascot label (planning phase only).
types.ts Phase / SlotQuestion / PreflightEvent / PlanState shapes.
components/
Hero/ Composer + AttachChip token pills. CTA fires /preflight.
Mascot/ Cursor-following orb + thinking line (desktop-only). Muted during Refine.
Chips/ Example intents. Hidden mid-flow.
Refine/ RefinePanel + SlotChips + Transcript — between Brief and Plan.
ProductCards/ Brand cards + chaptered Product Story. Hidden mid-flow.
Wizard/ Plan panel + PDF download + book-demo dialog.
embed/
PortalContainer.tsx Re-targets Radix portals into the shadow root.
RuntimeConfig.tsx apiUrl / publishableKey / forceDemo via context.
ErrorBoundary.tsx Graceful fallback if the React tree throws.
assetsBase.ts Captures bundle URL at boot (for sibling fonts).
polyfills.ts crypto.randomUUID + structuredClone (Safari < 15.4).
demo/cannedRun.ts Canned events. ?demo=1 = chips. ?demo=sufficient / ?demo=nonsense pick preflight variants.
lib/pdfPlan.tsx @react-pdf/renderer rendering of PlanState.
styles/tokens.css Brand tokens + Tailwind preflight (:host + :root).
App.tsx Root layout.
main.tsx SPA entry.
embed.tsx Library entry (mount/unmount, shadow root).
tests/
embed-host.html Hostile-CSS Webflow-like harness (isolation test).
invergent-host.html invergent.ai-style mock (visual context).
public/fonts/ TTFs shipped sibling to the lib bundle.
hosted/ Git-tracked build output served by jsDelivr.
Refresh via `cp -r dist/lib/. hosted/` after build:lib.
vite.config.ts SPA dev + production config (/agent proxy).
vite.lib.config.ts Library bundle config (UMD + ES).
Proprietary. © 2026 Invergent SA.