The widget
The chat bubble on your site. One <script> tag, about 41 KB gzipped in all, inside a shadow root so your page's CSS cannot break it and it cannot break your page. A small loader (about 6 KB) draws the button first; the chat itself loads once the page is idle, or as soon as a visitor reaches for the button, so it never slows your page down.
Try it first: the playground runs the real widget with every option below in a panel beside it, and exports the config you build. A link to your setup keeps all of it; see The playground.
Embedding
<script src="https://<your worker>/loader.js" data-site="acme" async></script>Put it before </body> on every page, or in the layout your pages share. Dashboard → Home has yours, with a Check my site button. The page must be on one of the origins in helppuff.json.
| Attribute | |
|---|---|
data-site | Required. The site id. |
data-open | Open the chat as soon as it loads. |
data-fill | Make the chat fill the whole page (or the iframe it is in), with no launcher or close button: for a full-page chat link or an embedded panel. The dashboard's test chat is /chat.html, which uses data-open data-fill. |
data-theme | light, dark or auto: overrides the configured theme. |
data-api | Where the chat API is, when the script is served from somewhere else (your own CDN). Default: wherever loader.js came from. |
Your Worker also serves a demo page at https://<your worker>/ (Dashboard → Home → Share a demo).
Customising
Most of it is in Dashboard → Settings: names, welcome message, the home screen (below), colour, position, button icon, the lead form. Everything else is under widget in helppuff.json; see the Configuration reference. Highlights:
"widget": {
"brand": { "agentName": "Ava", "accent": "#0F766E", "avatar": "https://acme.com.au/ava.png", "theme": "auto",
"tokens": { "radius-panel": "12px", "font": "Inter, system-ui, sans-serif" } },
"launcher": { "position": "bottom-left", "label": "Chat with us", "shape": "pill", "icon": "wrench",
"offset": { "x": 24, "y": 96 }, "hideOnPaths": ["/checkout/**"] },
"home": {
"title": "Hi there",
"subtitle": "Ask anything, or pick one.",
"shortcuts": [
{ "id": "quote", "label": "Get a quote", "icon": "quote", "action": { "id": "quote", "kind": "flow", "label": "Get a quote", "flowId": "quote" } },
{ "id": "call", "label": "Call us", "icon": "phone", "action": { "id": "call", "kind": "tel", "label": "Call", "phone": "+61390000000" } }
]
},
"teaser": { "text": "Need a quote? Ask me.", "afterScroll": 40, "paths": ["/services/**"] },
"flows": [
{ "id": "quote", "steps": [
{ "field": "service", "ask": "What do you need?", "input": "choice", "choices": ["Hot water", "Blocked drain", "Something else"] },
{ "field": "suburb", "ask": "Which suburb?", "input": "text" }
],
"submit": { "as": "message", "template": "I'd like a quote for {service} in {suburb}." } }
]
}- Shortcuts are buttons on the first screen (or above the message box during a chat): send a question, open a page, call, email, start a flow, open a form.
- Flows ask a few questions inside the widget (no AI involved) and send the answers as one message.
- Forms (
widget.forms) are inline forms a shortcut or the assistant can open; submitting sends the answers to the assistant. - Theme tokens override CSS custom properties (colours, fonts, radii, sizes, motion) without touching the stylesheet. Unknown tokens are ignored, and values cannot inject CSS.
stringsoverrides any text the widget shows (button labels, error messages), for wording or language.- "Powered by HelpPuff" is always shown at the bottom of the chat. It is not a setting;
strings.poweredBycan translate it, as long as the translation still says HelpPuff. Older configs withpoweredBystill load, and the value is ignored. - Live chat (Live chat): when it is on, a person button at the top of the chat asks for someone from the team (
strings.talkToPerson), and the header says who is answering (strings.liveStatus). Its code is a separate small file (live-*.js), downloaded only when a chat is handed over. - Photos and files (Photos and files): when visitors may send them, a paperclip sits next to the message box (
strings.attachis its label;strings.fileFailedthe message when a file could not be sent). Its code is a separate small file (upload-*.js), downloaded when a file is picked or shown.
The home screen
What visitors see when they open the chat: a heading, up to 8 buttons and a list of useful pages. Edit it in Settings → Home screen, with a preview:
- Buttons, in order. Each one asks a question for the visitor, opens a page, calls, emails, opens one of your forms (
widget.forms) or asks a few questions (widget.flows). Under More: a line under the label, and pages it shows on only (/services/**). The Get a quote button belongs to Jobs and is set there, and the Book a time button to Bookings (a booking link, or the date picker in the chat). - Useful pages: links under the buttons (prices, services, booking, FAQ), with a line under each. They open in a new tab.
Suggested from your website. When HelpPuff first learns your site, the AI picks the pages a visitor would most want and words a link for each (only pages your site has), and writes four questions visitors ask. The widget shows them straight away, until you set the home screen up yourself: your own buttons and links always win, and saving the page once makes it yours (nothing is suggested again). Suggest from my site on the page adds more at any time (with call and email buttons from your business details) for you to keep or drop. It checks the buttons already on the page and skips suggested buttons with the same label or action. It also skips useful pages already listed or already opened by a button.
In helppuff.json it is widget.home (title, subtitle, shortcuts, links), and helppuff config pull brings dashboard changes into it. Over the API: PUT /settings with home, and POST /home/suggest for suggestions.
The JavaScript API
HelpPuff.open(); // open the chat
HelpPuff.close();
HelpPuff.toggle();
HelpPuff.send('Do you work on Sundays?'); // send a message as the visitor (opens the chat)
HelpPuff.identify({ name: 'Ada', email: 'ada@example.com' }); // skip the lead form; details go to the lead
HelpPuff.identify({ name, email, token }); // a signed-in visitor: your server signed the token (see Signed-in visitors)
HelpPuff.reset(); // forget the conversation (e.g. on sign-out)
HelpPuff.destroy(); // remove the widget from the page
HelpPuff.on('open', () => …);
HelpPuff.on('close', () => …);
HelpPuff.on('lead', (lead) => …); // the visitor submitted their details
HelpPuff.on('message', ({ role, count }) => …); // a reply arrived
HelpPuff.off('lead', handler);identify({ name, email }) is what the page says: anyone can call it from the browser's console with someone else's email, so treat it as a claim. For a site with accounts, add a token your server signed: the Worker checks it, and the visitor's account becomes trusted {{user.*}} (Signed-in visitors).
Calls made before the script has loaded are queued if you add this stub first:
<script>
window.HelpPuff = window.HelpPuff || { q: [] };
['open', 'close', 'toggle', 'send', 'identify', 'reset', 'on', 'off'].forEach(function (name) {
if (!window.HelpPuff[name]) window.HelpPuff[name] = function () { window.HelpPuff.q.push([name].concat([].slice.call(arguments))); };
});
</script>Every method is safe to call at any time: if the widget is not available, it does nothing rather than throw.
Behaviour you can rely on
- It never breaks your page. Every failure (a missing config, a blocked network, an old browser) ends in a working widget or no widget, never an error on your page. It writes nothing to your
<head>, sets no cookies, and uses onlyhp:-prefixed local storage. - Accessible. Keyboard-only use, screen-reader announcements for replies, focus returned to the button on close, every target at least 44 px.
- Mobile. Full screen on phones, no zoom on input focus, the composer stays above the keyboard.
- Conversations survive page loads and reloads for the session's lifetime (
security.sessionTtlHours, default 24). - Debugging: add
?hpdebug=1to a page's URL to log what the widget does to the console;HelpPuff.debug()returns its state.