# Troubleshooting

> An install that does not show, connect or answer.

The widget isn't working as expected. Let's diagnose.

---

## Quick diagnostic checklist

Paste this into your browser console on the page where the widget
should be:

```js
// 1. Is the bundle loaded?
console.log('HelpAI:', typeof window.HelpAI);
// Expected: "function" (after bundle loads)

// 2. Is an instance mounted?
console.log('instance:', window.HelpAI?.getInstance?.());
// Expected: an object with open/close/expand methods, not null

// 3. Is the shadow host in the DOM?
console.log('host:', document.querySelector('helpai-chat-host, web-ai-chat'));
// Expected: a DOM element

// 4. What's the current data-mode?
const host = document.querySelector('helpai-chat-host, web-ai-chat');
console.log('data-mode:', host?.dataset.mode);
// Expected: "closed" / "open" / "expanded" / "fullscreen" / "inline" / "standalone"

// 5. What storage does the widget have?
for (const k of Object.keys(localStorage)) {
  if (k.startsWith('helpai.')) console.log(k, '=', localStorage.getItem(k));
}
```

---

## Widget doesn't appear

### Symptom: no bubble in the corner

1. **Check the console for errors** — most commonly a network error
   loading `cdn.help.ai/sdk/js/elements/latest/elements.js` (note the versioned
   path — the bare `cdn.help.ai/elements.js` root 404s). Verify the URL in
   DevTools → Network tab.

2. **Is the deployment identity complete?**

   ```js
   // In console:
   window.HelpAI?.getInstance?.();
   ```

   Returns `null` if no instance mounted. Likely causes:
   - Missing required `data-ai-agent-deployment-id` on the install script.
   - `init()` called without `aiAgentDeploymentId`. `publicKey` is optional only for intentional guest mode.
   - Typo in the key or deployment ID.

3. **Is the host element in the DOM?**

   ```js
   document.querySelector('helpai-chat-host');
   ```

   If null, the widget didn't mount. Check the console for init errors.

4. **Is the host hidden by host-page CSS?** — shouldn't happen (the
   widget uses `all: initial` to reset), but if you've got a very
   aggressive `* { display: none !important; }` style somewhere:

   ```js
   const host = document.querySelector('helpai-chat-host');
   console.log(getComputedStyle(host).display); // should be "block" or "inline-block"
   ```

5. **Is `launcher.hidden: true` set?** — that hides the FAB. Check
   your options.

6. **Is `mode` set to `inline` or `standalone`?** — those modes don't
   show a launcher. Check your options.

### Symptom: bubble shows but clicking does nothing

1. **Check the network tab** — clicking fires `/pai/handshake`.
   An opaque 404 means the credentials did not match an enabled deployment or
   the requesting origin was not allowed. 0 (blocked) means a CORS / ad-blocker
   issue.

2. **Are `x-public-key` and `x-ai-agent-deployment-id` making it through?** —
   some corporate proxies strip custom headers. Test on a personal network.

3. **Any CSP errors in the console?** — `"Refused to connect to
   https://help.ai/api/agent because it violates the following Content
   Security Policy…"` means you need to add `help.ai` to your
   `connect-src`. See [Security § CSP](/sdk/security-privacy#content-security-policy).

---

## Widget appears but doesn't behave right

### Symptom: "Failed to fetch" errors on open

Network issue. Common causes:

- **Ad blocker** — uBlock Origin / AdBlock Plus sometimes block
  `help.ai` API calls as tracking. Whitelist it.
- **Corporate proxy** — some proxies block `text/event-stream`
  (streaming responses). Test on a different network.
- **CSP** — your Content Security Policy is missing
  `help.ai` in `connect-src`.

### Symptom: panel appears in the wrong spot on mobile

Only in `floating` mode. The widget uses `position: fixed` anchored
to `presentation.position` (`bottom-right` default). If mobile viewport
units are behaving oddly (iOS Safari, looking at you):

1. Ensure you have a viewport meta tag:

   ```html
   <meta name="viewport" content="width=device-width, initial-scale=1" />
   ```

2. Avoid having `overflow: hidden` on `<html>` or `<body>` — this
   clips fixed positioning on some browsers.

3. If still stuck, try `presentation.mode: "inline"` inside a dedicated container
   on mobile breakpoints.

### Symptom: panel doesn't fill its container (inline mode)

The parent container needs explicit dimensions. Inline widgets fill
100% × 100% of their parent, which falls back to `min-height: 320px`
if the parent is 0-height.

```css
/* Parent needs a size */
.my-chat-container {
  height: 500px;
  /* or */
  display: flex;
  flex-direction: column;
  /* with widget set to flex: 1 */
}

.my-chat-container web-ai-chat {
  flex: 1 1 0;
  min-height: 0; /* critical for flex children */
}
```

### Symptom: two widgets on the same page overlap or conflict

Use distinct `widgetId` values:

```ts
init({ widgetId: 'support', publicKey: SUPPORT_KEY, aiAgentDeploymentId: SUPPORT_DEPLOYMENT_ID });
init({ widgetId: 'sales', publicKey: SALES_KEY, aiAgentDeploymentId: SALES_DEPLOYMENT_ID });
```

And position them in different corners:

```ts
init({ widgetId: 'support', presentation: { position: 'bottom-right' } });
init({ widgetId: 'sales', presentation: { position: 'bottom-left' } });
```

Without distinct widgetIds, the second `init()` **replaces** the
first.

---

## Styling issues

### Symptom: widget ignores my CSS

That's by design. The widget lives inside a shadow root — your page
CSS can't reach inside. Use the [theming options](/sdk/theming)
instead.

For advanced customization, override CSS custom properties on the
host element:

```css
web-ai-chat,
helpai-chat-host {
  --helpai-accent: #ff3e00;
  --helpai-radius: 16px;
  --helpai-font: "Söhne", system-ui, sans-serif;
}
```

These cascade through the shadow root.

### Symptom: the widget inherits my weird base font

Most likely the host page uses `* { font-family: X !important }`.
The widget's `all: initial` reset handles most cases, but extreme
overrides can leak.

Force the widget's font explicitly via option:

```ts
init({
  theme: {
    fontFamily: 'Inter, system-ui, sans-serif',
  },
});
```

### Symptom: launcher bubble is a different color than expected

Check:

1. `theme.accent` in your `init()` options.
2. The workspace default accent in your Help AI dashboard.
3. `prefers-color-scheme` — the accent might look different in dark
   mode vs. light.

To debug:

```js
const host = document.querySelector('helpai-chat-host');
console.log('data-theme:', host.dataset.theme); // auto / light / dark
console.log('computed accent:', getComputedStyle(host).getPropertyValue('--helpai-accent'));
```

---

## Persistence issues

### Symptom: chat history doesn't persist across reloads

1. **Is localStorage available?** Private/incognito modes may disable it.

   ```js
   try {
     localStorage.setItem('_test', '1');
     console.log('OK');
   } catch (e) {
     console.error('Blocked', e);
   }
   ```

2. **Check the widget's storage keys:**

   ```js
   for (const k of Object.keys(localStorage)) {
     if (k.startsWith('helpai.')) console.log(k);
   }
   ```

   You should see at least `helpai.default.visitorId` and (after a
   chat) `helpai.default.conversationId`.

3. **Is the `widgetId` consistent across reloads?** If you pass a
   dynamic `widgetId` (e.g., based on current time), each load gets
   a fresh namespace and history won't carry over.

### Symptom: user switches devices, chat history doesn't follow

That's the designed behavior for the current conversationId (per-device).
What DOES sync across devices:

- Locale preference.
- Theme mode.
- Sound mute toggle.
- Drag-resized panel dimensions.

These sync via the backend's `userPrefs` store, keyed by the widget's
own anonymous `visitorId`. Chat messages themselves are tied to that
`visitorId`, which is per-browser. `userContext` is informational
context for the AI only — it does not link chats across devices.

---

## React / framework issues

### Symptom: "window is not defined" during build

You're calling `init()` at module scope or during SSR. Move it
inside a lifecycle hook that only fires in the browser:

```tsx
// Wrong:
const chat = init({ publicKey, aiAgentDeploymentId }); // runs during SSR

export function App() { ... }

// Right:
export function App() {
  useEffect(() => {
    const chat = init({ publicKey, aiAgentDeploymentId });
    return () => chat.destroy();
  }, []);
}
```

### Symptom: widget mounts twice in React

React 18 strict mode intentionally double-invokes `useEffect` in
development. Your cleanup must be complete:

```tsx
useEffect(() => {
  const chat = init({ publicKey, aiAgentDeploymentId });
  return () => chat.destroy(); // ← this MUST unmount cleanly
}, []);
```

If you see "instance already exists at widgetId", your cleanup isn't
running. Check for missed `return () => …`.

### Symptom: HMR leaves stale widgets on the page

During Vite / Next.js HMR, the module re-runs without the React tree
being torn down. Symptoms: multiple launchers appear, old styles
stick around.

Workaround: use `init()` (idempotent) instead of `mount()`. `init()`
auto-replaces any existing instance at the same `widgetId`.

### Symptom: TypeScript complains about `<web-ai-chat>`

You're using the web component in React < 19. Add a type
declaration:

```ts
// src/web-ai-chat.d.ts
declare namespace JSX {
  interface IntrinsicElements {
    'web-ai-chat': React.DetailedHTMLProps<
      React.HTMLAttributes<HTMLElement> & {
        'public-key'?: string;
        mode?: 'floating' | 'inline' | 'standalone';
        theme?: 'auto' | 'light' | 'dark';
      },
      HTMLElement
    >;
  }
}
```

---

## Event / hook issues

### Symptom: `.on('message', ...)` never fires

1. **Is the instance really mounted?**

   ```js
   const chat = init({ publicKey: '...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000' });
   console.log(chat.widgetId); // should log the id
   ```

2. **Are messages being sent?** Open the Network tab, filter
   `stream-message`. A send should produce a POST.

3. **Is the listener attached BEFORE you send?** If you attach after
   the user sent a message, you miss that one.

### Symptom: pre-boot hooks never fire

1. **Is the bundle loading?** Check Network → `elements.js` →
   status 200.

2. **Is `window.HelpAIHooks` an array?**

   ```js
   Array.isArray(window.HelpAIHooks); // should be true
   ```

3. **Is the entry shape correct?**
   ```js
   // Tuple form:
   window.HelpAIHooks.push(['*', '*', (inst, e, d) => console.log(e)]);
   // Object form:
   window.HelpAIHooks.push({ widgetId: '*', event: '*', listener: fn });
   ```

---

## Performance issues

### Symptom: page feels slow when widget loads

1. **Are you using `defer` on the script tag?** Without `defer`, the
   script blocks HTML parsing.

2. **Check the Network tab** — the bundle should be ~107 KB gzipped (budget 108 KB)
   and load in well under a second on a good connection.

3. **Are you self-hosting an older version?** Pull the latest.
   Older versions were larger.

4. **Lighthouse or WebPageTest** — run a test. The widget is
   deferred and shouldn't affect LCP. If you see CLS spikes, the
   widget's host element might be affecting layout — ensure
   `position: fixed` is working (floating mode) or the inline
   container has a stable size.

### Symptom: streaming feels choppy

1. **Slow backend** — the widget just renders what it receives. Check
   the stream latency in Network → the `stream-message` entry.

2. **Network conditions** — SSE is sensitive to packet loss. Try a
   different network.

3. **Buffered response mode** — try `behavior.responseMode: "buffered"` if
   streaming is unusable on your users' networks. Tradeoff: they wait
   for the whole reply before seeing anything.

---

## CORS / CSP / networking

### Symptom: CORS errors in the console

Our API is CORS-permissive to every origin. If you're seeing CORS
errors:

- **Check the URL** — `https://help.ai` (the bare origin — not a
  relative path; the widget appends `/api/agent` + `/api/data` itself).
- **Check for browser extensions** blocking `help.ai`.
- **Check your reverse proxy** — some enterprise proxies strip CORS
  headers.

### Symptom: CSP blocks the widget

Add to your Content Security Policy:

```
script-src 'self' https://cdn.help.ai;
connect-src 'self' https://help.ai;
style-src 'self' 'unsafe-inline';            # Shadow DOM inline styles
img-src 'self' data: blob: https:;
font-src 'self' https:;
media-src 'self' blob:;                      # For voice recording
```

Full CSP guide: [Security § CSP](/sdk/security-privacy#content-security-policy).

---

## Widget-specific behavior questions

### "The widget opens to a stale chat thread — how do I reset?"

Click the trash icon (Clear action) in the panel → "Start new
conversation". This creates a fresh conversationId.

Programmatic:

```ts
// Clear the persisted conversationId + unmount, then remount
for (const k of Object.keys(localStorage)) {
  if (k.startsWith('helpai.') && k.endsWith('.conversationId')) localStorage.removeItem(k);
}
destroy();
init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000' });
```

### "Can I send a message programmatically for the user?"

Not via the public API — by design. Messages must come from user
input (so they see + confirm what's being sent). Use **welcome
suggestions** in your dashboard for "prefilled" conversation
starters.

### "Can I get the full chat transcript?"

From the client, no direct API. Subscribe to `message` events and
build your own log:

```ts
const transcript = [];
chat.on('message', (m) => transcript.push(m));
```

Via the backend, yes — query the chats API with your visitor's
conversation token. Contact support for the server-side endpoint docs.

### "Why can't I see what the user is typing in real-time?"

Privacy. The widget doesn't emit events for in-progress typing —
only committed messages (Enter / Send click). If you need
keystroke-level telemetry, that's not supported.

### "The widget's language is wrong for my user"

Resolution precedence:

1. User's persisted pick (language switcher).
2. `init({ i18n: { locale: '...' } })`.
3. `init({ i18n: { defaultLocale: '...' } })`.
4. `navigator.language` (when `i18n.defaultLocale: "auto"`).
5. English (`"en"`).

To force: `init({ i18n: { locale: 'fr-CA' } })`. To let the visitor switch,
provide `i18n.availableLocales` and include `language` in `header.actions`.

---

## Getting more help

1. **Read the [FAQ](/sdk/faq)** — most common questions answered
   there.

2. **Check our GitHub issues**: [github.com/help-ai/elements](https://github.com/help-ai/elements/issues).

3. **Email support**: [support@help.ai](mailto:support@help.ai). Include:
   - The URL of the page where the issue appears.
   - Browser + version.
   - Screenshot + console output.
   - Steps to reproduce.

4. **Status page**: [help.ai/status](https://help.ai/status) — check
   for ongoing incidents before troubleshooting.

5. **Debug mode**: add `?helpai-debug=1` to your URL. The widget
   logs its internal state to the console (prefixed with `[helpai]`).
