# Visitor widget

The stock chat bubble. One script tag, no build step, no CSS to import.

```html
<script src="https://<host>/widget.js" data-tenant="<tenantId>" defer></script>
```

Requirements:
1. The page's origin (e.g. `https://www.example.no`) is listed in the tenant's allowed origins
   (admin → Innstillinger → *Nettsteder som kan vise chatten*, or `PATCH /api/v1/operator/tenant`
   `{ allowedOrigins }`). `http://localhost:*` works outside production.
2. That's it. Name, portrait, welcome text, suggestions and team come from the tenant settings.

The widget renders inside a shadow root, so your CSS can't break it and its CSS can't leak into
your page.

## Options (`data-*` attributes)

| Attribute | Default | Meaning |
| --- | --- | --- |
| `data-tenant` | required | Tenant id. (`data-kunde` is an accepted alias.) |
| `data-position` | `right` | `left` puts the launcher bottom-left. |
| `data-open` | `false` | `true` opens the chat on load. |
| `data-language` | browser language | Force the reader language (`no`, `en`, …). |
| `data-accent` | tenant brand colour | Accent colour override, `#rrggbb`. |
| `data-user-name`, `data-user-email`, `data-user-phone`, `data-user-company` | | Who the visitor is, if your page knows (e.g. rendered by your server for a logged-in user). Same as `Snakkbar.setUser(...)`, see below. |

## JavaScript API

```js
Snakkbar.open();
Snakkbar.close();
Snakkbar.toggle();

// Switch to an identified visitor (token minted by your backend, see below).
Snakkbar.identify(async () => (await fetch("/api/snakkbar-token")).text());

// Tell Snakkbar who the visitor is (all fields optional; calls are merged).
Snakkbar.setUser({
  name: "Kari Nordmann",
  email: "kari@example.no",
  phone: "+47 900 00 000",
  company: "Nordmann AS",
  attributes: { customerNo: 1234, plan: "pro" }, // your own fields; null removes one
});

// Events: "open", "close", "message" (a stored Message), "unread".
const off = Snakkbar.on("message", (message) => console.log(message.role, message.body));
off();

Snakkbar.destroy(); // remove the widget completely
```

Mounting by hand (SPAs that load the script without `data-tenant`, or several tenants on one page):

```js
// after <script src="https://<host>/widget.js"> has loaded
const widget = window.Snakkbar.mountWidget({
  host: "https://<host>",
  tenantId: "<tenantId>",
  position: "right",
  user: { name: "Kari Nordmann", email: "kari@example.no" }, // optional, same shape as setUser
});
widget.setUser({ company: "Nordmann AS" });
```

A launcher badge shows unread assistant/staff messages while the chat is closed.

## Telling staff who the visitor is

`Snakkbar.setUser({ name, email, phone, company, attributes })` (also `WidgetController.setUser`,
`MountWidgetOptions.user` and the `data-user-*` attributes) sends what your page knows about the
visitor to Snakkbar (`PATCH /api/v1/visitor/me`). Staff see it on the visitor and their
conversations, the assistant doesn't ask for it again, and the callback form is filled in from it.

- Call it after the widget script has loaded, whenever you learn something (e.g. after login).
  Calls made before the widget has mounted are queued; calls are merged.
- After `identify(token)` it is applied again to the identified visitor. `identify(null)`
  (logout) forgets it, so the next anonymous visitor doesn't inherit it.
- Field rules: [api.md](api.md#patch-visitorme--visitorprofile). Invalid values are rejected by the
  API; the widget only logs the error to the console and never throws.
- It is **unverified**: anyone can type anything into a browser. For an identified visitor it only
  fills fields that are still empty and adds attribute keys that don't exist yet; trusted values belong in your backend's token request
  (`POST /server/visitor-tokens` takes `name`, `email`, `phone`, `company`, `attributes`).

## Live visitors

On every page it is on, the widget reports the visitor as a **live visitor**, also before (and
without) a chat: staff see who is on the site now, the current page, the navigation history,
device, rough location and where the session came from, in the admin's «Besøkende» and «Oversikt».
An operator can open «Se live» to follow the visitor's cursor, clicks and scroll over the page.
This is the SDK's `startTracking()`; details in [realtime.md](realtime.md#live-visitors), and what
is and isn't collected in [security.md](security.md#live-visitors).

The widget does nothing extra for this: no setup, and no visible change for the visitor. Inside an
operator's live view (an iframe named `snakkbar-mirror`) the widget renders nothing and only keeps
the page scrolled to where the visitor is.

**Switch.** Live visitors are on by default. An admin turns them off per tenant under Innstillinger
(or `PATCH /api/v1/operator/tenant` `{ "liveVisitors": false }`). Off, the widget sends nothing and
creates no visitor until someone opens the chat; staff then only see visitors who chat.

**Consent.** Live visitors track behaviour on the site (pages, and the cursor while watched) and
store a visitor token in `localStorage` for every browser that opens a page. In many
jurisdictions (e.g. the EU/EEA under GDPR and the ePrivacy rules) that needs the visitor's consent
unless your site has another legal basis. If your site needs consent for this kind of tracking,
do one of:

- **No consent mechanism, or you don't want the tracking:** turn `liveVisitors` off. The chat
  works as before; nothing is sent until the visitor opens it.
- **Consent banner:** load the widget only after the visitor has consented (statistics/analytics
  category, or whatever your banner calls it), and mention Snakkbar in your privacy notice:

  ```js
  // e.g. in your consent manager's "accepted" callback
  function loadSnakkbar() {
    if (window.Snakkbar?.open) return;
    const s = document.createElement("script");
    s.src = "https://<host>/widget.js";
    s.dataset.tenant = "<tenantId>";
    document.body.appendChild(s);
  }
  ```

  Visitors without consent then have no chat either. If they should still be able to chat, turn
  `liveVisitors` off instead: the switch is per tenant, not per visitor.

Whether consent is required is the site owner's call; Snakkbar gives you the switch and keeps the
data minimal.

## Identified visitors

By default every browser is an anonymous visitor. If your site has logged-in users, let the chat
follow them across devices and show staff who they are:

```ts
// Your backend (never expose the secret key in the browser)
const res = await fetch("https://<host>/api/v1/server/visitor-tokens", {
  method: "POST",
  headers: { authorization: `Bearer ${process.env.SNAKKBAR_SECRET_KEY}`, "content-type": "application/json" },
  body: JSON.stringify({ externalId: user.id, name: user.name, email: user.email, phone: user.phone, company: user.company }),
});
const { token } = await res.json(); // valid 24 h; send it to the browser
```

Then `Snakkbar.identify(token)` (or a function returning a fresh token). Call `identify(null)` on
logout to return to an anonymous visitor.

## Look and behaviour

- **Colours** come from your page: shadcn/ui CSS variables on `<html>` if present, otherwise the
  page's background and text colours, with the tenant's brand colour (or `data-accent`) as accent.
  Light and dark pages are handled, and the widget follows live theme switches.
- **Launcher**: shows the face of a staff member who is online (with a green dot), otherwise the
  assistant's portrait.
- **Desktop**: a 440×720 popup in the corner. Visitors can resize it by dragging the top edge, the
  side edge or the corner; the size is remembered, and double-clicking a handle resets it.
- **Mobile** (≤700 px): a full-screen sheet with a grab bar; keyboard-aware.
- **Chats**: the ☰ menu lists the visitor's earlier conversations and starts a new one.
- **People**: the team row and `@name` reach staff; the assistant hands over when asked.
- **Images**: up to 4 per message (JPEG, PNG, WebP, GIF; 5 MB each).
- **Translation**: messages in other languages show translated, with «Vis original».
- **Navigation**: staff can move the visitor to another page on the same site.
- Respects `prefers-reduced-motion`. Escape closes.

## Content Security Policy

If your site sends a CSP, allow:

```
script-src https://<host>;
connect-src https://<host>;
img-src https://<host> https://*.supabase.co data: blob:;
style-src 'unsafe-inline';
```

For the operators' live view («Se live») your pages must also let the Snakkbar admin frame them:
`frame-ancestors 'self' https://<host>` (plus the origin of your own admin if you embed it), and no
`X-Frame-Options` header that excludes those origins. Without it the live view shows a schematic
page instead. See [admin-embed.md](admin-embed.md#build-your-own-live-view).
