Widget Chat - AI Chatbot Widget
A beautiful, customizable AI chatbot widget you can drop into any Flutter, FlutterFlow, React, or web app — rich content, tools, multimodal input, and multi-language, all configured from a no-code dashboard at widget-chat.com.
💬 Try the live widget at widget-chat.com (bottom-right) — it runs this exact package.
Features
- 🎨 Beautiful UI: Modern, customizable design with smooth animations
- 🚀 Easy Integration: Single widget integration with minimal setup
- 🔒 Secure: Project-based authentication with secure keys
- 📱 Responsive: Adaptive design for mobile and web platforms
- ⚡ Real-time: Live chat with typing indicators
- 🎭 Customizable: Themes, colors, FAB styles, and behavior
- 🌐 Multi-platform: Works on iOS, Android, and Web
Installation
Add to your pubspec.yaml:
dependencies:
widget_chat: ^0.0.10
Quick Start
1. Import the package
import 'package:widget_chat/widget_chat.dart';
2. Add the ChatWidget to your app
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
// Your app content here
YourMainContent(),
// Add the chatbot
Positioned(
bottom: 20,
right: 20,
child: ChatWidget(
configuration: BotConfiguration(
projectSecretKey: 'your-project-secret-key',
userID: 'unique-user-id',
name: 'Support Bot',
welcomeMessage: 'Hello! How can I help you today?',
),
),
),
],
),
);
}
}
Configuration Options
Basic Configuration
BotConfiguration(
// Required
projectSecretKey: 'your-secret-key', // Get from your dashboard
userID: 'user-123', // Unique identifier for the user
// Optional
name: 'Assistant', // Bot display name
welcomeMessage: 'How can I help?', // Initial greeting
fontFamily: 'Roboto', // Chat font (default: Roboto)
color: '#3B82F6', // Primary color in hex
systemInstructions: 'Be helpful', // Bot behavior instructions
isPreviewMode: false, // Preview mode flag
)
FAB Customization
BotConfiguration(
projectSecretKey: 'your-key',
userID: 'user-123',
fabConfiguration: FabConfiguration(
icon: 'chat_bubble', // Icon name
iconSize: 24.0, // Icon size
iconColor: '#FFFFFF', // Icon color in hex
backgroundColor: '#3B82F6', // Background color in hex
buttonSize: 56.0, // FAB size
borderRadius: 28.0, // Corner radius
useAvatarAsIcon: false, // Use avatar image as icon
),
)
Advanced Usage
Responsive Design
// Mobile (default) - full-screen chat experience
ChatWidget(
configuration: BotConfiguration(
projectSecretKey: 'your-key',
userID: 'user-123',
),
)
// Web/Desktop - fixed 400x700 chat window
ChatWidget(
configuration: BotConfiguration(
projectSecretKey: 'your-key',
userID: 'user-123',
),
mobile: false,
)
Custom FAB Widget
ChatWidget(
configuration: BotConfiguration(
projectSecretKey: 'your-key',
userID: 'user-123',
),
fabWidget: CustomFABWidget(), // Your custom FAB
)
Chat State Callbacks
ChatWidget(
configuration: BotConfiguration(
projectSecretKey: 'your-key',
userID: 'user-123',
),
onOpen: ({required bool isOpen}) {
print('Chat is ${isOpen ? "open" : "closed"}');
// Track analytics, update UI, etc.
},
)
Host App Integration
The bot can drive your app — and your app can feed the bot — through four channels. All of them are optional; wire only what your product needs.
Rich-content action callbacks
Backend tools and the model can emit buttons/cards whose taps land in YOUR code. Declare the callback ids your app handles in the dashboard project's callback registry (so the model can't invent ids you don't dispatch), then:
ChatWidget(
configuration: BotConfiguration(projectSecretKey: '...', userID: 'user-123'),
onRichContentCallback: (callbackId, payload) {
switch (callbackId) {
case 'open_item': navigateToItem(payload?['item']);
case 'open_paywall': showPaywall();
case 'open_login': showSignIn();
}
},
)
Tip — for navigation, prefer ONE generic id over per-feature ids: declare a
single open_screen callback whose payload['screen'] selects the
destination, and keep a {screenKey: route} map host-side. New destinations
then need no new callback ids — just a new key in the map and in the
registry description (which is also what tells the model the allowed keys):
case 'open_screen':
final route = screenRoutes[payload?['screen']]; // {'studio': StudioPage.route, ...}
if (route != null) Navigator.of(context).push(route());
Authenticated tool calls (authToken)
Pass the END USER's JWT and the backend forwards it as a Bearer header to every tool call, so your API attributes actions to the right account:
BotConfiguration(
userID: user.id.toString(),
authToken: session.jwt, // re-read on EVERY send — refresh it proactively
)
authToken is re-read per message: rotate it mid-session with
updateConfiguration/a rebuild and the next tool call uses the fresh token.
Refresh BEFORE expiry (e.g. a 5-minute periodic check) — an expired token
makes every gated tool 401 and degrades the conversation.
Guest / anonymous sessions
userID is any non-empty string — no account required. The recommended
pattern for apps with optional sign-in:
- Guests get a device-persistent id (
anon-<random>, stored in prefs) and NOauthToken. - Tell the bot who it's talking to via
userContext({'authenticated': false}), and gate account-bound tools in your system prompt AND in each tool's description ("SIGNED-IN USERS ONLY … emit the open_login button instead"). A required boolean parameter on gated tools (e.g.user_is_signed_in, "copy user_context.authenticated exactly") makes small models respect the gate far more reliably than prose alone. - On login, remount the chat keyed to the real user id — history scopes per
userID, so the guest thread stays behind and the account thread loads. - Disable attachments for guests (
enableFileAttach: false) when your upload endpoint needs auth.
User context (userContext)
A free-form map injected into the system prompt on every send — entitlement flags, profile ids, supported callback vocabulary. Keep it small (tokenised each turn, 8 KB cap):
userContext: {
'authenticated': true,
'pro': user.isPro,
'supported_callbacks': ['open_item', 'open_paywall'],
}
Host slots
Backend tools with execution_target: host_render reserve a spot in the
bubble that YOUR widget fills — the tool's arguments arrive as the payload:
hostSlotBuilder: (slotId, payload) =>
slotId == 'item_preview' ? ItemPreview(payload) : null,
Slot builders must be pure functions of the payload — history reloads re-render them from stored JSON.
Image uploads
Wire imageUploader to your own storage and the chat sends compact URLs
instead of inlining base64 into the conversation:
imageUploader: (bytes, mimeType) async => myCdn.upload(bytes, mimeType),
Attached photos are forwarded by the backend to image-bearing tools (analysis, extraction, try-on style flows) — the model never handles raw bytes.
Theming that stays on brand
Since 0.0.18 the chat pins its accent to your configured brand color in
BOTH light and dark mode (filled buttons, send icon, focus rings render the
exact color instead of a Material tonal derivative). Per-mode surfaces and
bubble colors come from the dashboard (chat_appearance /
chat_appearance_dark), with defaultThemeMode and an optional in-chat
theme switcher (showThemeToggle). Prefer configuring colors on the
dashboard over hard-coding them host-side — config survives app releases.
Platform-Specific Setup
Mobile (Default)
The mobile parameter defaults to true, so the chat opens in full-screen mode out of the box - ideal for iOS and Android apps.
Web / Desktop
For web or desktop applications, set mobile: false to use a fixed-size (400x700) chat window:
ChatWidget(
configuration: config,
mobile: false,
)
The embedded iframe version (web_embed) already handles this automatically via URL parameters.
Available Icons
The FAB supports these icon options:
chat_bubble(default)chatmessagesupport_agenthelpquestion_answerforumcontact_supportheadset_mic
Security Best Practices
- Never expose your project secret key in client-side code
- Use unique user IDs to maintain conversation history
- Configure allowed origins for web deployments
- Keep your SDK updated for security patches
Example App
Check out the complete example in the /example folder:
import 'package:flutter/material.dart';
import 'package:widget_chat/widget_chat.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Widget Chat Example',
home: Scaffold(
body: Stack(
children: [
Center(
child: Text('Your App Content'),
),
Positioned(
bottom: 20,
right: 20,
child: ChatWidget(
configuration: BotConfiguration(
projectSecretKey: 'your-project-secret-key',
userID: 'demo-user',
name: 'AI Assistant',
welcomeMessage: 'Hi! How can I help you today?',
color: '#6366F1',
fabConfiguration: FabConfiguration(
icon: 'support_agent',
backgroundColor: '#6366F1',
),
),
),
),
],
),
),
);
}
}
Troubleshooting
Chat doesn't open
- Verify your project secret key is correct
- Check console for any error messages
- Ensure the widget is properly positioned in your widget tree
Styling issues
- Make sure hex colors include the # symbol
- Use supported Google Fonts for fontFamily
- Check that size values are reasonable (e.g., buttonSize: 56.0)
Support
- Documentation: https://docs.widget-chat.com
- Email: support@widget-chat.com
- GitHub Issues: Report bugs here
License
This project is licensed under the Proprietary License - see the LICENSE file for details.
Libraries
- chat_widget
- localization/generated/chat_localizations
- localization/generated/chat_localizations_ar
- localization/generated/chat_localizations_bg
- localization/generated/chat_localizations_bn
- localization/generated/chat_localizations_ca
- localization/generated/chat_localizations_cs
- localization/generated/chat_localizations_da
- localization/generated/chat_localizations_de
- localization/generated/chat_localizations_el
- localization/generated/chat_localizations_en
- localization/generated/chat_localizations_es
- localization/generated/chat_localizations_et
- localization/generated/chat_localizations_fa
- localization/generated/chat_localizations_fi
- localization/generated/chat_localizations_fr
- localization/generated/chat_localizations_ga
- localization/generated/chat_localizations_he
- localization/generated/chat_localizations_hi
- localization/generated/chat_localizations_hr
- localization/generated/chat_localizations_hu
- localization/generated/chat_localizations_id
- localization/generated/chat_localizations_it
- localization/generated/chat_localizations_ja
- localization/generated/chat_localizations_ko
- localization/generated/chat_localizations_lt
- localization/generated/chat_localizations_lv
- localization/generated/chat_localizations_ms
- localization/generated/chat_localizations_mt
- localization/generated/chat_localizations_nl
- localization/generated/chat_localizations_no
- localization/generated/chat_localizations_pl
- localization/generated/chat_localizations_pt
- localization/generated/chat_localizations_ro
- localization/generated/chat_localizations_ru
- localization/generated/chat_localizations_sk
- localization/generated/chat_localizations_sl
- localization/generated/chat_localizations_sr
- localization/generated/chat_localizations_sv
- localization/generated/chat_localizations_sw
- localization/generated/chat_localizations_ta
- localization/generated/chat_localizations_te
- localization/generated/chat_localizations_th
- localization/generated/chat_localizations_tl
- localization/generated/chat_localizations_tr
- localization/generated/chat_localizations_uk
- localization/generated/chat_localizations_vi
- localization/generated/chat_localizations_zh
- test