npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@synerise/ai-assistant-sdk

v1.15.0

Published

Drop-in browser SDK for embedding the Synerise AI Assistant in vanilla JavaScript applications.

Readme

@synerise/ai-assistant-sdk

Drop-in browser SDK for embedding the Synerise AI Assistant in vanilla JavaScript applications. Ships as ESM and CJS — works with bundlers or directly via <script type="module">.


Requirements

This SDK is designed for the Synerise platform. To use it you need:

  • An active Synerise tenant and a tracker key
  • Network access to https://api.synerise.com (or your custom Synerise API endpoint)
  • A browser environment (the SDK uses window, document, fetch)

If you don't have a Synerise account, visit synerise.com.

The SDK technically allows pointing apiUrl at any HTTP endpoint that follows the Synerise AI Assistant API contract, but it is not designed as a generic chat SDK.


Which package do I need?

| Your stack | Package | | -------------------------------------------- | -------------------------------------------------------------------------------------------- | | Vanilla JS / no bundler / <script> tag | @synerise/ai-assistant-sdk (this package) | | React 18 | @synerise/ai-assistant-react | | Preact | @synerise/ai-assistant-core | | Build a custom chat UI from primitives | @synerise/ai-assistant-ui |

How packages relate

@synerise/ai-assistant-sdk    ──┐
                                ├──>  @synerise/ai-assistant-core  ──>  @synerise/ai-assistant-ui
@synerise/ai-assistant-react  ──┘

The -sdk package is a self-contained ESM/CJS bundle. It already includes Preact and the UI internally — you don't install or configure anything else.


Installation

Bundler (Webpack, Vite, Rollup, esbuild, …)

npm install @synerise/ai-assistant-sdk
# or
pnpm add @synerise/ai-assistant-sdk
# or
yarn add @synerise/ai-assistant-sdk

Quick start

ESM / CJS (bundler)

// ESM
import { init } from "@synerise/ai-assistant-sdk";

const chat = init({
  rootElementId: "chat-container",
  context: { profileId: "user-123" },
  additionalContextValues: { segment: "premium" },
});

chat.open();
// CommonJS
const { init } = require("@synerise/ai-assistant-sdk");

const chat = init({
  rootElementId: "chat-container",
});

Browser (<script type="module">)

Host the contents of build/ on a CDN with CORS enabled and load the entry (ai-assistant.es.js) as a module. The browser fetches lazy chunks (e.g. the markdown renderer) on demand from the same path — serve them with Access-Control-Allow-Origin: * and Content-Type: text/javascript headers. Synerise's own CDN re-publishes the entry under sdk.es.js at https://web.snrbox.com/ai-shop-assistant/.

<!DOCTYPE html>
<html>
  <body>
    <div id="chat-container"></div>

    <!-- 1. Synerise JS SDK (required for default auth flow) -->
    <script>
      function onSyneriseLoad() {
        SR.init({
          trackerKey: "{{YOUR_TRACKER_KEY}}",
        });
      }
      (function (s, y, n, e, r, i) {
        s["SyneriseObjectNamespace"] = r;
        ((s[r] = s[r] || []),
          (s[r]._t = 1 * new Date()),
          (s[r]._i = 0),
          (s[r]._l = i));
        var z = y.createElement(n),
          se = y.getElementsByTagName(n)[0];
        z.async = 1;
        z.src = e;
        se.parentNode.insertBefore(z, se);
        z.onload = z.onreadystatechange = function () {
          if (!z.readyState || /complete|loaded/.test(z.readyState)) {
            s[i]();
            z.onload = null;
            z.onreadystatechange = null;
          }
        };
      })(
        window,
        document,
        "script",
        "//web.snrbox.com/synerise-javascript-sdk.min.js",
        "SR",
        "onSyneriseLoad",
      );
    </script>

    <!-- 2. Initialize the chat -->
    <script type="module">
      import { init } from "https://cdn.example.com/ai-assistant-sdk/ai-assistant.es.js";

      const chat = init({
        rootElementId: "chat-container",
        header: "Customer Support",
        context: { profileId: "user-123" },
        additionalContextValues: {},
      });
    </script>
  </body>
</html>

Authentication

The SDK supports two auth modes via the disableXSRFToken option:

| Mode | disableXSRFToken | Behavior | | --------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Synerise SDK auth (default) | true | SDK reads SR.auth.trackerKey() and SyneriseTC.uuid from the page and sends them as authParams in each request. Requires the Synerise JS SDK to be loaded and initialized first. | | Cookie + XSRF token | false | SDK includes credentials with each fetch and reads the XSRF-TOKEN cookie into the X-XSRF-TOKEN header. Use this if your tenant is configured for session-based auth. |

If you pass an explicit apiUrl, you also bypass the default Synerise endpoint — useful for staging/dev environments.


API reference

init(options)ChatInstance

| Option | Type | Required | Description | | ------------------------- | ------------------------- | -------- | --------------------------------------------------------------------------------- | | rootElementId | string | yes | DOM element id where the chat is mounted | | context | Context \| null | no | Context sent with each request. Omit or pass null if unused (default null). | | additionalContextValues | AdditionalContextValues \| null | no | Extra metadata sent with each request. Omit or pass null if unused (default null). | | header | string | no | Chat header title (default "AI Assistant") | | avatar | string | no | URL of the assistant avatar | | theme | Theme | no | Theme variables override (see Theming) | | texts | Texts | no | Override default UI texts | | slots | Slots | no | Override specific UI parts (see Slots) | | stream | boolean | no | When true, uses SSE for assistant responses | | fastMode | boolean | no | When true, sends fastMode=true query param | | assistantId | string | no | Optional assistant configuration ID (UUID) sent as assistantId query param. Falls back to backend default when omitted. | | apiUrl | string | no | Override base API URL (default https://api.synerise.com/agents/v1/ai-assistant) | | disableXSRFToken | boolean | no | See Authentication (default true) | | threadId | string | no | Mount with this existing thread loaded — see Conversation history | | onMessage | (response) => void | no | Called on every successful response | | onConversationTitle | ({ threadId, title }) => void | no | Called when the backend emits the conversation's auto-generated title (stream mode only) — see Conversation history | | onStreamError | ({ message, type, stage }) => void | no | Called when the backend refuses a streamed exchange (error SSE event) — e.g. a guardrail rejection. See Refused responses | | pageContext | StorefrontPageContext \| null | no | Initial storefront page (PDP) the user is on — see Storefront page context | | autoDetectPageContext | boolean | no | When true, SDK reads OpenGraph product meta tags and keeps pageContext in sync — see Storefront page context |

Returned ChatInstance methods

| Method | Returns | Description | | ----------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------ | | open() | void | Opens the chat UI | | close() | void | Closes the chat UI | | message(text) | void | Sends a message programmatically | | unmount() | void | Removes the chat from the DOM and cleans up | | setContext(value) | void | Replace context | | setAdditionalContextValues(value) | void | Replace additionalContextValues | | setPageContext(value) | void | Set or clear the storefront page (PDP). Engages the dedicated channel, which overrides any additionalContextValues.page_context — see Storefront page context | | loadThread(threadId) | Promise<void> | Replace current messages with the messages of an existing thread — see Conversation history | | getConversations() | Promise<AIAssistantConversation[]> | List the user's historical threads — see Conversation history |


Storefront page context (PDP)

When the user opens the chat from a product detail page, telling the assistant which product they're viewing lets it resolve "this", "it", "cheaper", "is this any good?" against the right item instead of asking the user to repeat themselves.

The signal travels per turn under additionalContextValues.page_context. Two ways to wire it up.

Manual — call setPageContext on route change

import { init, type StorefrontPageContext } from "@synerise/ai-assistant-sdk";

const chat = init({ rootElementId: "chat-container" });

// On entering a PDP:
chat.setPageContext({ pageType: "product", itemId: "SKU-12345" });

// On leaving the PDP (or any non-product page):
chat.setPageContext(null);

pageType accepts only "product" today. itemId must match the feed catalog id used elsewhere in your Synerise integration.

Automatic — autoDetectPageContext: true

The SDK reads OpenGraph product-extension meta tags at mount and then watches <head> for further changes via MutationObserver. Works out of the box with libraries that mutate meta tags on navigation (react-helmet, vue-meta, next/head, etc.). The initial /chat POST already carries the detected context — meta tags are read synchronously during mount, so you don't need to defer opening the chat after navigating to a PDP.

<head>
  <meta property="og:type" content="product" />
  <meta property="product:retailer_part_no" content="SKU-12345" />
</head>
init({ rootElementId: "chat-container", autoDetectPageContext: true });

Recognised meta values:

  • og:typeproduct or product.item. product.group (variant parent) is intentionally ignored.
  • product:retailer_part_no — the canonical Facebook / Synerise catalog id.

Auto-detect is opt-in because the meta tags only help when they match your feed ids. If they don't, use manual setPageContext from your router instead.

How the two channels interact

The dedicated channel (pageContext init prop, autoDetectPageContext, setPageContext) and the generic additionalContextValues.page_context channel interact deterministically:

  • Before you touch the dedicated channel (no pageContext prop, no autoDetectPageContext, no setPageContext() call), anything under additionalContextValues.page_context flows through unchanged.
  • Once you touch it — pass pageContext (even null), enable auto-detect, or call setPageContext() — the dedicated channel becomes authoritative for every subsequent turn:
    • setPageContext({...}) overrides any additionalContextValues.page_context.
    • setPageContext(null) strips page_context from the merged extras even if it's still present in additionalContextValues.

Pick one channel and stick with it; the dedicated one is recommended.

Pure detection helper

import { detectPageContextFromMeta } from "@synerise/ai-assistant-sdk";

const detected = detectPageContextFromMeta(); // StorefrontPageContext | null

For when you want to read meta tags on demand from your own listener without subscribing to the SDK's observer.


Conversation history

The SDK exposes the user's conversation history as programmatic API. The chat UI does not render a history picker today — your host application owns that surface and uses these methods to build it.

Mount with an existing thread

const chat = init({
  rootElementId: "chat-container",
  threadId: "b9f1d2a4-…", // UUID of an existing thread
});

chat.open();

The chat fetches that thread's messages on mount instead of starting a fresh conversation.

Switch threads at runtime

await chat.loadThread("b9f1d2a4-…");

Clears the active messages and loads the selected thread in place.

List the user's conversations

const conversations = await chat.getConversations();
// AIAssistantConversation[]

Shape:

type AIAssistantConversation = {
  threadId: string;
  agentType: string;
  realm: string;
  businessProfileId: number;
  userId?: number | null;
  clientId?: string | null;
  createdAt: string;                       // ISO timestamp
  title?: string | null;                   // auto-generated from the first user message
  summary?: string | null;                 // assistant-generated short label
  meta?: Record<string, unknown> | null;
};

Minimal history picker

const chat = init({ rootElementId: "chat-container" });
chat.open();

const list = document.getElementById("history");
const conversations = await chat.getConversations();

conversations
  .sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt))
  .forEach((conv) => {
    const item = document.createElement("li");
    item.textContent = conv.title || conv.summary || conv.threadId;
    item.onclick = () => chat.loadThread(conv.threadId);
    list.appendChild(item);
  });

Live title updates (onConversationTitle)

In stream mode the backend generates a conversation title from the first user message and pushes it as a conversation_title SSE event. Pass onConversationTitle to receive it without refetching the history:

const chat = init({
  rootElementId: "chat-container",
  stream: true,
  onConversationTitle: ({ threadId, title }) => {
    // update the single entry in your history picker in place…
    renameHistoryItem(threadId, title);
    // …or treat it as a trigger to refetch:
    // chat.getConversations().then(renderHistory);
  },
});

The same title is persisted server-side, so subsequent getConversations() calls return it as title.

When the thread no longer exists

If the backend returns 404 for the requested thread (deleted, expired, wrong tenant), the chat enters a fatal-error state with errorType: "THREAD_NOT_FOUND". The recovery button is labelled "Start new conversation" (customise via texts.startNewConversation) and starts a fresh conversation rather than retrying the failing request — a stale link in a history list degrades gracefully into a new chat.


Refused responses (guardrails)

In stream mode the backend can refuse an exchange instead of answering it. It then emits an error SSE event in place of the response — no message event follows, only [DONE]:

event: error
data: {"message":"Nie mogę pomóc w tej sprawie. Czy mogę pomóc w czymś innym?","type":"validation_failed","stage":"input"}

data: [DONE]

The SDK handles both shapes of that event:

  • With message — the backend already prepared user-facing copy, so it is rendered as the assistant's reply, exactly like the fallback messages guardrails used to send as ordinary messages. The chat stays usable, no error state is shown, and onMessage reports the turn like any other response. A prompt refused at stage: "input" is never echoed by the backend, so the SDK keeps the user's own bubble in the transcript; note that such a turn is not part of the server-side history, so it will not reappear after loadThread().
    • The exception is the opening request being refused this way: it carries no threadId, so nothing can be sent afterwards. The copy is still rendered, with the error state (and Try Again) below it — and since that turn ends as an error rather than a response, onMessage does not fire for it.
  • Without message — nothing renderable, so the chat falls back to its error state with errorType: "STREAM_ERROR" and a Try Again button (customise the copy via texts.errorMessage.STREAM_ERROR). The prompt stays in the transcript, so the error is shown under the question it belongs to.

onStreamError fires for both, with the raw payload — use it for analytics or to react to specific type / stage values (a callback that throws is contained and does not break the response):

init({
  rootElementId: "ai-assistant",
  stream: true,
  onStreamError: ({ type, stage }) => {
    analytics.track("assistant_refused", { type, stage });
  },
});

Theming

All visual properties are theme variables. Override only the keys you need:

init({
  rootElementId: "chat-container",
  context: {},
  additionalContextValues: {},
  theme: {
    variables: {
      "chat-header-bg": "#0f172a",
      "chat-header-text-color": "#f8fafc",
      "chat-submit-button-bg-default": "#7c3aed",
    },
  },
});

Full list of theme variables and defaults:

{
  "chat-error-color": "#F52922",
  "chat-success-color": "#54CB0B",
  "chat-border-color": "#DBE0E3",
  "chat-box-shadow": "0px 4px 12px 0px #2329360A",

  "chat-container-text-family": "'Graphik LCG Web', Arial, sans-serif",
  "chat-container-border-color": "linear-gradient(270deg, #8e54e0 14.42%, #ff6c4d 100%)",
  "chat-container-border-radius": "8px",
  "chat-container-bg": "#f9fafb",
  "chat-border-radius": "8px",

  "chat-header-bg": "#ffffff",
  "chat-header-text-color": "#384350",
  "chat-header-border-color": "#e9edee",

  "chat-prompt-bg": "#384350",
  "chat-prompt-text-color": "#ffffff",

  "chat-message-text-color": "#384350",
  "chat-message-highlighted-border-color": "linear-gradient(270deg, #8e54e0 14.42%, #ff6c4d 100%)",
  "chat-suggestion-element-description-color": "#384350",
  "chat-carousel-button-radius": "3px",
  "chat-link-text-color-default": "#0044d9",
  "chat-link-text-color-hover": "#0337A8",

  "chat-message-buttons-direction": "row",
  "chat-message-buttons-justify": "flex-start",
  "chat-message-buttons-gap": "8px",

  "chat-input-bg-default": "#ffffff",
  "chat-input-bg-disabled": "#f9fafb",
  "chat-input-text-color": "#57616d",
  "chat-input-text-placeholder-color": "#949ea6",
  "chat-input-border-color-default": "linear-gradient(270deg, #8e54e0 14.42%, #ff6c4d 100%)",
  "chat-input-border-color-focus": "#6d2ed3",

  "chat-button-radius": "8px",
  "chat-button-color-default": "#0b68ff",
  "chat-button-color-hover": "#238afe",
  "chat-button-color-disabled": "#238afe",
  "chat-button-bg-default": "#ffffff",
  "chat-button-bg-hover": "#ffffff",
  "chat-button-bg-disabled": "#ffffff",
  "chat-button-border-color-default": "transparent",
  "chat-button-border-color-hover": "transparent",
  "chat-button-border-color-disabled": "transparent",
  "chat-button-border-color-focus": "#0b68ff",
  "chat-button-border-width": "1px",
  "chat-button-shadow": "0px 4px 12px 0px rgba(35, 41, 54, 0.04)",

  "chat-submit-button-radius": "3px",
  "chat-submit-button-color-default": "#ffffff",
  "chat-submit-button-color-hover": "#ffffff",
  "chat-submit-button-color-disabled": "#ffffff",
  "chat-submit-button-bg-default": "#6d2ed3",
  "chat-submit-button-bg-hover": "#8e54e0",
  "chat-submit-button-bg-disabled": "#b88cee",
  "chat-submit-button-border-color-default": "transparent",
  "chat-submit-button-border-color-hover": "transparent",
  "chat-submit-button-border-color-disabled": "transparent",
  "chat-submit-button-border-color-focus": "#0b68ff",
  "chat-submit-button-border-width": "0px",
}

Slots

Replace specific parts of the UI with your own renderer. Each slot is a function that returns either a raw HTMLElement (handy for vanilla JS) or a Preact/React ReactNode (e.g. a VNode produced with h(...)). The chat detects which one you return and wraps it accordingly.

init({
  rootElementId: "chat-container",
  context: {},
  additionalContextValues: {},
  slots: {
    header: {
      closeButton: ({ onClose }) => {
        const button = document.createElement("button");
        button.textContent = "x";
        button.onclick = onClose;
        return button;
      },
    },
  },
});

Available slots: header.title, header.closeButton, header.extraActions, messages.avatar, messages.loader, messages.error, messages.type.{bubbleText,button,text,suggestion,list,products,custom}, textArea.submitButton, textArea.input, markdown.{tagName}. See type definitions for full signatures.

The product and bubble-text slots additionally receive an onItemClick callback for click tracking — see Product click tracking.


Product click tracking

The assistant reports an assistant.click telemetry event whenever a user clicks a product it surfaced — a card in a product carousel (CAROUSEL) or an inline product link woven into assistant text (BUBBLE_TEXT).

Default UI — nothing to do

If you don't override the product or bubble-text slots, clicks on the built-in markup are tracked automatically, including middle-click to open a product in a background tab. No configuration.

Custom UI — call onItemClick

When you override a slot you replace the assistant's own markup, so it can no longer detect the click itself. Each click-bearing slot is therefore handed a pre-bound onItemClick: call it from your click handler and the event — with the owning response's seqNo/correlationId and the source component — is sent for you. You never assemble the payload yourself.

| Slot | Callback signature | Call it with | | -------------------------- | --------------------------- | ---------------------------------------------------------------------- | | messages.type.products.item | onItemClick(): void | nothing — already bound to this card's item | | messages.type.products.container | onItemClick(itemId): void | the clicked product's itemId | | messages.type.bubbleText | onItemClick(itemId): void | the itemId of the clicked link (resolve it from productLinks) |

init({
  rootElementId: "chat-container",
  slots: {
    messages: {
      type: {
        products: {
          // data: ProductItem & { onItemClick: () => void }
          item: ({ title, img, action, onItemClick }) => {
            const card = document.createElement("a");
            if (action.type === "REDIRECT") {
              card.href = action.url;
              card.target = "_blank";
              card.rel = "noopener noreferrer";
            }
            card.innerHTML = `<img src="${img}" alt="" /><span>${title}</span>`;
            card.addEventListener("click", () => onItemClick());
            return card;
          },
        },
      },
    },
  },
});

For a container slot (you render every card yourself), pass each product's id:

container: ({ content, onItemClick }) => {
  const wrap = document.createElement("div");
  content.forEach((product) => {
    const card = document.createElement("a");
    card.textContent = product.title;
    card.addEventListener("click", () => onItemClick(product.itemId));
    wrap.appendChild(card);
  });
  return wrap;
};

Notes:

  • Calling onItemClick for a product the backend couldn't attribute (no itemId), or for items in the opening greeting (which carries no seqNo), is a safe no-op — you can wire it unconditionally.
  • The event is best-effort telemetry: it's fire-and-forget and never blocks the click or navigation, and a network failure is swallowed silently.

Programmatic example

const chat = init({
  rootElementId: "chat-container",
  context: { profileId: "user-123" },
  additionalContextValues: { segment: "premium" },
  onMessage: (response) => console.log("threadId:", response.meta.threadId),
});

chat.open();
chat.message("Show me top products from the last 7 days");

Troubleshooting

  • Root element with id X not found — ensure the DOM is ready and #chat-container exists before calling init().
  • Chat is already mounted in container — the container already has rendered content. Call chat.unmount() before re-mounting.
  • 401 / 403 from API — verify your tracker key, that the Synerise JS SDK loaded successfully, and that disableXSRFToken matches your tenant's auth flow.
  • CORS errors — your tenant must allow your origin. Contact Synerise support.
  • Server-side rendering (Next.js, Remix, etc.) — this package is browser-only. Import it inside useEffect or behind a typeof window !== "undefined" guard.
  • TypeScript errors with Slots — the type uses both Preact and React ReactNode in some signatures; if you mix React/Preact in the same TS project, narrow the slot return types explicitly.

Support

For technical and licensing inquiries: [email protected].

License

Proprietary. See LICENSE.