Skip to content

Troubleshooting

An install that does not show, connect or answer.

Last updated

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.


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 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.


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 — most common questions answered there.

  2. Check our GitHub issues: github.com/help-ai/elements (opens another site).

  3. Email support: 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 (opens another site) — 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]).

Still need help?

Ask the community