Assistant JS API

Control the Olotalk assistant programmatically from your own JavaScript.

The global Olotalk() function is available on any page where the loader snippet is embedded. Once the loader script has executed, calls made before the assistant bundle finishes booting are queued and replayed automatically.

initauto on load
ready
open ⇄ closestays mounted
destroyunmounts
after destroy, init() mounts a fresh one
init fires automatically on DOMContentLoaded. open and close toggle the dialog while the instance stays mounted. destroy() unmounts it and removes its DOM, after which init() mounts a fresh one.

Command API

Olotalk(command: string, payload?: any): void | Promise<void>

Olotalk('open')

Opens the assistant dialog.

Olotalk('open')

If the assistant is already open, this is a no-op.

You can also open it with a question already in the composer — useful for your own “Ask about this” buttons:

// Ask it straight away
Olotalk('open', { prefill: 'What is included in the Growth plan?' })

// Or put it in the box and let the visitor edit before sending
Olotalk('open', { prefill: selectedText, send: false })

send defaults to sending, because a button labelled with a question is a complete question. Pass send: false when the text came from something the visitor highlighted rather than something they chose to ask — it lets them adjust it first. The prefill applies whether or not the panel was already open.


Olotalk('close')

Closes the assistant dialog. The assistant instance remains mounted — the next open() call restores it.

Olotalk('close')

Olotalk('destroy')

Unmounts the assistant completely and removes all DOM nodes it created. Use this to remove the assistant from a specific page without a full navigation.

await Olotalk('destroy')

Returns a Promise that resolves when unmounting is complete. After destroy(), you can re-init with Olotalk('init').


Olotalk('init', config?)

Manually trigger assistant initialisation. Optionally pass a partial config to override values set by the embed snippet.

// Init with the config from the snippet
Olotalk('init')

// Init with a locale override
Olotalk('init', { locale: 'fr' })

init is called automatically by the loader on DOMContentLoaded. You only need to call it manually if you:

  • set window.OlotalkConfig = { ... } after the loader has already booted, or
  • want to re-init after a destroy() call.

Low-level assistant instance API

If you import @olotalk/assistant directly instead of using the loader, createAssistant() returns an assistant instance with methods and events.

import { createAssistant } from '@olotalk/assistant'

const assistant = createAssistant(document.body, {
  assistantId: 'ast_oltk_7Kd2mQxRv9TbNhLpW3Zsy',
  origin: 'https://api.olotalk.com',
  config: assistantConfig,
  cssUrl: 'https://cdn.jsdelivr.net/npm/@olotalk/assistant@0/dist/olotalk-assistant.css',
})

Instance methods

assistant.open()
assistant.open({ prefill: 'Do you support SSO?' })            // asks it
assistant.open({ prefill: highlighted, send: false })         // holds it for editing
assistant.close()
assistant.isOpen()                                            // true while the panel is showing
assistant.setTheme('dark')
await assistant.setLocale('fr')                               // keeps the conversation
assistant.destroy({ outro: true })

setLocale returns a promise resolving to whether the language changed. Reach for it whenever your own switcher does not reload the page — see Driving the language yourself.

One assistant per page

createAssistant() returns the assistant already mounted for that assistantId if there is one, rather than mounting a second. That way several affordances — a corner bubble and your own “Ask AI” button, say — share one panel and one conversation instead of opening two.

Use getAssistant() when you want to drive the existing one:

import { getAssistant } from '@olotalk/assistant'

document.querySelector('#ask-about-pricing').addEventListener('click', () => {
  getAssistant('ast_oltk_7Kd2mQxRv9TbNhLpW3Zsy')
    ?.open({ prefill: 'What is included in the Growth plan?' })
})

It returns undefined when nothing is mounted for that ID — it never mounts one as a side effect.

Instance events

const unsubscribe = assistant.on('open', () => {
  console.log('Assistant opened')
})

assistant.on('ready', ({ assistantId }) => {
  console.log('Ready', assistantId)
})

assistant.on('send', ({ text }) => {
  console.log('Sent', text)
})

unsubscribe()
EventPayloadWhen it fires
ready{ assistantId: string }Assistant has mounted and is fully interactive
openundefinedAssistant dialog opens
closeundefinedAssistant dialog closes
send{ text: string }User submits a message
cta_click{ id: string; action: ActionType; intent: string }Visitor clicks a CTA button proposed by the assistant
lead_submit{ leadId: string; status: string; score: number }Visitor submits a lead capture form

Where ActionType is "lead_form" | "link" | "escalate_to_human" | "trigger_webhook" | "book_meeting" | "hubspot_create_contact_deal".

The loader-based embed does not expose the internal assistant instance as a public global API. Use the Olotalk() command interface above for script-tag integrations.


Custom element (<olotalk-assistant>)

The ESM build exports OlotalkAssistantElement, a standard Custom Element you can register and use declaratively in any HTML or framework template.

Register

import { defineOlotalkAssistantElement } from '@olotalk/assistant'

defineOlotalkAssistantElement() // registers <olotalk-assistant>

Call once at app startup (e.g. in main.ts). Safe to call multiple times — re-registration is a no-op.

Use

<olotalk-assistant
  assistant-id="ast_oltk_7Kd2mQxRv9TbNhLpW3Zsy"
  origin="https://api.olotalk.com"
  theme="light"
  mode="inline"
  css-url="https://cdn.jsdelivr.net/npm/@olotalk/assistant@0/dist/olotalk-assistant.css"
></olotalk-assistant>

Attributes

AttributeTypeDescription
assistant-idstringAssistant ID (required)
site-idstringLegacy alias for assistant-id
originstringOlotalk API base URL — optional; defaults to the cloud API
theme"light" | "dark" | JSONUI theme — a JSON object for full token override
modeSee Shapes — fourteen valuesWhich shape to mount — see Shapes below
reachstringExtra triggers over the mounted shape, comma-separated: "selection,commandk" — see Combining shapes
selection-answersstringWhere a highlight opens the panel: "beside" (default) beside the words, "in-place" where the assistant lives — see Combining shapes
localestringThe language to render in. Changing it after mount swaps the assistant’s strings in place, keeping the conversation — see Driving the language yourself
placement"floating" | "embedded"Deprecated — use mode. floating means bubble, embedded means inline
css-urlstringAssistant stylesheet URL
configJSON stringFull AssistantConfig object as serialised JSON

Shapes

One assistant, mounted in different places. Every one of them runs the same conversation, the same grounding and the same citations — only the mount differs.

Each row settles three things at once — where the panel sits, what opens it, and what is on the page before anything happens. What a shape decides explains the rule behind the table, including which shapes put no button on the page at all.

modeWhere it sitsHow it opens
bubbleA launcher in the corner, panel floating over the pageThe visitor clicks the launcher. The default
startersCorner launcher, with suggested questions on hoverThe visitor clicks the launcher, or a suggestion
inlineFills the element you mount it in, always openNothing to open — it is part of the page
drawerFull height, flush to the edge, pushing the page aside rather than covering itYour own button — data-olotalk-open, or open()
commandkCentred over a dimmed pageK / CtrlK
cornerCorner panel, nothing on the page at restYour own control, or a reach trigger
centerCentred over a dimmed page, nothing at restYour own control, or a reach trigger
expandOne line in your page that unfolds in place, pushing content downThe visitor presses the line
anchoredBeside one element — a pricing tier, a spec rowYour own affordance, with open({ anchor })
fullpageThe assistant is the pageAlways open; nothing to summon
sidebarA fixed full-height rail at the edgeAlways open; nothing to summon
navfieldNothing of ours — your nav’s own search fieldYour field submits, with open({ prefill })
askbarNothing of ours — your own bar at the foot of the pageYour bar submits, with open({ prefill })
selectionNothing at rest — an offer appears where text is highlightedThe visitor selects a passage and takes the offer

Most of the shapes draw no launcher of their own — the How it opens column above says what opens each one. Wherever it names your own button, field or bar, that is host code calling open(). A drawer takes yours:

import { getAssistant } from '@olotalk/assistant'

document.querySelector('#ask-ai').addEventListener('click', () => {
  getAssistant('ast_oltk_7Kd2mQxRv9TbNhLpW3Zsy')?.open()
})

Installed with the pasted snippet instead? You don’t need imports or a build step — the Olotalk global takes the same calls and queues them until the assistant is ready:

<button onclick="Olotalk('open')">Ask AI</button>

Every open(...) payload on this page travels the same way through the global — Olotalk('open', { anchor, context, prefill, send }) — so each snippet below has a loader-global twin: replace getAssistant(id)?.open(…) with Olotalk('open', …).

commandk binds the shortcut itself, and only when mounted as that shape — no other mode touches that key. Because its resting state is nothing at all, it is undiscoverable unless you advertise it: pair it with a hint in your navigation.

drawer and commandk dim the page behind them, lock its scroll while open, trap focus inside the panel, close on Esc or a click on the dim, and return focus to whatever opened them.

Anchored to an element

anchored opens beside the thing it is about and never covers it — below by preference, above when there is no room, following the element as the page scrolls:

document.querySelector('#growth-tier .ask').addEventListener('click', () => {
  getAssistant(id)?.open({
    anchor: document.querySelector('#growth-tier'),
    context: 'Growth plan',
    prefill: 'What is included in the Growth plan?',
    send: false,
  })
})

On a phone there is no room beside anything, so it becomes a bottom sheet — and because the anchor is no longer visible, the sheet says what it is about. context is that label; without it we fall back to the element’s data-olotalk-context, its aria-label, or its own heading.

From something the visitor highlighted

selection has no resting state at all. When a visitor highlights a passage on your page, a pill appears above it. Taking that offer opens the assistant beside the words — and the passage arrives as a removable quote above the composer, which stays empty for the question they actually want to ask. A highlighted passage is not a question, and asking it verbatim would put words in their mouth; sending combines the quote and their question.

<olotalk-assistant assistant-id="ast_oltk_…" mode="selection"></olotalk-assistant>

You can also raise this over a shape you already mount, so a corner bubble gains it without changing anything else:

data-olotalk-mode="bubble"
data-olotalk-reach="selection"

There is nothing else to wire. It ignores selections inside inputs, textareas and contenteditable regions — someone editing is not someone asking — bounds what it will seed to between a word and a paragraph, and steps aside for a right-click.

On a touch device it offers nothing, deliberately. Selecting text there raises the operating system’s own Copy / Look Up / Share callout, which cannot be styled, moved, or suppressed without disabling selection entirely and taking Copy with it. Competing with it would be worse than not offering, so on a phone this shape is silent and any other mount you have keeps working normally.

Fields you already have

navfield and askbar render nothing of their own. The field is yours — in your navigation, or a bar across the foot of a doc — and submitting it opens the assistant with the question already asked:

form.addEventListener('submit', (e) => {
  e.preventDefault()
  getAssistant(id)?.open({ prefill: input.value })
})

That is the entire integration. It works with any markup you already have, which is the point: the shape is your field, not our rendering of one.

Combining shapes

A page mounts one shape — that is where the conversation lives — but it can be summoned more than one way:

  • From your own UI. Any button, field or link can call getAssistant(id).open({ prefill }), whatever shape is mounted. A nav field beside a corner bubble is two lines of your code.
  • From ours, with reach. Name extra triggers over the mounted shape:
<script
  src="https://cdn.jsdelivr.net/npm/@olotalk/assistant-loader@0/dist/loader.iife.js"
  data-olotalk-assistant-id="ast_oltk_7Kd2mQxRv9TbNhLpW3Zsy"
  data-olotalk-mode="bubble"
  data-olotalk-reach="selection,commandk"
  async
></script>

That is a corner bubble that also opens when a visitor highlights a passage — beside the highlight, with the passage quoted above an empty composer — and when they press K. Reach accepts selection and commandk; unknown ids are ignored with a console warning. However many ways in you add, there is still exactly one panel, one conversation and one session — reach adds doors, not rooms.

mode="selection" and reach="selection" are not the same thing

The word does double duty, and it is the easiest thing to misread here:

  • data-olotalk-mode="selection" — select-to-ask is the assistant. Nothing sits on the page at rest, there is no button anywhere, and highlighting text is the only way in.
  • data-olotalk-mode="bubble" data-olotalk-reach="selection" — the corner bubble is the assistant and highlighting is an extra way into it. The launcher is still there, because that is what a bubble is.

If you want select-to-ask and no button on the page, that is the first one, on its own.

Keeping the panel where it lives

By default a highlight brings the panel to the words — the answer arrives where the question was asked, even for a corner bubble. If your assistant is a fixture your visitors have already located, you may prefer it to stay put:

data-olotalk-mode="bubble"
data-olotalk-reach="selection"
data-olotalk-selection-answers="in-place"

The panel then opens where the assistant lives, still with the passage quoted above an empty composer. beside is the default; anything else is ignored with a console warning. This affects the selection trigger only — your own open({ anchor }) calls are untouched.

On phones

You do not choose a mobile shape. At phone width the corner, edge, centred and anchored mounts all become the same bottom sheet — dimmed page behind it, a grab handle that drags to dismiss, partial height for a quick answer and most of the screen for a real conversation — and inline stays in flow where you put it. One embed, both presentations.

Methods and properties

const el = document.querySelector('olotalk-assistant')

el.open()
el.open({ prefill: 'Do you support SSO?' })
el.close()
await el.destroy()

// Set config/theme programmatically (triggers rebuild)
el.config = { ...assistantConfig }
el.theme = 'dark'

// Language: swapped in place, no rebuild — the conversation survives
el.setAttribute('locale', 'fr')

The element rebuilds automatically when observed attributes change — except theme and locale, which apply without one.


Adaptive language (after first turn)

When a visitor’s first message is in a different supported language than the current chrome, the assistant automatically:

  1. Replies in the detected language (the bot has always done this)
  2. Renders an inline prompt above the input: Continue in {language}? · Yes / No
  3. On Yes: re-fetches the assistant config in the new locale, swaps the dictionary + suggested-question chips, and persists the choice in localStorage under olotalk:lang:{assistantId}
  4. On No: locks the current locale into the session so the prompt won’t fire again

Returning visitors mount directly in their previously-accepted language. There is no API to drive the prompt programmatically — it’s a passive UX affordance that fires on the API’s per-turn detection.

To bypass the prompt entirely, pin the locale via data-olotalk-locale. An explicit override beats every auto-detection signal, so the prompt won’t fire and the chrome stays on the chosen locale.

Driving the language yourself

If your site has its own language picker, tell the assistant when the visitor uses it:

Olotalk('locale', 'fr')                    // script-tag embed
getAssistant(id).setLocale('fr')           // direct import
el.setAttribute('locale', 'fr')            // custom element

This swaps the chrome, the text direction and the suggested questions in place — the open conversation is kept — and tells the server, so the answers change language too, not just the panel around them. Reach for it whenever your language switcher does not reload the page, which is every single-page app: the assistant resolves its language once, at mount, from data-olotalk-locale, then the visitor’s previously-accepted choice, then <html lang>, then the browser. Nothing re-reads those afterwards, so without this call the panel goes on answering in the language the visitor just left.

Do not use Olotalk('init', { locale }) for this. init rebuilds the assistant, which throws away whatever the visitor was in the middle of asking.

There is a trade-off worth knowing: pinning a language — whether at mount with data-olotalk-locale or afterwards with this call — is an explicit instruction, so it outranks the visitor’s own signals. The adaptive prompt above will not fire while one is set, because you have already answered the question it asks. If you would rather let visitors choose for themselves, don’t pin a locale; the assistant follows <html lang> on each page load and offers the switch when someone writes in another language.

The set of supported languages is configured per-assistant on the Source step of the assistant builder — open your assistant from the Assistants nav, then expand Ingestion status, coverage & languages. Auto-detection is bounded by this set: a language outside it is ignored both for the initial chrome resolution and for the adaptive prompt.


Triggering the assistant from a custom button

A common pattern is to use your own button instead of the default floating launcher. Mount mode="corner": it is the same corner panel as bubble, but with nothing on the page at rest. Then make any button a door with data-olotalk-open.

<button data-olotalk-open>Chat with us</button>

<script
  src="https://cdn.jsdelivr.net/npm/@olotalk/assistant-loader@0/dist/loader.iife.js"
  data-olotalk-assistant-id="ast_oltk_7Kd2mQxRv9TbNhLpW3Zsy"
  data-olotalk-mode="corner"
  async
></script>

If you keep the default bubble mode, your button is an extra way in; it does not remove the built-in launcher. The launcher lives inside the assistant’s shadow root and is not meant to be hidden with page CSS.


TypeScript types

If you use TypeScript, declare the global to get type-checking:

// global.d.ts
declare global {
  interface Window {
    Olotalk?: (command: 'open' | 'close' | 'destroy' | 'init' | 'locale', payload?: any) => void | Promise<void>
    OlotalkConfig?: {
      assistantId?: string
      siteId?: string
      origin?: string
      locale?: string
      theme?: 'light' | 'dark' | Record<string, string>
      mode?: string                  // which shape — see Shapes above
      reach?: string                 // extra triggers, e.g. 'selection,commandk'
      selectionAnswers?: 'beside' | 'in-place'
      placement?: 'floating' | 'embedded'   // deprecated — use mode
      mount?: string
      cdnBase?: string
      version?: string
      cssUrl?: string
    }
    // Note: conversion / lead-capture / CTA rules are configured server-side
    // (dashboard Action policy), NOT via this client config object.
  }
}
export {}

Before the assistant boots

Do not call Olotalk() from a script that runs before the loader script itself has executed. Once the loader is on the page, commands are queued automatically until the assistant is ready, so click handlers and later page scripts are safe:

// Safe after the loader script has executed
Olotalk('open')