# Lexi Bolt-On Integration Guide

This document is for engineers bolting Lexi onto an LC app. Two host patterns are supported in Sprint 2.2:

1. **Inline panel** (Comp Intelligence-style) — Lexi lives in a sidebar or pane inside an app
2. **Floating widget** (public website-style) — Lexi pinned to bottom-right of the page

Both patterns use the same two-file embed: `lexi-embed.js` (engine) + `lexi-widget.js` (default UI). Apps that want a custom UI use only `lexi-embed.js` and call `Lexi.api().ask()` directly.

## Prerequisites

Three things must be reachable from the host page:

| Resource | Path | Notes |
|---|---|---|
| Lexi runtime | `/lexi/*.js` | 13 files; serve from the host's static origin |
| Embed | `/embed/lexi-embed.js`, `/embed/lexi-widget.js`, `/embed/lexi.css` | 3 files; same origin |
| Worker URL | `https://lexi-api-sandbox.<account>.workers.dev` | cross-origin; CORS allowlist must include the host origin |

For Sprint 2.2 sandbox, the Pages site at `lexi-platform-sandbox.pages.dev` serves both `/lexi/*` and `/embed/*` and points at the deployed sandbox Worker.

## Pattern 1: Inline panel (Comp Intelligence)

```html
<!-- 1. Include the stylesheet -->
<link rel="stylesheet" href="/embed/lexi.css">

<!-- 2. Put the synthetic comp snapshot on window BEFORE embed loads.
        The comp-intelligence-context.stub.js builder reads from
        window.CompIntelSnapshot. -->
<script>
window.CompIntelSnapshot = {
  period: 'Q2 2026 (SYNTHETIC)',
  totalWrvus: 304611,
  totalCompPaid: 16200000,
  avgWrvuPerProvider: 2160,
  providerCount: 141,
  stipendSpend: 0,
  fmvExceptions: 0,
  compModelMix: { hybrid: 0.45, salaryPlusComp: 0.30, wrvuOnly: 0.25 },
  specialties: [
    { id: 'syn-fm',    label: 'Family Medicine',  n: 22, medianWrvu: 4200, mgmaPct: 48 },
    { id: 'syn-cards', label: 'Cardiology',       n: 14, medianWrvu: 5800, mgmaPct: 62 },
    { id: 'syn-ortho', label: 'Orthopedics',      n: 11, medianWrvu: 7100, mgmaPct: 71 }
  ]
};

// Tell the runtime about environment and deployment BEFORE embed loads
window.LEXI_ENVIRONMENT = 'sandbox';
window.LEXI_DEPLOYMENT  = 'comp-intelligence';
</script>

<!-- 3. Empty target div where the widget will mount -->
<div id="lexiPanel"></div>

<!-- 4. Load embed engine + default widget -->
<script src="/embed/lexi-embed.js"></script>
<script src="/embed/lexi-widget.js"></script>

<!-- 5. Initialize and mount -->
<script>
(async function() {
  // Worker URL: query string override, then default. In production,
  // the host bakes the URL in directly.
  const params = new URLSearchParams(window.location.search);
  const workerUrl = params.get('worker') ||
                    'https://lexi-api-sandbox.<account>.workers.dev';

  await Lexi.init({
    workerUrl,
    deployment: 'comp-intelligence',
    environment: 'sandbox',
    jwt: () => null,        // sandbox bypass; production supplies real Supabase JWT
    basePath: '/lexi'
  });

  Lexi.mountWidget({
    target: '#lexiPanel',
    title: 'Ask Lexi about Comp Intelligence',
    placeholder: 'Ask about wRVUs, specialties, FMV...',
    testMode: true,
    suggestedPrompts: [
      'What was total wRVU output this quarter?',
      'Which specialty has the highest median wRVU?',
      'How many FMV exceptions are there?',
      'Compare cardiology to orthopedics on wRVU output.'
    ]
  });
})();
</script>
```

A `<div id="lexiPanel">` of `width: 380px; height: 100%;` fits in a sidebar. The widget uses `min-height: 320px; max-height: 70vh` by default and adapts to container size.

## Pattern 2: Floating widget (public site)

```html
<link rel="stylesheet" href="/embed/lexi.css">

<script>
  window.LEXI_ENVIRONMENT = 'sandbox';
  window.LEXI_DEPLOYMENT  = 'lexingtonclinic-public';
</script>

<!-- Floating widget pinned to bottom-right of the viewport -->
<style>
  #lexiPanel {
    position: fixed; bottom: 24px; right: 24px;
    width: 380px; max-width: calc(100vw - 32px);
    z-index: 50; box-shadow: 0 10px 30px rgba(0,0,0,0.15);
    border-radius: 12px;
  }
  @media (max-width: 700px) {
    #lexiPanel { right: 8px; left: 8px; width: auto; }
  }
</style>
<div id="lexiPanel"></div>

<script src="/embed/lexi-embed.js"></script>
<script src="/embed/lexi-widget.js"></script>

<script>
(async function() {
  const params = new URLSearchParams(window.location.search);
  const workerUrl = params.get('worker') ||
                    'https://lexi-api-sandbox.<account>.workers.dev';

  await Lexi.init({
    workerUrl,
    deployment: 'lexingtonclinic-public',
    environment: 'sandbox',
    jwt: () => null,
    basePath: '/lexi'
  });

  Lexi.mountWidget({
    target: '#lexiPanel',
    title: 'Ask LC',
    placeholder: 'How can we help?',
    testMode: true,
    suggestedPrompts: [
      'How do I schedule an appointment?',
      'What insurance plans do you accept?',
      'How do I contact Lexington Clinic?',
      'Where are locations listed?'
    ]
  });
})();
</script>
```

## Pattern 3: Custom UI (engine only)

Apps that want their own UI skip `lexi-widget.js` and `lexi.css` entirely:

```html
<script src="/embed/lexi-embed.js"></script>
<script>
(async function() {
  const api = await Lexi.init({
    workerUrl: 'https://lexi-api-sandbox.<account>.workers.dev',
    deployment: 'comp-intelligence',
    environment: 'sandbox',
    jwt: () => null,
    basePath: '/lexi'
  });

  // Build your own UI; call api.ask() to send questions.
  const myInput = document.getElementById('myCustomInput');
  const myOutput = document.getElementById('myCustomOutput');
  myInput.addEventListener('submit', async (e) => {
    e.preventDefault();
    try {
      const r = await api.ask(myInput.value);
      myOutput.textContent = r.text;
      console.log('context source:', r.contextSource);   // e.g. 'comp-intelligence-context.stub'
      console.log('adapter:',         r.adapterId);      // 'mock' in sandbox
      console.log('audit id:',        r.auditId);
    } catch (e) {
      myOutput.textContent = 'Error: ' + e.code;
    }
  });
})();
</script>
```

## Response shape

Every successful `api.ask()` resolves to:

```js
{
  text: 'string',                      // The assistant's response
  model: 'mock-1',                     // Model used (or 'foundry-default' in Sprint 2.3+)
  usage: { inputTokens, outputTokens },
  adapterId: 'mock',                   // Which adapter answered
  deploymentId: 'comp-intelligence',
  contextSource: 'comp-intelligence-context.stub',  // Which context builder
  auditId: 'uuid',                     // For correlating to Worker logs
  latencyMs: 12
}
```

`contextSource` is the source-of-truth field for the widget's "from:" chip and for any host-side audit display. It identifies which browser-side context builder shaped the systemPrompt that the adapter answered from.

## Error handling

`api.ask()` rejects with an `Error` whose `.code` matches the Worker's stable error codes. The default widget renders friendly messages for known codes; custom UIs can pattern-match on `.code` to render their own:

| code | meaning |
|---|---|
| `WORKER_FETCH_FAILED` | network or DNS error |
| `AUTH_HEADER_MISSING` | sandbox bypass disabled and no JWT supplied |
| `SUPABASE_JWT_EXPIRED` | JWT expired; refresh session |
| `BAA_REQUIRED` | classification requires a BAA-covered adapter |
| `PROMPT_TOO_LARGE` | system prompt + history + question exceeds 32,000 chars |
| `HISTORY_TOO_LONG` | conversation has more than 40 turns |
| `BOOT_BLOCKED` | runtime startup check failed |
| `WORKER_BOOT_FAILED` | Worker boot check failed |
| `UNKNOWN_DEPLOYMENT` | deploymentId not in the registry |
| `ADAPTER_NOT_ALLOWED` | adapter not in the deployment's allowedAdapters |
| `CORS_ORIGIN_REJECTED` | host origin not in the Worker's ALLOWED_ORIGINS |

## Diagnostics

The widget header shows the environment + deployment as a chip. The diagnostics footer (toggle with `showDiagnostics: false`) shows the active Worker URL. Each assistant message has:

- A "from: &lt;contextSource&gt;" green chip in the header
- A "Copy" button to copy the response text
- A "Retry" button (shown only if the previous attempt errored)
- A meta line: `adapter=mock · 12ms · audit=a4f3b2c1`

The header has a "Clear" button (Cmd/Ctrl+K) to reset the conversation.

## What's still ahead (not required for bolt-on)

Sprint 2.2 ships the test platform. The bolt-on contract is stable for these future changes:

- **Real Foundry adapter** (Sprint 2.3+): no host change needed. The Worker swaps adapters, the embed and host code are untouched.
- **Real Supabase auth** (production): the host changes `jwt: () => null` to `jwt: () => mySupabaseSession.access_token`. Nothing else.
- **Admin-managed FAQ corpus** (Sprint 3): the `public-faq-context` builder fetches the corpus instead of inlining it. Host code unchanged.
- **Real Comp Intel data feed** (Sprint 2.x or 3): the host sets `window.CompIntelSnapshot` from its real data source instead of hardcoding. Builder unchanged.
