@hachther/akilio-widget
v1.0.0
Published
Embeddable React AI assistant widget for Akilio backend.
Readme
Akilio Widget
Style isolation
The widget renders in a Shadow DOM by default. Host-site resets and framework styles cannot override its buttons, text areas, images, or layout, and widget styles do not leak into the surrounding page.
<AkilioWidget
apiBaseUrl='https://app.akilio-ai.com'
projectKey='your-project-key'
styleIsolation='shadow'
/>Use styleIsolation="scoped" for a normal-DOM integration with Akilio-prefixed
CSS, or styleIsolation="none" when loading @hachther/akilio-widget/style.css
yourself. Standalone scripts accept data-style-isolation="shadow"; Shadow DOM
is also their default.
Streaming responses
The widget uses the authenticated SSE endpoint by default. It displays retrieval
and answer-preparation status, appends answer.delta content as it arrives, and
then replaces the transient answer with the canonical final payload (including
citations, products, and suggestions).
<AkilioWidget
apiBaseUrl='https://app.akilio-ai.com'
projectKey='your-project-key'
streaming
/>Use streaming={false} or data-streaming="false" to use POST /api/v1/chat/
for compatibility with an older backend or proxy.
Response notification sound
An optional dependency-free chime can notify visitors when the final response arrives, including while the browser tab is in the background:
<AkilioWidget
projectKey='your-project-key'
enableResponseSound
responseSoundVolume={0.22}
/>Standalone scripts use data-enable-response-sound="true" and optionally
data-response-sound-volume="0.22". The sound is disabled by default. It is
prepared when the visitor sends a question to comply with browser autoplay
rules, plays only for successful final responses, downloads no audio file, and
never blocks message delivery if audio is unavailable or denied.
Launcher position
The built-in launcher defaults to the bottom-right corner. It can also be placed at the bottom-left or displayed as a vertical tab in the middle of the right viewport edge:
<AkilioWidget projectKey='your-project-key' launcherPosition='right-middle' />Supported values are bottom-right, bottom-left, and right-middle. The
right-middle position automatically opens a full-height right drawer on
desktop, even when the configured mode is floating. On mobile it returns to
the compact bottom-right launcher and the normal mobile presentation.
The standalone equivalent is data-launcher-position="right-middle".
Adaptive launcher
The built-in launcher defaults to Akilio’s compact adaptive experience: it appears as an icon, expands once with a page-aware invitation, then stays quiet. It can show a single answer-ready signal when a response completes while chat is closed. Discovery and dismissal are remembered per project, and motion is automatically reduced for visitors who request it.
Use the original persistent label while validating the new experience:
<AkilioWidget launcherExperience='static' />Standalone scripts use data-launcher-experience="static".
Page-end contextual prompt
Documentation pages can show a compact “Ask about this page” input when the reader approaches the end of the document:
<AkilioWidget
projectKey='your-project-key'
contextualPrompt={{
enabled: true,
trigger: 'near-page-end',
threshold: 320,
placeholder: 'Ask a question about this page...',
dismissible: true,
}}
/>Submitting opens the main chat and sends the question through its normal
authenticated streaming flow with the current page URL and title. The prompt
does not steal focus, stays hidden while chat is open, supports Escape and a
dismiss button, and does not appear before required privacy consent is granted.
It uses an IntersectionObserver end marker with a scroll fallback.
Standalone attributes are data-contextual-prompt="true",
data-contextual-prompt-threshold="320",
data-contextual-prompt-placeholder="Ask about this page...", and
data-contextual-prompt-dismissible="true".
Server widget policy
The public project configuration may include an authoritative widget_policy.
The widget applies it automatically:
availabledisables questions while the project is unavailable.assistant_modeis sent to chat instead of a client routing hint.commerce_enabledcontrols product rendering and commerce event tracking.brandingenforces required, removable, or white-label presentation.
For projects with branding: "removable", the host can hide Akilio branding:
<AkilioWidget showAkilioBranding={false} />The standalone equivalent is data-show-akilio-branding="false". A policy of
akilio_required always shows Akilio branding, while white_label always hides
it regardless of the client option. Older API responses without
widget_policy retain the previous widget behavior.
The message composer automatically expands for multiline text up to its bounded maximum height, after which it scrolls internally.
When branding is required, a compact “Powered by Akilio” notice is displayed below the composer at the bottom of the widget. It is omitted for removable and white-label policies.
The default widget primary color follows the Akilio palette (#0F766E) with
#115E59 for hover and pressed states. Project configuration and the theme
prop continue to override these defaults; theme.primaryDark can customize the
interactive hover color.
Floating, drawer, side-panel, and hybrid Shadow DOM widgets use the browser top layer when supported. This keeps the widget above host-site chat, search, cookie, and navigation overlays—even when those elements activate after the composer receives focus. Older browsers use a fixed, isolated host at the maximum safe z-index. Inline widgets remain in the normal document layout.
Browser control API
After the widget fires akilio:ready, integrations can control the real widget
without querying its DOM. The frozen API works with normal DOM and Shadow DOM:
await window.AkilioWidget.open();
await window.AkilioWidget.typeDraft('How can I request a quote?', {
interval: 35,
focus: true,
clearExisting: true,
});
// Let the visitor click Send, or submit through the same SSE workflow:
const response = await window.AkilioWidget.submit();Available methods are open, close, setDraft, typeDraft, clearDraft,
cancelTyping, submit, ask, reset, and getState. Rejected operations
include a stable code such as EMPTY_DRAFT, BUSY, TYPING_CANCELLED, or
STREAM_FAILED. getState() returns only safe UI and conversation state and
never exposes authentication data.
Lifecycle events are dispatched on window: akilio:ready, akilio:open,
akilio:close, akilio:draft-change, akilio:typing-start,
akilio:typing-complete, akilio:typing-cancel, akilio:question,
akilio:answer-start, akilio:answer-delta, akilio:answer-complete,
akilio:error, and akilio:reset.
Launcher coachmark
The default launcher includes a dependency-free, one-time discovery coachmark. It appears after a short delay, never submits or opens the widget automatically, and is dismissed when the visitor closes it, opens the widget, or presses Escape. Dismissal is stored per project and tour version.
<AkilioWidget enableTour tourDelayMs={3000} tourVersion='v1' />Standalone attributes are data-enable-tour, data-tour-delay-ms, and
data-tour-version. Set enableTour={false} to disable it. To replay the
coachmark programmatically, dispatch:
window.dispatchEvent(new Event('akilio:start-tour'));The coachmark stays inside the widget’s style boundary, does not darken or block the host page, does not steal keyboard focus, and respects reduced-motion preferences.
Capability-driven chat actions
The widget reads capabilities.chat from the public project configuration. An
enabled summarize_page intent that supports the widget surface is shown in
an action tray immediately above the composer when its page.url requirement
is available. Selecting it sends intent: "summarize_page" and the current
page context to the chat endpoint. Unsupported, disabled, wrong-surface, or
unmet-requirement actions are not displayed. The action tray remains available
throughout the conversation and is temporarily hidden while a response streams.
Older API responses without the capability catalog retain the previous
suggestion behavior.
An enabled contact intent adds a “Contact the team” action. Contact responses
can display validated email, phone, and form links from contact_options.
When the response includes a start_contact_request action and public
configuration enables capabilities.chat.contact_request, the widget renders
the advertised fields as an inline contact form. It applies the server-provided
consent text, submits through the authenticated /contact-requests/ endpoint,
prevents submission without required consent, and displays success, progress,
failure, dark-mode, and mobile states. Repeating the source message remains
idempotent on the API.
Production React and standalone JavaScript widget for the Akilio backend.
Included modes
floating: compact chat popover anchored above the floating launcher.side-panel: persistent desktop side panel with a mobile drawer fallback.inline: renders inside the supplied container.hybrid: backward-compatible alias for side panel on desktop and drawer on mobile.drawer: full-height overlay drawer opened from the floating launcher.
Build
npm install
npm run typecheck
npm run buildProduction files:
dist/index.min.js
dist/index.umd.min.cjs
dist/index.d.ts
dist/style.css
dist/loader.js
dist/widget-manifest.json
dist/releases/0.7.0/akilio-widget.min.js
dist/akilio-widget.min.js
dist/_headersReact
import { AkilioWidget } from '@hachther/akilio-widget';
import '@hachther/akilio-widget/style.css';
<AkilioWidget
apiBaseUrl='https://api.akilio-ai.com'
projectKey='ask_pk_xxx'
mode='floating'
colorMode='system'
auth={{
getAccessToken: async () => sessionStorage.getItem('akilio_access_token'),
}}
requireConsent
privacyNotice='Questions are processed to provide an AI-generated answer.'
privacyNoticeUrl='https://example.com/privacy'
/>;Custom launcher and controlled state
Hide the built-in launcher and control the floating popup from any host element, such as a header navigation button:
const [chatOpen, setChatOpen] = useState(false);
<button onClick={() => setChatOpen(true)}>Ask Akilio</button>
<AkilioWidget
apiBaseUrl='https://api.akilio-ai.com'
projectKey='ask_pk_xxx'
mode='floating'
launcher='hidden'
open={chatOpen}
onOpenChange={setChatOpen}
/>Or supply a custom React launcher:
<AkilioWidget
apiBaseUrl='https://api.akilio-ai.com'
projectKey='ask_pk_xxx'
mode='floating'
launcher={({ open, toggleChat }) => (
<button className='header-assistant' onClick={toggleChat}>
{open ? 'Close assistant' : 'Ask Akilio'}
</button>
)}
/>Authentication supports:
auth={{ accessToken: 'short-lived-jwt' }}
auth={{ getAccessToken: refreshAndReturnToken }}
auth={{ apiKey: 'widget-key', apiKeyHeader: 'X-Akilio-Key' }}
auth={{ credentials: 'include' }}The token resolver is evaluated for every request, so a host application can rotate short-lived backend tokens without remounting the widget.
Standalone script
For production websites, use the stable loader URL:
<script
data-akilio-widget
data-cfasync="false"
src="https://cdn.example.com/widget/loader.js"
data-api-base-url="https://api.akilio-ai.com"
data-project-key="ask_pk_xxx"
data-mode="floating"
data-color-mode="system"
data-access-token="short-lived-widget-token"
data-require-consent="true"
data-privacy-notice="Questions are processed to provide an AI-generated answer."
data-privacy-notice-url="https://example.com/privacy"
></script>For an inline widget:
<div id="support-assistant" style="height: 680px"></div>
<script
src="https://cdn.example.com/widget/loader.js"
data-akilio-widget
data-auto-init="false"
></script>
<script>
window.addEventListener('akilio:loader-ready', () => {
window.Akilio.mount({
target: '#support-assistant',
apiBaseUrl: 'https://api.akilio-ai.com',
projectKey: 'ask_pk_xxx',
mode: 'inline',
colorMode: 'system',
accessToken: 'short-lived-widget-token',
});
});
</script>loader.js is revalidated by the browser and loads the immutable release file
for the current package version. Deploy the complete dist directory without
deleting earlier dist/releases/* versions. Each build also keeps
akilio-widget.min.js as a non-immutable compatibility file.
For optional Cloudflare Pages deployments, the generated dist/_headers requires browser and edge
revalidation for loader.js and widget-manifest.json. Files under
releases/* receive a one-year immutable browser and edge cache. Configure
Cloudflare Browser Cache TTL to Respect Existing Headers for this path so a
cache rule does not keep the loader stale.
Cloudflare R2 does not use _headers; the R2 deployment command writes the
equivalent Cache-Control metadata on each uploaded object.
Publishing a new package version updates only the loader and manifest pointers; existing embed snippets do not change and a manual cache purge is not normally required. The manifest includes the active version, release path, byte size, and SHA-256 digest for deployment verification.
Deploy to Cloudflare R2
Set the R2 bucket name in your local .env or shell environment:
CLOUDFLARE_R2_BUCKET=your-r2-bucket
# Optional folder within the bucket:
CLOUDFLARE_R2_PREFIX=widgetThen validate, build, and publish the CDN assets with:
npm run deploy:r2You can also provide the target without storing it in .env:
npm run deploy:r2 -- --bucket your-r2-bucket --prefix widgetWrangler uses an existing local Cloudflare login. In CI, provide
CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID as protected environment
variables. Never commit a Cloudflare API token.
The deploy command uploads the immutable versioned bundle first, followed by
the compatibility bundle, manifest, and finally loader.js. The versioned file
receives a one-year immutable cache policy. Stable files receive
max-age=0, must-revalidate, so the CDN entry point can discover each release.
Add --dry-run to build and inspect the target object paths without uploading.
The standalone API can hide the launcher and control the chat imperatively:
const widget = window.Akilio.mount({
target: '#akilio-root',
apiBaseUrl: 'https://api.akilio-ai.com',
projectKey: 'ask_pk_xxx',
mode: 'floating',
launcher: 'hidden',
});
document.querySelector('#header-chat').addEventListener('click', widget.toggle);
// widget.open(); widget.close(); widget.destroy();Script-tag configuration also accepts data-launcher="hidden".
Supported authentication attributes are data-access-token, data-widget-token, data-api-key, and data-api-key-header.
Backend contract
GET /api/v1/projects/{project_key}/config/
POST /api/v1/chat/
GET /api/v1/conversations/{conversation_id}/?project_key=...
POST /api/v1/messages/{message_id}/feedback/Every request uses the configured authentication headers. Chat requests also include consent metadata when available.
Rendering performance
The composer maintains its own local input state. Typing therefore does not update the widget root or re-render the message history. Message bubbles and Markdown answers are memoized, and automatic scrolling depends on message count rather than text input changes.
Local history retains the latest 60 messages. Server history is restored from the persisted conversation ID when the local message cache is empty.
Manual light/dark switch
The widget displays an accessible light/dark toggle in its header by default. The visitor's choice is persisted per project in local storage. When no manual choice exists, colorMode="system" follows the operating-system preference.
<AkilioWidget
apiBaseUrl='https://api.example.com'
projectKey='project-key'
colorMode='system'
allowColorModeToggle
/>For the standalone build, use data-allow-color-mode-toggle="true". Set the React prop or data attribute to false to hide the control.
Widget authentication flow
By default, the widget now implements the Akilio Public API bootstrap flow automatically:
- Load public configuration from
GET /api/v1/projects/{public_key}/config/. - Create or reuse a stable browser subject.
- Request a short-lived token from
POST /api/v1/projects/{public_key}/widget-token/. - Send
Authorization: Bearer <token>to chat, conversation, and feedback endpoints. - Refresh the token before
expires_inelapses, and retry once after an HTTP 401.
No token configuration is required for a normal public widget:
<AkilioWidget
apiBaseUrl='https://api.akilio-ai.com'
projectKey='your-public-project-key'
/>Use a known subject when the host application has one:
<AkilioWidget
apiBaseUrl='https://api.akilio-ai.com'
projectKey='your-public-project-key'
externalUserId='customer-123'
/>Host-managed tokens remain supported and take precedence over automatic issuance:
<AkilioWidget
apiBaseUrl='https://api.akilio-ai.com'
projectKey='your-public-project-key'
auth={{ getAccessToken: refreshAndReturnToken }}
/>Advanced token controls:
<AkilioWidget
apiBaseUrl='https://api.akilio-ai.com'
projectKey='your-public-project-key'
auth={{
autoIssueWidgetToken: true,
tokenRefreshLeewaySeconds: 30,
onWidgetToken: ({ expiresAt }) =>
console.debug('Akilio token expires at', expiresAt),
}}
/>Next.js (App Router)
Render the widget from a Client Component because it uses browser functionality after hydration:
'use client';
import { AkilioWidget } from '@hachther/akilio-widget';
import '@hachther/akilio-widget/style.css';
export function AkilioAssistant() {
return (
<AkilioWidget
apiBaseUrl={process.env.NEXT_PUBLIC_AKILIO_API_URL!}
projectKey={process.env.NEXT_PUBLIC_AKILIO_PROJECT_KEY!}
/>
);
}The package itself is now safe to import in an SSR module. The component must still be rendered beneath a use client boundary. A dynamic import with ssr: false is optional, not required.
Ecommerce responses
When the chat API returns a commerce capability, the widget renders a compact
commerce surface inside the assistant message. Direct matches, recommendations,
comparisons, compatible products, bundles, upsells, and cross-sells receive
capability-aware headings and layouts.
<AkilioWidget
apiBaseUrl='https://api.example.com'
projectKey='public-project-key'
onProductClick={(product, variant) => {
console.log('Product selected', product, variant);
}}
/>Product links open through the existing product callback. Trusted
commerce.actions are matched only to products returned in the same response.
add_to_cart proposals require a second explicit confirmation, then use the
backend's one-time capability-confirmation flow and an idempotency key before a
cart tool is executed. The provider cart ID is retained in sessionStorage for
subsequent additions during the same browser session. The widget also records
authenticated commerce events.
The widget refreshes GET /commerce/cart/current/ whenever its chat panel is
opened. The header cart control shows the returned line items and only exposes a
checkout link supplied by the backend. A chat response with cart_state causes
an immediate refresh, ensuring the local cart display follows server-side cart
changes without treating the compact summary as the source of truth.
/chat/ response contract
AkilioChatResponse follows the public OpenAPI ChatResponse schema. It
supports documented conversation context, answer content, citations, commerce,
suggestions, warnings, market resolution, grounding/refusal flags, escalation,
contact options, customer actions, agent tools, and intent decisions. Commerce
products and actions are runtime-validated before they are rendered because the
OpenAPI schema deliberately leaves those objects extensible.
The documented response does not include top-level images, rich_actions,
forms, or cart fields. The widget does not treat those fields as part of its
public chat contract. Add them to the backend OpenAPI schema before introducing
matching widget rendering.
Human handoff
When a configured contact action is submitted, the widget sends its consented
request to POST /chat/contact-requests/ with the conversation and assistant
message that offered the action. POST /chat/handoffs/ is an equivalent backend
alias. The backend derives the conversation summary, intent, customer context,
and action context from the validated conversation. The widget renders the
browser-safe handoff delivery status returned by the API.
Assistant mode
Use assistantMode to pass the optional assistant_mode routing hint to the chat endpoint:
<AkilioWidget
apiBaseUrl='https://api.example.com'
projectKey='public-project-key'
assistantMode='ecommerce'
/>Supported values are auto, general, knowledge, ecommerce, and hybrid. The project configuration remains authoritative; this option only narrows behavior permitted by the backend.
For the standalone build:
<script
src="/akilio-widget.min.js"
data-akilio-widget
data-api-base-url="https://api.example.com"
data-project-key="public-project-key"
data-assistant-mode="ecommerce"
></script>Follow-up suggestions
When /api/v1/chat/ returns a suggestions array, the widget displays accessible suggestion chips under that assistant response. Selecting a chip sends it as the next user message. Suggestions are retained in local history and restored from server conversation message metadata or structured content when supplied by the API.
Widget Core (AKI-WIDGET-001)
Built-in interface translations cover English and French (including regional
locales such as fr-CA). Supply locale and copy for other languages; missing
translations fall back to English. The widget sets its language and direction
for common right-to-left locales. The standalone script reads the page language.
The backend receives the requested locale for generated responses.
White-label policy hides Akilio marks in the launcher, discovery tip, header, messages, and confirmation cards. Default text becomes neutral; explicitly configured project titles and copy remain under the integrator's control.
History, preferences, configuration, and anonymous continuation credentials use browser storage with an in-memory fallback when storage is blocked or full. Persistent storage is required to resume after a full page reload; memory-only sessions remain usable during the current visit. History is scoped by project and locale and retains the latest 60 messages.
Mobile forms use 16px inputs and main controls have 44px touch targets, alongside viewport sizing and safe-area padding. Physical-device keyboard testing and live backend acceptance testing are still required before production sign-off.
Adding languages
Translations live in src/locales/en.ts and src/locales/fr.ts. To ship a new
built-in language:
- Copy
en.tsto a new file such ases.tsand rename the export toes. - Translate every message, the starter questions, and the
whiteLabelwording. Preserve placeholders such as{seconds}. Setdirectiontoltrorrtl. - Import the pack in
src/locales/index.tsand add it tobuiltInLanguages. - Run
npm run typecheck,npm test, andnpm run build.
Each complete pack uses satisfies AkilioLanguagePack, so missing translation
keys are reported by TypeScript when the interface grows.
You can also add languages without rebuilding the widget using the translations
option. Partial packs are supported; untranslated text falls back to English:
import type { AkilioTranslations } from '@hachther/akilio-widget';
const translations = {
es: {
direction: 'ltr',
messages: {
title: 'Asistente',
launcher: 'Hacer una pregunta',
submit: 'Enviar',
placeholder: 'Escribe tu pregunta...',
suggestions: ['¿Cómo puedo contactar con el equipo?'],
},
whiteLabel: {
title: 'Asistente',
launcher: 'Hacer una pregunta',
configFailed: 'No se puede cargar la configuración.',
},
},
} satisfies AkilioTranslations;
<AkilioWidget
projectKey='your-project-key'
locale='es-MX'
translations={translations}
/>;The same dictionary works with window.Akilio.mount({ projectKey: 'your-project-key',
locale: 'es-MX', translations }). For script integrations, use programmatic mounting
with auto-init disabled when supplying dictionaries.
Locale matching ignores case and accepts underscores. The fallback order is the
most specific locale, then its parent language/script, then English. For example,
zh-Hant-TW inherits from zh-Hant, zh, and en. Custom dictionaries override
built-ins at the same locale. Existing project title, placeholder, and starter
questions still take precedence; the copy prop remains the final text override.
White-label wording is stored separately from branded defaults. Dictionaries are
scoped to each widget, so one integration cannot change another's translations.
Changing locale or translations props updates React UI copy. The requested
locale is still sent to the backend; adding UI translations does not configure
backend response languages. This framework uses plain text messages and existing
named placeholders, not ICU plural rules.
Business branding (AKI-WIDGET-002)
<AkilioWidget
projectKey='your-project-key'
agentName='Alex'
welcomeMessage='Welcome to Acme. How can I help?'
starterPrompts={['Explore our services', 'Contact the team']}
launcherPosition='bottom-left'
theme={{
logo_url: 'https://example.com/logo.png',
brand_name: 'Acme',
primary_color: '#123456',
primary_dark_color: '#102030',
primary_foreground_color: '#ffffff',
}}
/>Business logos appear in the launcher, discovery tip, header, empty state, assistant answers, and human-help confirmation cards. HTTP(S) and relative image URLs are accepted. Failed or invalid images fall back to the Akilio mark when branding is enabled, or a neutral icon when branding is hidden. Required “Powered by Akilio” attribution remains visible with a business logo.
Name priority: copy.title, agentName, host theme.brand_name, server
theme_config.brand_name, server title, then language defaults. Welcome text
uses copy.emptyState, welcomeMessage, server welcome_message, then language
defaults. The welcome message appears before the first conversation message.
Before configuration arrives, starter prompts use starterPrompts and then
language suggestions. Once available, starter_questions from the public
configuration endpoint is authoritative, including an explicit empty array that
hides prompts. Up to four are shown; server-advertised capability actions remain
independent of starter prompts.
Standalone embeds accept the same options via Akilio.mount() or these attributes:
<script
src="https://your-cdn.example/widget/loader.js"
data-akilio-widget
data-project-key="your-project-key"
data-agent-name="Alex"
data-brand-name="Acme"
data-logo-url="https://example.com/logo.png"
data-welcome-message="Welcome to Acme. How can I help?"
data-starter-prompts='["Explore our services", "Contact the team"]'
data-theme='{"primary_color":"#123456"}'
data-launcher-position="bottom-left"
></script>Use data-starter-prompts='[]' to hide prompts. Dedicated logo/name attributes
override corresponding values in data-theme. Malformed prompt JSON falls back
to configuration defaults. Custom React launchers and external HTML buttons using
launcher: 'hidden' with open(), close(), or toggle() remain supported.
