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.
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()
| Event | Payload | When it fires |
|---|---|---|
ready | { assistantId: string } | Assistant has mounted and is fully interactive |
open | undefined | Assistant dialog opens |
close | undefined | Assistant 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
| Attribute | Type | Description |
|---|---|---|
assistant-id | string | Assistant ID (required) |
site-id | string | Legacy alias for assistant-id |
origin | string | Olotalk API base URL — optional; defaults to the cloud API |
theme | "light" | "dark" | JSON | UI theme — a JSON object for full token override |
mode | See Shapes — fourteen values | Which shape to mount — see Shapes below |
reach | string | Extra triggers over the mounted shape, comma-separated: "selection,commandk" — see Combining shapes |
selection-answers | string | Where a highlight opens the panel: "beside" (default) beside the words, "in-place" where the assistant lives — see Combining shapes |
locale | string | The 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-url | string | Assistant stylesheet URL |
config | JSON string | Full 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.
mode | Where it sits | How it opens |
|---|---|---|
bubble | A launcher in the corner, panel floating over the page | The visitor clicks the launcher. The default |
starters | Corner launcher, with suggested questions on hover | The visitor clicks the launcher, or a suggestion |
inline | Fills the element you mount it in, always open | Nothing to open — it is part of the page |
drawer | Full height, flush to the edge, pushing the page aside rather than covering it | Your own button — data-olotalk-open, or open() |
commandk | Centred over a dimmed page | ⌘K / CtrlK |
corner | Corner panel, nothing on the page at rest | Your own control, or a reach trigger |
center | Centred over a dimmed page, nothing at rest | Your own control, or a reach trigger |
expand | One line in your page that unfolds in place, pushing content down | The visitor presses the line |
anchored | Beside one element — a pricing tier, a spec row | Your own affordance, with open({ anchor }) |
fullpage | The assistant is the page | Always open; nothing to summon |
sidebar | A fixed full-height rail at the edge | Always open; nothing to summon |
navfield | Nothing of ours — your nav’s own search field | Your field submits, with open({ prefill }) |
askbar | Nothing of ours — your own bar at the foot of the page | Your bar submits, with open({ prefill }) |
selection | Nothing at rest — an offer appears where text is highlighted | The 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:
- Replies in the detected language (the bot has always done this)
- Renders an inline prompt above the input:
Continue in {language}? · Yes / No - On Yes: re-fetches the assistant config in the new locale, swaps the dictionary + suggested-question chips, and persists the choice in
localStorageunderolotalk:lang:{assistantId} - 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')