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:
// 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#
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 barecdn.help.ai/elements.jsroot 404s). Verify the URL in DevTools → Network tab.Is the deployment identity complete?
js// In console: window.HelpAI?.getInstance?.();Returns
nullif no instance mounted. Likely causes:- Missing required
data-ai-agent-deployment-idon the install script. init()called withoutaiAgentDeploymentId.publicKeyis optional only for intentional guest mode.- Typo in the key or deployment ID.
- Missing required
Is the host element in the DOM?
jsdocument.querySelector('helpai-chat-host');If null, the widget didn't mount. Check the console for init errors.
Is the host hidden by host-page CSS? — shouldn't happen (the widget uses
all: initialto reset), but if you've got a very aggressive* { display: none !important; }style somewhere:jsconst host = document.querySelector('helpai-chat-host'); console.log(getComputedStyle(host).display); // should be "block" or "inline-block"Is
launcher.hidden: trueset? — that hides the FAB. Check your options.Is
modeset toinlineorstandalone? — those modes don't show a launcher. Check your options.
Symptom: bubble shows but clicking does nothing#
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.Are
x-public-keyandx-ai-agent-deployment-idmaking it through? — some corporate proxies strip custom headers. Test on a personal network.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 addhelp.aito yourconnect-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.aiAPI 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.aiinconnect-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):
Ensure you have a viewport meta tag:
html<meta name="viewport" content="width=device-width, initial-scale=1" />Avoid having
overflow: hiddenon<html>or<body>— this clips fixed positioning on some browsers.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.
/* 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:
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:
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:
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:
init({
theme: {
fontFamily: 'Inter, system-ui, sans-serif',
},
});Symptom: launcher bubble is a different color than expected#
Check:
theme.accentin yourinit()options.- The workspace default accent in your Help AI dashboard.
prefers-color-scheme— the accent might look different in dark mode vs. light.
To debug:
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#
Is localStorage available? Private/incognito modes may disable it.
jstry { localStorage.setItem('_test', '1'); console.log('OK'); } catch (e) { console.error('Blocked', e); }Check the widget's storage keys:
jsfor (const k of Object.keys(localStorage)) { if (k.startsWith('helpai.')) console.log(k); }You should see at least
helpai.default.visitorIdand (after a chat)helpai.default.conversationId.Is the
widgetIdconsistent across reloads? If you pass a dynamicwidgetId(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:
// 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:
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:
// 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#
Is the instance really mounted?
jsconst chat = init({ publicKey: '...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000' }); console.log(chat.widgetId); // should log the idAre messages being sent? Open the Network tab, filter
stream-message. A send should produce a POST.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#
Is the bundle loading? Check Network →
elements.js→ status 200.Is
window.HelpAIHooksan array?jsArray.isArray(window.HelpAIHooks); // should be trueIs 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#
Are you using
deferon the script tag? Withoutdefer, the script blocks HTML parsing.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.
Are you self-hosting an older version? Pull the latest. Older versions were larger.
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: fixedis working (floating mode) or the inline container has a stable size.
Symptom: streaming feels choppy#
Slow backend — the widget just renders what it receives. Check the stream latency in Network → the
stream-messageentry.Network conditions — SSE is sensitive to packet loss. Try a different network.
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/dataitself). - 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 recordingFull 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:
// 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:
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:
- User's persisted pick (language switcher).
init({ i18n: { locale: '...' } }).init({ i18n: { defaultLocale: '...' } }).navigator.language(wheni18n.defaultLocale: "auto").- 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#
Read the FAQ — most common questions answered there.
Check our GitHub issues: github.com/help-ai/elements (opens another site).
Email support: support@help.ai. Include:
- The URL of the page where the issue appears.
- Browser + version.
- Screenshot + console output.
- Steps to reproduce.
Status page: help.ai/status (opens another site) — check for ongoing incidents before troubleshooting.
Debug mode: add
?helpai-debug=1to your URL. The widget logs its internal state to the console (prefixed with[helpai]).