# The chat widget

How a customer puts the widget on their site, and how to test it.

## The three pieces

```
customer's page          your dashboard              the widget app
  <script src=  ──────▶  coddeskai.com/embed.js  ──▶  widget.coddeskai.com
   embed.js>              (the loader)                 (the chat UI, in an iframe)
```

- **The loader** is generated per request by `apps/web/app/embed.js/route.ts`. It
  draws the launcher button and opens the widget in an iframe. It is not a file
  in the repo: the previous one was, and it went stale pointing at the Vercel
  app of the tutorial this project started from.
- **The widget app** is `apps/widget`, a separate Next.js deployment. The loader
  frames it with `?organizationId=…`.

## Deployment

| What | Where | Notes |
|---|---|---|
| `apps/web` | `coddeskai.com` | serves `/embed.js`; no extra config |
| `apps/widget` | `widget.coddeskai.com` | its own Vercel project, `apps/widget/vercel.json` |

`NEXT_PUBLIC_WIDGET_URL` on the **web** deployment tells the loader where the
widget lives. It defaults to `https://widget.coddeskai.com`; set it if you host
the widget elsewhere.

`NEXT_PUBLIC_EMBED_URL` is optional. Unset, the install snippet points at this
app's own `/embed.js`, which is what you want. Set it only to serve the loader
from a different origin, such as a CDN.

## What the customer pastes

Dashboard → **Widget Customization** → **Install Widget** → Copy. It looks like:

```html
<script
  src="https://coddeskai.com/embed.js"
  data-organization-id="org_..."
></script>
```

Before the closing `</body>` tag. Optional attributes:

| Attribute | Default | Purpose |
|---|---|---|
| `data-position` | `bottom-right` | or `bottom-left` |
| `data-accent-color` | `#6366f1` | launcher button colour |
| `data-widget-url` | the deployment default | point at a self-hosted widget |

The loader exposes `window.CoreDeskAI` with `show()`, `hide()`, `toggle()` and
`destroy()` if the site wants to drive it from its own button.

## Testing

`tools/widget-test/index.html` loads the widget exactly as a customer's site
would.

```bash
npx serve tools/widget-test -l 4173
# open http://localhost:4173
```

Paste the organization id, press **Load widget**, and the launcher appears
bottom-right.

**Serve it over http, do not open the file directly.** A `file://` page sends no
referrer, and the allowed-domains check has nothing to test against.

To test against a local dashboard, set the loader URL to
`http://localhost:3100/embed.js` and run `apps/web` and `apps/widget`
(ports 3100 and 3001) with `NEXT_PUBLIC_WIDGET_URL=http://localhost:3001`.

## Allowed domains

**Widget Customization → Allowed domains**. Empty means any domain, which is
what every widget predating the feature has.

The check runs in `contactSessions.create`, on the first message of every widget
conversation. The host it tests comes from `document.referrer` — the page
embedding the iframe. It cannot come from the widget's own `window.location`,
which is always the widget host; reading that made a configured list reject
every visitor.

A bare `example.com` also admits its subdomains. `*.example.com` is the explicit
spelling of the same thing. Matching is on label boundaries, so
`notexample.com` never satisfies `example.com`.

When testing from `localhost`, either leave the list empty or add `localhost`.

## When it does not work

| Symptom | Cause |
|---|---|
| No launcher button | loader 404, or `data-organization-id` missing — check the console |
| Launcher appears, iframe blank | widget app not deployed; `widget.coddeskai.com` returns `DEPLOYMENT_NOT_FOUND` until it is |
| "This widget is not permitted on this domain" | the embedding page's host is not in Allowed domains |
| Widget loads, agent says nothing useful | not a widget problem — check the agent's model and tools |
