Docs

Web SDK

Dependency-free ES modules for the help centre, walkthroughs, support chat and a report widget. There is no npm install or build step.

Requirements

A current browser with ES modules, shadow DOM, native dialogs and Intl.Segmenter. Serve the SDK's src/ directory and your generated bundle as static files, and allow your site's origin on the hlpd app.

Load and mount

JavaScript
import { Hlpd } from "./hlpd/src/index.js";

const hlpd = await Hlpd.load({
  apiKey: "hk_…",
  bundleURL: "/hlpd-bundle/",
  // baseURL defaults to https://api.hlpd.uk
  tickets: { serverURL: "https://api.hlpd.uk", projectID: "brp_your_product" },
  context: () => ({ screen_name: "Settings" }),
  onEscalate: (ticketId, summary) => { /* Observe a sent ticket or handle app-owned support. */ },
});

const help = hlpd.mountHelpCenter(document.querySelector("#help"));
const support = hlpd.mountSupport(document.querySelector("#support"));
// help.unmount(); support.unmount(); hlpd.destroy();

Both mounts render into shadow roots with the bundled stylesheet. Light and dark themes follow prefers-color-scheme, and dialogs keep drafts and close with Escape. Topics and search use only bundle files, so the help stays available when the API cannot be reached.

Hlpd.load fetches the manifest and lexical index first and loads the embedding files in the background. await hlpd.semanticReady waits for hybrid search (it resolves to false if embeddings are absent or invalid), and hlpd.orderReady resolves to the fetched order, or null.

Search and suggestions

JavaScript
const hits = hlpd.search("pause reminders", 5);
// [{ id, title, summary, score, lexical, semantic }]
const suggestions = hlpd.suggestions("reminders keep buzzing at night");

Suggestions in your own form

Give the SDK your input or textarea and an empty element above it. Suggestions wait 400 ms and need three content words plus the bundle's confidence threshold.

JavaScript
const attachment = hlpd.attachSuggestions(descriptionInput, suggestionsElement, {
  surface: "bug_report",
});

// After your own report submission succeeds, before resetting the form:
attachment.reportFiled({ ticketId: response.ticket_id });
// attachment.detach();

Opening a card shows the topic and its walkthrough modes in an overlay. reportFiled records the suggested and opened topics with your ticket id and resets tracking for the next report.

Your own chat UI

hlpd.chat is an observable headless model. Its entries contain { id, role, text, attachments, isError? }, with attachment types topic, walkthrough, escalation and ticket.

JavaScript
const unsubscribe = hlpd.chat.subscribe(model => renderYourChat(model));
await hlpd.chat.send("Where can I pause reminders?");
hlpd.chat.openSupport();
hlpd.chat.updateDraft(draftId, { title: "Reminders won't stop", summary: "Details…" });
await hlpd.chat.sendSupport(draftId);
hlpd.chat.cancelDraft(draftId);
hlpd.chat.openedTopic(topicId);
hlpd.chat.viewedWalkthrough(topicId);
// hlpd.chat.reset(); unsubscribe();

With no tickets configuration, the chat says "Support is handled by the app" and calls onEscalate(null, summary). With tickets, it calls onEscalate only after Send succeeds. hlpd.agent.ask(question) returns raw agent replies without submitting tickets.

Tickets

hlpd.tickets exposes list(), ticket(id), refresh(), refreshTicket(id), markRead(id), messages(id), reply(id, body) and subscribe(listener), plus submitReport({ title, text, manifest, context, topicsOpened, attachments }) for host forms that share the SDK inbox. Attachments are File or Blob objects: PNG, JPEG, WebP or GIF, at most 5 MB each.

Report widget

src/bug-reporter.js and src/bug-reporter.css also work on their own as a report and feature-request form for any page. Mark up a form and it mounts automatically:

HTML
<link rel="stylesheet" href="/hlpd/src/bug-reporter.css">
<script type="module" src="/hlpd/src/bug-reporter.js"></script>

<form class="bug-report-widget" data-bug-report-form
      data-server-url="https://api.hlpd.uk" data-project-id="brp_your_project">
  <input type="hidden" name="kind" value="bug">
  <label>Title <input name="title" maxlength="200" required></label>
  <label>What happened? <textarea name="body" required></textarea></label>
  <label>Screenshot or photo <input name="attachments" type="file" accept="image/*" multiple></label>
  <button type="submit">Send</button>
  <div class="bug-report-status" data-bug-report-status aria-live="polite"></div>
</form>

Or call mountBugReportForm(form, { serverUrl, projectId }) yourself. The widget lists the visitor's saved tickets and threads. widget/index.html in the SDK is a complete example with a launcher and dialog.

Walkthroughs

Pass a walkthroughs host to Hlpd.load. Modes are showMe and guideMe; the optional supports(script, mode) can return false to disable a mode. begin returns a session, and the exported createDOMSession builds one from a registry of your real elements:

JavaScript
import { Hlpd, createDOMSession } from "./hlpd/src/index.js";

const hlpd = await Hlpd.load({
  apiKey: "hk_…",
  bundleURL: "/hlpd-bundle/",
  walkthroughs: {
    async begin({ walkthrough, mode, container, signal }) {
      if (mode === "showMe") {
        const demo = createDemoLogView(); // your own view with disposable sample data
        container.append(demo.root);
        return createDOMSession({
          root: demo.root,
          registry: new Map([["record-drink", demo.recordButton]]),
          isolated: true,
          dispose: () => demo.root.remove(),
        });
      }
      return createDOMSession({
        root: document.querySelector("#app"),
        registry: new Map([["record-drink", document.querySelector("#record-drink")]]),
      });
    },
  },
});
  • Show me requires isolated: true and a newly created view with disposable data, mounted in container. Never connect it to production data.
  • Guide me returns the real page root. The SDK never calls perform in Guide me; actionable steps advance only after the user's own tap or typing.
  • The SDK never executes selectors or bundle code. Targets resolve only through your registry.

Narration audio is generated in advance by the publisher and plays by default, with Mute and Enable sound controls. Captions stay visible if the browser blocks autoplay.

Device id

The SDK generates one 16-character base62 device id per browser storage and keeps it under Hlpd.deviceID. Help requests send it as X-Hlpd-Device and reports send it as client_id. deviceID(storage) from src/storage.js reads it.