# Vocantly Widget

A **drop-in embeddable support chat widget** built on the headless `@vocantly/sdk`. One
`<script>` renders a floating chat bubble + panel; your visitors chat with the AI bot (and human
agents) with zero UI work on your side.

`dist/vocantly-widget.js` is a single self-contained file (~63 KB) — the SDK, its deps, and the
UI are all bundled in. No import maps, no build step for the host.

## The security model (important)

The widget runs in the browser, so it only ever holds an **SDK token** (scoped to one app + one
visitor, short-lived). Your **API key + secret never touch the browser.** The flow:

```
Your backend (holds API key)                     Browser (this widget)
  POST /apps/:id/support-sessions   ── token + conversation_id ──▶  VocantlyWidget.init({...})
  (X-Api-Key + X-Api-Secret)
```

Your server calls the provisioning endpoint, then hands only `{ token, conversation_id }` to the page.

## Install

**Option A — script tag (any website, via CDN):**

```html
<!-- unpkg / jsDelivr serve the bundled IIFE; global: VocantlyWidget -->
<script src="https://unpkg.com/@vocantly/widget"></script>
<!-- pin a version:  https://unpkg.com/@vocantly/widget@0.1.5 -->
```

**Option B — npm (React / Vue / Next / bundlers):**

```bash
npm install @vocantly/widget
```
```js
import { init } from '@vocantly/widget';
init({ wsUrl, token, conversationId, /* … */ });
```

Both ship the SDK + deps bundled in — zero peer dependencies. Usage is the same either way:

```html
<script src="https://unpkg.com/@vocantly/widget"></script>
<script>
  VocantlyWidget.init({
    wsUrl: 'wss://your-realtime/ws',
    token: 'SDK_TOKEN_FROM_YOUR_BACKEND',
    conversationId: 'CONVERSATION_ID_FROM_YOUR_BACKEND',
    apiUrl: 'https://your-backend/api/v1',   // optional — enables message history
    title: 'Support',
    subtitle: 'We reply in a few minutes',
    primaryColor: '#195EFF',
    position: 'bottom-right',                // or 'bottom-left'
    greeting: 'Hi! 👋 How can we help?',
    autoOpen: false,

    // Optional pre-chat contact capture (needs apiUrl):
    collectContact: true,                    // ask name + email before chatting
    requireEmail: false,                     // if true, email is mandatory to start
    contactPrompt: 'Before we start — how can we reach you?',
  });
</script>
```

### Your backend (one call to provision)

```js
// server-side — keep the secret here
const r = await fetch(`${BASE}/apps/${APP_ID}/support-sessions`, {
  method: 'POST',
  headers: { 'X-Api-Key': PUBLIC_KEY, 'X-Api-Secret': SECRET_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ external_user_id: yourUserId, name, email }),
});
const { token, conversation_id } = (await r.json()).data;   // hand these to the browser
```

## No backend? Anonymous embed with just your public key

For a static landing page with **unauthenticated visitors and no server**, use `publicKey` instead of
`token`/`conversationId`. The widget self-provisions an anonymous session — no secret in the browser:

```html
<script src="https://unpkg.com/@vocantly/widget"></script>
<script>
  VocantlyWidget.init({
    publicKey: 'pk_…',                         // your app PUBLIC key (safe to expose)
    wsUrl:  'wss://ws.vocantly.com/ws',
    apiUrl: 'https://api.vocantly.com/api/v1',  // required in this mode
    collectContact: true,
  });
</script>
```

**One prerequisite — allow your site's origin.** Anonymous embedding is off by default. Add your
domain(s) to the app's `allowed_origins` (dashboard, or via the API):

```bash
PATCH /apps/:id   { "allowed_origins": ["https://acme.com", "https://www.acme.com"] }
```

The widget generates a **stable visitor id** (localStorage) and caches `{token, conversationId}` so a
reload resumes the same conversation. Requests are origin-checked server-side and rate-limited.

> Provide **either** `publicKey` (anonymous) **or** `token` + `conversationId` (backend-provisioned).

## API

| Call | Effect |
|---|---|
| `VocantlyWidget.init(opts)` | Mount + connect. Returns `{ open, close, send, destroy }`. |
| `VocantlyWidget.open()` / `.close()` | Toggle the panel. |
| `VocantlyWidget.destroy()` | Disconnect + remove from the DOM. |

### Pre-chat contact capture

With `collectContact: true`, the widget asks the visitor for their name + (optional) email before
chatting and saves it to their customer profile via `PATCH /sdk/customers/me` (authenticated by the
SDK token — no secrets in the browser). Agents then see the visitor's contact details in the inbox.
Requires `apiUrl` to be set. Use `requireEmail: true` to make email mandatory.

## Build

```bash
npm run build       # → dist/vocantly-widget.js (minified IIFE, global VocantlyWidget)
npm run build:dev   # non-minified + sourcemap
```
The build bundles `../vocantly-js-sdk/dist/index.js`. If you rebuild the SDK, rebuild the widget.

## Demo

```bash
npm run serve:demo   # serves this folder on :5173
# open http://localhost:5173/   (redirects to the demo)
```
> Avoid ports Chrome blocks as "unsafe" (e.g. 5060 = SIP → `ERR_UNSAFE_PORT`). 5173 is safe.
The demo is a mock business page. Enter a workspace owner's email/password + a **Development** App ID
and click **Boot** — it provisions via `/apps/:id/support-sessions` (dev login stands in for your
backend) and mounts the widget. Requires backend `:3001`, Go realtime `:8080`, and the app to have the
**bot enabled + an active FAQ**.

## Known limitation

Bot replies are currently delivered live but **not persisted** server-side, so they won't appear in
message history on reload (customer/agent messages do). Persisting bot messages is a backend follow-up.
