Skip to content

Frameworks

The React kit (<HelpAI />, useHelpAI()), Next.js, Remix, Vue, Nuxt, Svelte, Astro, Angular, WordPress, Shopify, Webflow.

Last updated

Copy-paste recipes for every major frontend framework + CMS.


Contents#


Next.js#

The React kit, @helpai/elements/react, is server-render safe and already marked 'use client', so it drops straight into a layout. Nothing touches the page until it runs in the browser.

App Router (13+):

tsx
// app/layout.tsx
import { HelpAI } from '@helpai/elements/react';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang='en'>
      <body>
        {children}
        <HelpAI publicKey={process.env.NEXT_PUBLIC_HELPAI_KEY!} aiAgentDeploymentId={process.env.NEXT_PUBLIC_HELPAI_DEPLOYMENT_ID!} theme='host' />
      </body>
    </html>
  );
}
bash
# .env.local
NEXT_PUBLIC_HELPAI_KEY=agent_pk_abc123...
NEXT_PUBLIC_HELPAI_DEPLOYMENT_ID=01900000-0000-7000-8000-000000000000

A server component can only pass serializable props, so callbacks (onOpen, onMessage, …) go on a <HelpAI /> rendered from a 'use client' component of your own. useHelpAI() (below) works in any client component.

Pages Router: render <HelpAI /> in pages/_app.tsx the same way; no dynamic(..., { ssr: false }) needed.


React#

The React kit (React 18+) mounts the widget with <HelpAI /> and drives it from any component with useHelpAI():

tsx
import { HelpAI, useHelpAI } from '@helpai/elements/react';

export function Root() {
  return (
    <>
      <main>{/* your app */}</main>
      <HelpAI publicKey={import.meta.env.VITE_HELPAI_KEY} aiAgentDeploymentId={import.meta.env.VITE_HELPAI_DEPLOYMENT_ID} theme='host' />
    </>
  );
}

function PricingHelp() {
  const { ask, open, state } = useHelpAI();
  return (
    <>
      <button onClick={() => ask('Which plan fits a team of 10?')}>Ask about plans</button>
      <button onClick={() => open({ to: 'help' })}>Help center {state.unread > 0 && `(${state.unread})`}</button>
    </>
  );
}
  • <HelpAI {...options} /> takes every widget option as a prop; aiAgentDeploymentId is required. It renders nothing itself: the widget mounts once the page is in the browser, a prop change re-configures the live widget without a remount, and unmounting removes it. Callback props (onOpen, onMessage, …) can be new functions on every render; the widget always calls the latest one.
  • useHelpAI(widgetId?) returns { state, ask, open, close, reset, track }. state is { open, ready, unread, conversationId? } and re-renders the component when it changes; the server render sees the initial state. The commands are stable across renders and do nothing until the widget has mounted (state.ready). ask(text) fills the composer; ask(text, { send: true }) sends it. Call reset() when your user logs out.
  • One import path. The kit carries the whole widget API (getInstance, hook, the types), so import everything from @helpai/elements/react; the package's other entries are separate copies with their own widgets.
  • Two widgets on one page: give each <HelpAI widgetId='support' … /> and read it with useHelpAI('support').
  • Page tools: useHelpAITool(tool) gives the agent a tool while the component is mounted, or pass <HelpAI tools={[...]} /> (Page tools). execute can be a new function on every render; the agent's call runs the latest one, and only a changed definition registers or configures anything again.
  • Signed-in users: pass userToken, a function that fetches the token your server signs (Verified users). Keep it stable for one user (a module function, or useCallback keyed on your user id): a different function signs in afresh, and dropping it signs out.

The kit bundles the widget and imports only React from your app, so you don't need Preact installed.

React 19 — use the web component directly#

React 19+ supports custom elements natively:

tsx
// No JS bundle import needed — add a <script> tag or use the web-component sub-entry
import '@helpai/elements/web-component';

function Chat() {
  return <web-ai-chat public-key={import.meta.env.VITE_HELPAI_KEY} ai-agent-deployment-id={import.meta.env.VITE_HELPAI_DEPLOYMENT_ID} theme='auto' />;
}

TypeScript typings#

import type {} from '@helpai/elements/jsx/react'; once (in your entry file or a .d.ts) registers <web-ai-chat> in React 19's JSX: every attribute the element reads autocompletes, ai-agent-deployment-id is required, and a ref is the typed element with ask, open, state, ready and reset. Vue and Svelte have @helpai/elements/jsx/vue and @helpai/elements/jsx/svelte (JSX typings).

tsx
import { useRef } from 'react';

import type {} from '@helpai/elements/jsx/react';

function Chat() {
  const chat = useRef<HTMLElementTagNameMap['web-ai-chat']>(null);
  return (
    <>
      <button onClick={() => chat.current?.ask('Which plan fits a team of 10?')}>Ask about plans</button>
      <web-ai-chat ref={chat} public-key='agent_pk_...' ai-agent-deployment-id='...' theme='host' />
    </>
  );
}

Vue 3#

Composition API:

vue
<!-- components/Chat.vue -->
<script setup lang="ts">
import { onMounted, onBeforeUnmount } from 'vue';
import { init, type ChatWidgetInstance } from '@helpai/elements';

let instance: ChatWidgetInstance | null = null;

onMounted(() => {
  instance = init({
    publicKey: import.meta.env.VITE_HELPAI_KEY,
    aiAgentDeploymentId: import.meta.env.VITE_HELPAI_DEPLOYMENT_ID,
  });
});

onBeforeUnmount(() => {
  instance?.destroy();
});
</script>

<template>
  <!-- Nothing to render — the widget mounts its own DOM -->
</template>
vue
<!-- App.vue -->
<script setup lang="ts">
import Chat from './components/Chat.vue';
</script>

<template>
  <main>
    <!-- your app -->
  </main>
  <Chat />
</template>

Web component approach#

Vue 3 supports custom elements natively. Register the web-component sub-entry, then use the tag:

ts
// main.ts
import { createApp } from 'vue';
import '@helpai/elements/web-component';

import App from './App.vue';

const app = createApp(App);
// Tell Vue to treat `web-ai-chat` as a custom element (not a Vue component)
app.config.compilerOptions.isCustomElement = (tag) => tag === 'web-ai-chat';
app.mount('#app');
vue
<template>
  <web-ai-chat public-key="agent_pk_..." ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" theme="auto" />
</template>

Nuxt 3#

ts
// plugins/web-ai-chat.client.ts
import { init } from '@helpai/elements';

export default defineNuxtPlugin((nuxtApp) => {
  const chat = init({
    publicKey: useRuntimeConfig().public.helpaiKey,
    aiAgentDeploymentId: useRuntimeConfig().public.helpaiDeploymentId,
  });

  nuxtApp.hook('app:beforeMount', () => {
    // Widget already mounted by init(). This hook is just a reference.
  });

  // Clean up on app tear-down (rarely triggered in SPA navigation)
  nuxtApp.hook('app:suspense:resolve', () => {
    // No-op — widget stays across navigations
  });

  return {
    provide: {
      chat,
    },
  };
});
ts
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      helpaiKey: process.env.NUXT_PUBLIC_HELPAI_KEY,
      helpaiDeploymentId: process.env.NUXT_PUBLIC_HELPAI_DEPLOYMENT_ID,
    },
  },
});

Note the .client.ts suffix — ensures the plugin only runs in the browser.


Svelte / SvelteKit#

SvelteKit:

svelte
<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  import { browser } from '$app/environment';
  import { init, type ChatWidgetInstance } from '@helpai/elements';
  import { PUBLIC_HELPAI_DEPLOYMENT_ID, PUBLIC_HELPAI_KEY } from '$env/static/public';

  let instance: ChatWidgetInstance | null = null;

  onMount(() => {
    if (!browser) return;
    instance = init({ publicKey: PUBLIC_HELPAI_KEY, aiAgentDeploymentId: PUBLIC_HELPAI_DEPLOYMENT_ID });
  });

  onDestroy(() => {
    instance?.destroy();
  });
</script>

<slot />

Plain Svelte (no SvelteKit):

svelte
<!-- App.svelte -->
<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  import { init, type ChatWidgetInstance } from '@helpai/elements';

  let instance: ChatWidgetInstance | null = null;

  onMount(() => {
    instance = init({ publicKey: import.meta.env.VITE_HELPAI_KEY, aiAgentDeploymentId: import.meta.env.VITE_HELPAI_DEPLOYMENT_ID });
  });

  onDestroy(() => {
    instance?.destroy();
  });
</script>

<main>
  <!-- your app -->
</main>

Web component in Svelte#

Svelte handles custom elements natively:

svelte
<script lang="ts">
  import '@helpai/elements/web-component';
</script>

<web-ai-chat public-key="agent_pk_..." ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" theme="auto" />

Astro#

Astro is SSR-first; wrap the widget in a client directive:

---
// src/components/Chat.astro
const publicKey = import.meta.env.PUBLIC_HELPAI_KEY;
const aiAgentDeploymentId = import.meta.env.PUBLIC_HELPAI_DEPLOYMENT_ID;
---

<script define:vars={{ publicKey, aiAgentDeploymentId }}>
  import('@helpai/elements').then(({ init }) => {
    init({ publicKey, aiAgentDeploymentId });
  });
</script>
---
// src/layouts/Layout.astro
import Chat from '../components/Chat.astro';
---
<html>
  <body>
    <slot />
    <Chat />
  </body>
</html>

Or use the web component (even simpler — no JS needed):

---
import '@helpai/elements/web-component';
---

<web-ai-chat public-key={Astro.env.PUBLIC_HELPAI_KEY} ai-agent-deployment-id={Astro.env.PUBLIC_HELPAI_DEPLOYMENT_ID} />

Remix#

Remix (and React Router 7) render on the server, which the React kit handles: pass the keys from the loader.

tsx
// app/root.tsx
import { HelpAI } from '@helpai/elements/react';
import { Outlet, useLoaderData } from '@remix-run/react';

export async function loader() {
  return {
    helpaiKey: process.env.HELPAI_PUBLIC_KEY ?? '',
    helpaiDeploymentId: process.env.HELPAI_DEPLOYMENT_ID ?? '',
  };
}

export default function App() {
  const { helpaiKey, helpaiDeploymentId } = useLoaderData<typeof loader>();

  return (
    <html>
      <body>
        <Outlet />
        {helpaiKey && helpaiDeploymentId && <HelpAI publicKey={helpaiKey} aiAgentDeploymentId={helpaiDeploymentId} />}
      </body>
    </html>
  );
}

SolidJS#

tsx
import { init, type ChatWidgetInstance } from '@helpai/elements';
import { onMount, onCleanup } from 'solid-js';

export function Chat() {
  let instance: ChatWidgetInstance | null = null;

  onMount(() => {
    instance = init({ publicKey: import.meta.env.VITE_HELPAI_KEY, aiAgentDeploymentId: import.meta.env.VITE_HELPAI_DEPLOYMENT_ID });
  });

  onCleanup(() => {
    instance?.destroy();
  });

  return null;
}

Solid also supports custom elements natively:

tsx
import '@helpai/elements/web-component';

export default function App() {
  return <web-ai-chat public-key='agent_pk_...' ai-agent-deployment-id='01900000-0000-7000-8000-000000000000' />;
}

Angular#

Angular needs CUSTOM_ELEMENTS_SCHEMA to accept the custom tag:

ts
// src/app/app.module.ts
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import '@helpai/elements/web-component';

@NgModule({
  // ...
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}
html
<!-- app.component.html -->
<web-ai-chat public-key="agent_pk_..." ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" theme="auto"></web-ai-chat>

For programmatic control:

ts
// chat.service.ts
import { Injectable } from '@angular/core';
import { init, type ChatWidgetInstance } from '@helpai/elements';

import { environment } from '../environments/environment';

@Injectable({ providedIn: 'root' })
export class ChatService {
  private instance: ChatWidgetInstance | null = null;

  mount() {
    if (!this.instance) {
      this.instance = init({ publicKey: environment.helpaiKey, aiAgentDeploymentId: environment.helpaiDeploymentId });
    }
  }

  destroy() {
    this.instance?.destroy();
    this.instance = null;
  }

  open() {
    this.instance?.open();
  }
}

WordPress#

Add the snippet via the Theme Header/Footer plugin OR directly in your theme's header.php:

html
<!-- Before </head> -->
<script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js" data-public-key="agent_pk_abc123..." data-ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" defer></script>

Via a plugin#

Any "Insert Headers and Footers" plugin works:

  1. Install the plugin (e.g., "Insert Headers and Footers" by WPBeginner).
  2. Settings → Insert Headers and Footers.
  3. Paste the script tag into the "Scripts in Header" box.
  4. Save.

Via functions.php#

php
<?php
// Add to your theme's functions.php
function helpai_enqueue_chat() {
  ?>
  <script
    src="https://cdn.help.ai/sdk/js/elements/latest/elements.js"
    data-public-key="agent_pk_abc123..."
    data-ai-agent-deployment-id="01900000-0000-7000-8000-000000000000"
    defer
  ></script>
  <?php
}
add_action('wp_head', 'helpai_enqueue_chat');

Personalization for logged-in users#

php
<?php
function helpai_enqueue_chat_personalized() {
  $user = wp_get_current_user();
  $attrs = 'data-public-key="agent_pk_..." data-ai-agent-deployment-id="01900000-0000-7000-8000-000000000000"';
  if ($user->ID) {
    $attrs .= ' data-user-id="' . esc_attr($user->ID) . '"';
    $attrs .= ' data-user-email="' . esc_attr($user->user_email) . '"';
    $attrs .= ' data-user-name="' . esc_attr($user->display_name) . '"';
    $attrs .= ' data-user-context=\'' . esc_attr(json_encode(array("plan" => "pro"))) . '\'';
  }
  ?>
  <script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js" <?php echo $attrs; ?> defer></script>
  <?php
}
add_action('wp_head', 'helpai_enqueue_chat_personalized');

Note: data-user-* / data-page-* are host-asserted context for the AI, never an auth credential — the browser deployment is selected by data-public-key + data-ai-agent-deployment-id. Explicit scalar attributes win over keys in the data-user-context / data-page-context JSON.


Shopify#

Shopify has a "custom pixels" and "additional scripts" workflow. Easiest path:

  1. In your admin: Online Store → Themes → Edit code.
  2. Open theme.liquid.
  3. Paste before </head>:
html
<script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js" data-public-key="agent_pk_..." data-ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" defer></script>

With Shopify customer data#

html
{% if customer %}
<script
  src="https://cdn.help.ai/sdk/js/elements/latest/elements.js"
  data-public-key="agent_pk_..."
  data-ai-agent-deployment-id="01900000-0000-7000-8000-000000000000"
  data-user-id="{{ customer.id }}"
  data-user-email="{{ customer.email }}"
  data-user-name="{{ customer.first_name }} {{ customer.last_name }}"
  data-page-area="orders"
  defer
></script>
{% else %}
<script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js" data-public-key="agent_pk_..." data-ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" defer></script>
{% endif %}

Webflow#

Webflow lets you add site-wide custom code:

  1. Site settings → Custom code → Head code.
  2. Paste:
html
<script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js" data-public-key="agent_pk_..." data-ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" defer></script>
  1. Save + publish.

Page-specific customization: open the page → Page settings → Inside <head> tag and paste a page-specific snippet.


Squarespace / Wix#

Squarespace:

  1. Settings → Advanced → Code Injection.
  2. Paste the script tag into the Header section.

Wix:

  1. Settings → Custom Code.
  2. Add new snippet.
  3. Paste + choose "All pages" + "Head" placement.

Framework-agnostic troubleshooting#

The widget doesn't appear in SSR previews — that's expected. The widget is client-only. It appears after hydration / mount.

The widget flashes open then closes on SPA navigation — you're probably calling destroy() then init() on every route change. Use init() alone — it's idempotent. If there's an existing instance at the same widgetId, it merges the new options in.

Hot module replacement breaks the widget — Next.js / Vite HMR sometimes mounts duplicate instances. Always pair your useEffect / onMounted with a cleanup that calls destroy().

CSP blocks the widget — the widget inlines styles inside a shadow root (requires style-src 'unsafe-inline' or a nonce pattern). See Security & privacy § CSP.

More: Troubleshooting.


Next#

Still need help?

Ask the community