@clikvn/agent-widget-lite
v0.6.14
Published
Lightweight chatbot widget
Readme
@clikvn/agent-widget-lite
Lightweight AI chatbot widget for React apps and plain HTML pages. Connects to the Clik AI API with SSE streaming, agent reasoning steps, contact capture form, and a fully themeable UI.
Installation
npm install @clikvn/agent-widget-litePeer dependencies (React apps only):
npm install react react-domUsage
1. React — Floating widget (button + chat panel)
import { ChatWidget } from '@clikvn/agent-widget-lite';
import type { WidgetConfig } from '@clikvn/agent-widget-lite';
const config: WidgetConfig = {
apiHost: 'https://api.clik.vn/chatbot',
agentId: 'your-agent-id',
autoOpen: false,
autoOpenMinWidth: 640,
theme: {
overview: {
title: 'Anakie Assistant',
},
placeholder: 'Tell us about your project...',
floatingButton: {
// Omit these to use responsive defaults:
// desktop: 440x610, right/bottom 24px
// mobile <= 640px: calc(100vw - 24px)x620, right/bottom 12px
size: 44,
animation: 'pulse ping',
animationStrength: 'normal',
animationPingColor: 'light',
animationStopAfterClose: true,
},
},
};
export default function App() {
return (
<>
{/* your app */}
<ChatWidget config={config} />
</>
);
}ChatWidget renders a fixed floating button (default: bottom-right 24px). Clicking it opens/closes the chat panel. Set autoOpen: true to open the floating chat panel automatically when the widget first mounts. Use autoOpenMinWidth to restrict auto-open to screens at least that wide; it is only checked when autoOpen is true, and smaller screens stay closed. autoOpenMinWidth accepts a number such as 640 or a pixel string such as '640px'.
2. React — Embedded chat panel (no floating button)
Use ChatWindow directly to embed the panel inside your own layout:
import { ChatWindow } from '@clikvn/agent-widget-lite';
import type { WidgetConfig } from '@clikvn/agent-widget-lite';
const config: WidgetConfig = {
apiHost: 'https://api.clik.vn/chatbot',
agentId: 'your-agent-id',
};
export default function Sidebar() {
return (
<div style={{ width: 390, height: '100vh' }}>
<ChatWindow config={config} embedded />
</div>
);
}ChatWindow props:
| Prop | Type | Description |
| ---------- | -------------- | -------------------------------------------------------------------------- |
| config | WidgetConfig | Required. Full configuration object. |
| embedded | boolean | Fills container, hides close button. Use when the panel is always visible. |
| onClose | () => void | Called when the user clicks the close button (non-embedded). |
3. Plain HTML / vanilla JS — Script tag
Include the stylesheet and browser bundle, then call ChatbotLite.initWidget():
<!doctype html>
<html>
<head>
<link
rel="stylesheet"
href="https://unpkg.com/@clikvn/agent-widget-lite/dist/web.css"
/>
<script
type="module"
src="https://unpkg.com/@clikvn/agent-widget-lite/dist/web.js"
></script>
</head>
<body>
<script type="module">
import ChatbotLite from 'https://unpkg.com/@clikvn/agent-widget-lite/dist/web.js';
ChatbotLite.initWidget({
apiHost: 'https://api.clik.vn/chatbot',
agentId: 'your-agent-id',
autoOpen: false,
autoOpenMinWidth: 640,
theme: {
welcomeMessage: 'Hi! How can I help?',
placeholder: 'Tell us about your project...',
fontFamily: 'inherit', // Leave unset to inherit the page/WordPress theme font
backgroundColor: '#ffffff',
header: {
backgroundColor: '#0D0D0D',
borderBottom: 'none',
textColor: '#ffffff',
fontSize: '1rem',
fontWeight: 700,
lineHeight: '1.5rem',
},
input: {
backgroundColor: '#ffffff',
switchToTextarea: false,
},
apiMessage: {
textColor: '#171717',
fontSize: '1rem',
lineHeight: '1.45',
showBubble: true,
backgroundColor: '#F4F3F1',
borderRadius: '14px',
padding: '1rem 1.125rem',
},
userMessage: {
textColor: '#ffffff',
fontSize: '1rem',
lineHeight: '1.45',
showBubble: true,
backgroundColor: '#111111',
borderRadius: '14px 4px 14px 14px',
padding: '1rem 1.125rem',
},
overview: {
title: 'Anakie Assistant',
description:
'Premium joinery guidance for new builds and renovations.',
},
},
});
</script>
</body>
</html>To unmount:
window.ChatbotLite.destroy();The widget mounts into a <clik-chatbot-lite> element appended to <body>. Calling initWidget again with a new config re-renders in place.
4. WordPress
Copy dist/web.js and dist/web.css into your theme or plugin, then enqueue the stylesheet and initialize the module in the footer:
add_action('wp_enqueue_scripts', function () {
wp_enqueue_style(
'clik-chatbot-lite',
plugin_dir_url(__FILE__) . 'assets/web.css',
array(),
'0.3.0'
);
});
add_action('wp_footer', function () {
$script_url = plugin_dir_url(__FILE__) . 'assets/web.js';
$config = array(
'apiHost' => 'https://api.clik.vn/chatbot',
'agentId' => 'your-agent-id',
'autoOpen' => false,
'autoOpenMinWidth' => 640,
'theme' => array(
'overview' => array(
'title' => 'Anakie Assistant',
),
'placeholder' => 'Tell us about your project...',
),
);
?>
<script type="module">
import ChatbotLite from <?php echo wp_json_encode(esc_url($script_url)); ?>;
ChatbotLite.initWidget(<?php echo wp_json_encode($config); ?>);
</script>
<?php
}, 99);Do not put private values such as webhookApiKey or a NocoDB API token directly in the browser config. For those, route requests through a WordPress server-side endpoint.
Configuration (WidgetConfig)
interface WidgetConfig {
// Required
apiHost: string; // Base URL of the Clik chatbot API
agentId: string; // Agent ID from the Clik dashboard
// Optional — API
webhookUrl?: string; // Use a custom webhook URL instead of the default prediction endpoint
webhookApiKey?: string; // Bearer token for the webhook
brandAlias?: string; // Brand identifier passed to the API
// Optional — Features
autoOpen?: boolean; // Open ChatWidget automatically on mount (default: false)
autoOpenMinWidth?: number | string; // Only auto-open when viewport width is at least this many px
showNewChatButton?: boolean; // Show a "New chat" menu button in the header (default: false)
onNewChat?: (newChatId: string) => void; // Called after the user starts a new chat
onProductClick?: (product: ProductType, context: ProductClickContext) => void; // Called when a product list row is clicked
// Optional — Contact form (NocoDB)
nocodb?: NocoDBConfig;
// Optional — UI appearance
theme?: Theme;
}NocoDBConfig
When provided, the contact capture form submits to a NocoDB table.
interface NocoDBConfig {
apiUrl: string; // NocoDB instance base URL, e.g. https://db.example.com
apiToken: string; // NocoDB API token (xc-token)
baseId: string; // NocoDB base ID
tableId: string; // NocoDB table ID
enabled?: boolean; // Set to false to disable submission (default: true)
}Expected table columns: Social name, Name, Phone number, Email, SessionID, Platform, Main pain points, Location.
theme.floatingButton
Controls the floating trigger button and the chat panel that opens from it (used by ChatWidget only).
interface FloatingButtonConfig {
// Trigger button
position?: {
desktop?: { top?: string; right?: string; bottom?: string; left?: string };
mobile?: { top?: string; right?: string; bottom?: string; left?: string };
}; // Default: desktop right/bottom 24px, mobile right/bottom 12px
size?: number | { desktop?: number; mobile?: number }; // Button size in px (default: 44)
backgroundColor?: string; // Button background color (default: '#0D0D0D')
border?: string; // Button border shorthand (default: '1px solid #ffffff2b')
borderRadius?: string; // Button corner radius (default: '50%')
icon?: ReactNode | string; // React element or image URL for the button icon
animation?: 'vibrate' | 'pulse' | 'ping' | 'vibrate ping' | 'pulse ping'; // Button animation (default: 'vibrate')
animationStrength?: 'low' | 'normal' | 'strong'; // Button animation strength (default: 'normal')
animationPingColor?: 'light' | 'dark'; // Ping ring color when animation includes ping (default: 'light')
animationStopAfterMs?: number; // Stop animating after this many ms. Leave unset to keep animating until opened.
animationStopAfterClose?: boolean; // Stop animating after the chat panel is closed (default: false)
// Chat panel
panelPosition?: {
desktop?: { top?: string; right?: string; bottom?: string; left?: string };
mobile?: { top?: string; right?: string; bottom?: string; left?: string };
}; // Default: desktop right/bottom 24px, mobile right/bottom 12px
panelSize?: {
desktop?: { width?: string | number; height?: string | number };
mobile?: { width?: string | number; height?: string | number };
}; // Default: desktop 440x610px, mobile calc(100vw - 24px)x620px
panelBorderRadius?: string; // Panel corner radius (default: '12px')
panelBoxShadow?: string; // Panel drop shadow (default: '0 8px 32px rgba(0,0,0,0.18)'). Set to 'none' to disable.
panelBorder?: string; // Panel border shorthand (default: '1px solid #ffffff38')
}theme
interface Theme {
fontFamily?: string; // Whole-widget font. Leave unset to inherit the embedding page font.
backgroundColor?: string; // Messages panel background (default: '#ffffff')
welcomeMessage?: string; // Fallback header title when overview.title is not set
placeholder?: string; // Input placeholder text
header?: {
backgroundColor?: string; // Header background color
borderBottom?: string; // CSS border shorthand, e.g. '1px solid #e4e4e7'
textColor?: string; // Header title text color
fontSize?: string | number;
fontWeight?: string | number;
lineHeight?: string | number;
padding?: string; // Header padding CSS shorthand
height?: string | number; // Header height
};
input?: {
backgroundColor?: string; // Composer input background color
padding?: string; // Footer/composer padding CSS shorthand
height?: string | number; // Footer/composer height
switchToTextarea?: boolean; // Switch from input to textarea on newline/overflow (default: false)
showImageUploadButton?: boolean; // Show the image upload button
submitButton?: {
size?: string | number; // Submit button width/height
borderRadius?: string; // Submit button corner radius
icon?: ReactNode | string; // React element or image URL
};
};
apiMessage?: {
avatarSrc?: string; // Override bot avatar URL. Supplying a URL shows the avatar unless showAvatar is false.
backgroundColor?: string; // Bubble background color (default when bubble: '#F4F3F1')
textColor?: string; // Text color
fontSize?: string | number;
fontWeight?: string | number;
lineHeight?: string | number;
showAvatar?: boolean; // Show/hide the avatar ring (default: true when avatarSrc is set, otherwise false)
showBubble?: boolean; // false renders as text lines; true renders as a bubble (default: true)
borderRadius?: string; // Bubble radius (default: '14px' when bubble)
padding?: string; // Bubble padding (default: '1rem 1.125rem' when bubble)
};
userMessage?: {
backgroundColor?: string; // Bubble background color (default: '#111111')
textColor?: string; // Text color
fontSize?: string | number;
fontWeight?: string | number;
lineHeight?: string | number;
showBubble?: boolean; // true renders as bubble; false renders as text lines (default: true)
borderRadius?: string; // Bubble radius (default: '14px 4px 14px 14px')
padding?: string; // Bubble padding (default: '1rem 1.125rem')
};
floatingButton?: FloatingButtonConfig;
bot?: {
name?: string; // Bot display name
avatar?: string; // Bot avatar URL
};
overview?: {
title?: string; // Heading in the header and empty-state overview panel
description?: string; // Subtext in the overview panel
};
scrollToTop?: {
showButton?: boolean; // Show/hide the scroll-to-top button (default: true)
size?: string | number; // Button size (default: 32)
border?: string; // Button border shorthand (default: '1px solid #000000')
borderRadius?: string; // Button radius (default: '100%')
backgroundColor?: string; // Button background (default: '#000000')
icon?: string; // Optional image URL. Uses the default ArrowUp icon when unset.
iconColor?: string; // Default icon color (default: '#ffffff')
endRefSize?: {
width?: string | number; // Bottom spacer width (default: '1rem')
height?: string | number; // Bottom spacer height (default: '1rem')
};
};
userContactForm?: {
title?: string; // Form heading (default: "Your contact")
labelName?: string; // Name field label
labelEmail?: string; // Email field label
labelContact?: string; // Phone field label
labelAddress?: string; // Address field label
labelPurpose?: string; // Purpose field label
messages?: {
nameError?: string;
invalidContact?: string;
invalidEmail?: string;
needFillContactOrEmail?: string;
submitSuccess?: string;
submitError?: string;
};
};
buttons?: {
textBtnSubmit?: string; // Submit button label (default: "Submit")
textBtnCancel?: string; // Cancel button label (default: "Cancel")
};
}Full example config
const config: WidgetConfig = {
apiHost: 'https://api.clik.vn/chatbot',
agentId: 'your-agent-id',
autoOpen: false,
autoOpenMinWidth: 640,
showNewChatButton: true,
onNewChat: (newChatId) => console.log('New chat started:', newChatId),
onProductClick: (product, context) => console.log('Product clicked:', product, context),
nocodb: {
apiUrl: 'https://db.example.com',
apiToken: 'your-noco-token',
baseId: 'your-base-id',
tableId: 'your-table-id',
enabled: true,
},
theme: {
fontFamily: 'inherit',
backgroundColor: '#ffffff',
welcomeMessage: 'Hi! How can I help?',
placeholder: 'Tell us about your project...',
floatingButton: {
position: {
desktop: { right: '24px', bottom: '24px' },
mobile: { right: '12px', bottom: '12px' },
},
size: {
desktop: 44,
mobile: 44,
},
backgroundColor: '#0D0D0D',
border: '1px solid #ffffff2b',
borderRadius: '50%',
animation: 'pulse ping',
animationStrength: 'normal',
animationPingColor: 'light',
animationStopAfterMs: 10000,
animationStopAfterClose: true,
panelPosition: {
desktop: { right: '24px', bottom: '24px' },
mobile: { right: '12px', bottom: '12px' },
},
panelSize: {
desktop: { width: 440, height: 610 },
mobile: { width: 'calc(100vw - 24px)', height: 620 },
},
panelBorderRadius: '12px',
panelBoxShadow: '0 8px 32px rgba(0,0,0,0.18)',
panelBorder: '1px solid #ffffff38',
},
bot: {
name: 'Aria',
avatar: 'https://example.com/bot-avatar.png',
},
header: {
backgroundColor: '#0D0D0D',
borderBottom: 'none',
textColor: '#ffffff',
fontSize: '1rem',
fontWeight: 700,
lineHeight: '1.5rem',
padding: '1rem 1.125rem 1rem 1.25rem',
},
input: {
backgroundColor: '#ffffff',
padding: '1rem 1.5rem',
switchToTextarea: false,
showImageUploadButton: true,
submitButton: {
size: 40,
borderRadius: '6px',
icon: 'https://example.com/send.svg',
},
},
apiMessage: {
avatarSrc: 'https://example.com/bot-avatar.png',
backgroundColor: '#F4F3F1',
textColor: '#171717',
fontSize: '1rem',
lineHeight: '1.45',
showAvatar: true,
showBubble: true,
borderRadius: '14px',
padding: '1rem 1.125rem',
},
userMessage: {
backgroundColor: '#111111',
textColor: '#FFFFFF',
fontSize: '1rem',
lineHeight: '1.45',
showBubble: true,
borderRadius: '14px 4px 14px 14px',
padding: '1rem 1.125rem',
},
overview: {
title: 'Anakie Assistant',
description: 'Premium joinery guidance for new builds and renovations.',
},
scrollToTop: {
showButton: true,
size: 32,
border: '1px solid #000000',
borderRadius: '100%',
backgroundColor: '#000000',
icon: 'https://example.com/arrow-up.svg',
iconColor: '#ffffff',
endRefSize: {
width: '1rem',
height: '1rem',
},
},
userContactForm: {
title: 'Leave your contact',
labelName: 'Name',
labelEmail: 'Email',
labelContact: 'Phone',
labelAddress: 'Address',
labelPurpose: 'Purpose',
messages: {
submitSuccess: 'Thanks! We will be in touch.',
submitError: 'Something went wrong. Please try again.',
},
},
buttons: {
textBtnSubmit: 'Submit',
textBtnCancel: 'Cancel',
},
},
};Contact capture form
When the agent invokes the display_contact_form tool, a contact form appears below the assistant message. It collects name (required), email, phone, address, and purpose.
Submit flow:
- On submit the widget sends a JSON event message to the chat:
{ "type": "application_event", "event": "contact_form_submitted", "tool": "save_contact_form", "name": "...", ... } - If
nocodb.enabledistrue, the widget also submits the contact data to the configured NocoDB table. - The agent is expected to respond with a
save_contact_formtool call whose output contains{ "status": "succeeded" | "failed" }. - If
statusis"succeeded", the widget marks the contact as submitted and replaces the hidden event message with a success confirmation panel. This state persists across page refreshes. - If
statusis not"succeeded", the contact state reverts tocancelled.
State machine: unknown → displayed → submitted | cancelled (submitted → cancelled on failed save). Persisted in localStorage keyed by agentId + chatId. Starting a new chat clears the previous session's contact state.
The current contact status and purpose are injected into every chat request as contact_status and contact_purpose fields, so the agent can adjust its behaviour accordingly.
Decoration style iframe
When an assistant message includes the render_decoration_style used tool, the widget renders an embedded iframe inside the chat, directly below that assistant message.
{
"role": "apiMessage",
"content": "Here is the render preview.",
"usedTools": [
{
"tool": "render_decoration_style",
"toolInput": {},
"toolOutput": "https://example.com/render-preview"
}
]
}The iframe uses the URL returned in toolOutput. Plain URL strings are supported, and JSON string outputs with a top-level url field are also accepted.
If the assistant message content is the same URL as the iframe source, the widget hides that message text and only shows the embedded preview.
The embedded preview includes a fullscreen control that expands the iframe preview to the browser viewport.
Chat history & session persistence
The widget automatically persists the chatId in localStorage (key: clik_chat_${agentId}) and fetches the conversation history on mount via:
GET {apiHost}/ext/v1/chats/chatmessage/{chatId}Refreshing the page continues the previous conversation. Starting a new chat (via showNewChatButton) generates a fresh chatId.
Agent reasoning
The widget automatically displays agent thinking steps (tool calls, sub-agent flows) as a collapsible "Thought for N seconds" panel above each assistant message.
Local development
# Install dependencies
npm install
# Build (both NPM package and web bundle)
npm run build
# Watch mode
npm run devRunning the example app
cd example && npm install && npm run devThe example app starts a Vite dev server with a live config editor on the left and the chat panel on the right. The config form covers common chat fields, NocoDB credentials, feature toggles, and floating widget appearance; the default config also demonstrates the available theme styling options. Toggle "Floating widget" to switch between embedded and floating modes.
Exports
// NPM package entry (dist/index.js) — use in React apps
import { ChatWidget, ChatWindow } from '@clikvn/agent-widget-lite';
import type {
WidgetConfig,
Message,
UsedTool,
StreamEvent,
} from '@clikvn/agent-widget-lite';
// Web bundle (dist/web.js) — use via script tag
window.ChatbotLite.initWidget(config);
window.ChatbotLite.destroy();
// Web stylesheet (dist/web.css) - load next to dist/web.jsLicense
ISC © Clik JSC
