Skip to content

Repository files navigation

Invergent Hero Agent

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.


Quick start

npm install
npm run dev          # http://127.0.0.1:5175/?demo=1

Without 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.


How it works

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.


Embed in Webflow

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.

1. Build & ship

npm run build:lib
cp -r dist/lib/. hosted/
git add hosted/ && git commit -m "build: bump bundle"
git push

hosted/ 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).

2. The bundle 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-tags
https://cdn.jsdelivr.net/gh/invergent-ai/website-hero-agent@0.2.0/hosted/invergent-hero.umd.js

jsDelivr strips the leading v from tag names in URLs. Tag is v0.2.0 locally, 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.

3. Wire it up in Webflow

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.

mount() options

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.

Smoke-test locally (no backend required)

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=1

Swap ?demo=1 for ?key=surg_wk_… once an agent is provisioned.

Cache invalidation

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.

Alternative hosts

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.


Operations

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:

  1. Studio → Agents → pick the agent → Channels tab → Website.
  2. Toggle ON. Studio mints a surg_wk_… key client-side.
  3. Add allowed origins (exact-match, no wildcards) — one per hosting environment, e.g. https://invergent.ai, http://localhost:5175.
  4. Optional: session_message_cap (0 = no cap).
  5. Save.
  6. 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.

Debugging

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:5175 and http://localhost:5175 are different origins. Add both if you flip between them.

Wire-protocol reference: docs/channels/website.md in the Surogates repo.


Project layout

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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages