@joseantonionuevo/ai-chat-widget
v1.5.1
Published
A ready-to-use, customizable chat widget for AI applications using Vercel AI SDK v6
Maintainers
Readme
AI Chat Widget
A ready-to-use, fully customizable floating chat widget for AI-powered applications. Built with React and designed to work seamlessly with the Vercel AI SDK v6.
What is this?
AI Chat Widget is an open-source React component that adds a beautiful, functional chat interface to any web application. It's designed for developers who want to integrate AI chat capabilities (like GPT, Claude, or any LLM) into their apps without building the entire chat UI from scratch.
Perfect for:
- Customer support chatbots
- AI assistants embedded in SaaS products
- Documentation Q&A bots
- Any application that needs conversational AI
Key Features
- 5 Beautiful Dark Themes - Pre-built color schemes ready to use
- Fully Customizable - Override any color via props
- Real-time Streaming - Shows responses as they're generated
- Markdown Support - Renders formatted text, code blocks, lists
- Context-Based Suggestions - Clickable follow-up questions after AI responses
- Restart Chat Button - Clear conversation and start fresh from header
- Smart Rate Limit Handling - Auto-detects 429 errors with optional auto-retry
- Lightweight - ~15KB minified, lazy-loads heavy dependencies
- Accessible - Keyboard navigation, ARIA labels, screen reader support
- Internationalization - English and Spanish included, easy to extend
- TypeScript - Full type definitions included
- Zero Config - Works out of the box with sensible defaults
Table of Contents
- Installation
- Quick Start
- Theme Presets
- Customizing Colors
- All Customizable Props
- Customizing Labels
- Rate Limit Handling
- Custom Icons
- Backend Requirements
- Framework Integration
- Advanced Usage
- TypeScript
- Contributing
- License
Installation
# npm
npm install @joseantonionuevo/ai-chat-widget
# pnpm
pnpm add @joseantonionuevo/ai-chat-widget
# yarn
yarn add @joseantonionuevo/ai-chat-widgetPeer Dependencies
This package requires the following peer dependencies:
| Package | Version | Purpose |
|---------|---------|---------|
| react | >=18.0.0 | UI framework |
| react-dom | >=18.0.0 | DOM rendering |
| @ai-sdk/react | >=3.0.0 | Chat hooks (useChat) |
| ai | >=6.0.0 | Vercel AI SDK core |
Install them if not already in your project:
npm install react react-dom @ai-sdk/react aiOptional Dependencies (for Markdown)
For rich markdown rendering in assistant responses:
npm install react-markdown remark-gfmIf not installed, responses will render as plain text.
Quick Start
import { ChatWidget } from '@joseantonionuevo/ai-chat-widget';
function App() {
return (
<div>
<h1>My Application</h1>
{/* The widget appears as a floating button */}
<ChatWidget apiUrl="/api/chat" />
</div>
);
}That's it! A floating chat button will appear in the bottom-right corner of your app.
Theme Presets
The widget includes 5 professionally designed dark mode themes. Each theme is carefully crafted with proper contrast ratios and visual hierarchy.
Using a Theme Preset
<ChatWidget apiUrl="/api/chat" theme="midnight" />
<ChatWidget apiUrl="/api/chat" theme="ocean" />
<ChatWidget apiUrl="/api/chat" theme="forest" />
<ChatWidget apiUrl="/api/chat" theme="sunset" />
<ChatWidget apiUrl="/api/chat" theme="lavender" />1. Midnight (Default)
A sophisticated dark theme with indigo accents. Perfect for modern, professional applications.
<ChatWidget apiUrl="/api/chat" theme="midnight" />| Color Variable | Value | Description |
|----------------|-------|-------------|
| primary | #6366f1 | Indigo - buttons, user bubbles |
| primaryHover | #4f46e5 | Darker indigo for hover states |
| background | #0f172a | Slate 900 - main background |
| surface | #1e293b | Slate 800 - cards, input area |
| surfaceHover | #334155 | Slate 700 - hover states |
| text | #f1f5f9 | Slate 100 - primary text |
| textSecondary | #94a3b8 | Slate 400 - muted text |
| border | #334155 | Slate 700 - borders |
| userBubble | #6366f1 | Indigo - user message background |
| userBubbleText | #ffffff | White - user message text |
| assistantBubble | #1e293b | Slate 800 - assistant background |
| assistantBubbleText | #f1f5f9 | Slate 100 - assistant text |
| error | #ef4444 | Red 500 - error text |
| errorBg | #450a0a | Red 950 - error background |
2. Ocean
A calming dark theme with sky blue accents. Great for productivity and communication apps.
<ChatWidget apiUrl="/api/chat" theme="ocean" />| Color Variable | Value | Description |
|----------------|-------|-------------|
| primary | #0ea5e9 | Sky 500 - buttons, user bubbles |
| primaryHover | #0284c7 | Sky 600 - hover states |
| background | #0c1222 | Deep blue - main background |
| surface | #172033 | Dark blue - cards, input area |
| surfaceHover | #1e3a5f | Navy - hover states |
| text | #e0f2fe | Sky 100 - primary text |
| textSecondary | #7dd3fc | Sky 300 - muted text |
| border | #1e3a5f | Navy - borders |
| userBubble | #0ea5e9 | Sky 500 - user message background |
| userBubbleText | #ffffff | White - user message text |
| assistantBubble | #172033 | Dark blue - assistant background |
| assistantBubbleText | #e0f2fe | Sky 100 - assistant text |
3. Forest
A nature-inspired dark theme with green accents. Ideal for wellness, eco, or nature-related apps.
<ChatWidget apiUrl="/api/chat" theme="forest" />| Color Variable | Value | Description |
|----------------|-------|-------------|
| primary | #22c55e | Green 500 - buttons, user bubbles |
| primaryHover | #16a34a | Green 600 - hover states |
| background | #052e16 | Deep green - main background |
| surface | #14532d | Green 900 - cards, input area |
| surfaceHover | #166534 | Green 800 - hover states |
| text | #dcfce7 | Green 100 - primary text |
| textSecondary | #86efac | Green 300 - muted text |
| border | #166534 | Green 800 - borders |
| userBubble | #22c55e | Green 500 - user message background |
| userBubbleText | #052e16 | Deep green - user message text |
| assistantBubble | #14532d | Green 900 - assistant background |
| assistantBubbleText | #dcfce7 | Green 100 - assistant text |
4. Sunset
A warm dark theme with orange accents. Perfect for creative, energetic, or entertainment apps.
<ChatWidget apiUrl="/api/chat" theme="sunset" />| Color Variable | Value | Description |
|----------------|-------|-------------|
| primary | #f97316 | Orange 500 - buttons, user bubbles |
| primaryHover | #ea580c | Orange 600 - hover states |
| background | #1c1917 | Stone 900 - main background |
| surface | #292524 | Stone 800 - cards, input area |
| surfaceHover | #44403c | Stone 700 - hover states |
| text | #fafaf9 | Stone 50 - primary text |
| textSecondary | #d6d3d1 | Stone 300 - muted text |
| border | #44403c | Stone 700 - borders |
| userBubble | #f97316 | Orange 500 - user message background |
| userBubbleText | #ffffff | White - user message text |
| assistantBubble | #292524 | Stone 800 - assistant background |
| assistantBubbleText | #fafaf9 | Stone 50 - assistant text |
5. Lavender
An elegant dark theme with purple accents. Great for luxury, creative, or design-focused apps.
<ChatWidget apiUrl="/api/chat" theme="lavender" />| Color Variable | Value | Description |
|----------------|-------|-------------|
| primary | #a855f7 | Purple 500 - buttons, user bubbles |
| primaryHover | #9333ea | Purple 600 - hover states |
| background | #1a0a2e | Deep purple - main background |
| surface | #2d1b4e | Dark purple - cards, input area |
| surfaceHover | #3b2d5c | Purple - hover states |
| text | #f3e8ff | Purple 100 - primary text |
| textSecondary | #d8b4fe | Purple 300 - muted text |
| border | #3b2d5c | Purple - borders |
| userBubble | #a855f7 | Purple 500 - user message background |
| userBubbleText | #ffffff | White - user message text |
| assistantBubble | #2d1b4e | Dark purple - assistant background |
| assistantBubbleText | #f3e8ff | Purple 100 - assistant text |
Customizing Colors
You can create a completely custom theme by passing a theme object instead of a preset name.
Required Colors (4 minimum)
At minimum, you must provide these 4 colors:
<ChatWidget
apiUrl="/api/chat"
theme={{
primary: '#7bc7c1', // Your brand color
background: '#ffffff', // Main background
surface: '#f5f5f5', // Cards, input area
text: '#333333', // Primary text
}}
/>All Theme Color Options
For complete control, you can specify all 14 color variables:
<ChatWidget
apiUrl="/api/chat"
theme={{
// Required colors
primary: '#7bc7c1', // Main accent color (buttons, highlights)
background: '#ffffff', // Chat window background
surface: '#fdf4e0', // Input area, assistant bubble background
text: '#2d2d2d', // Primary text color
// Optional colors (auto-generated if not provided)
primaryHover: '#5ba8a2', // Primary color on hover (default: darker primary)
surfaceHover: '#fefbf5', // Surface color on hover (default: lighter surface)
textSecondary: '#666666', // Muted/secondary text (default: lighter text)
border: '#e0e0e0', // Border color (default: same as surface)
userBubble: '#7bc7c1', // User message background (default: primary)
userBubbleText: '#ffffff', // User message text (default: white)
assistantBubble: '#fdf4e0', // Assistant message background (default: surface)
assistantBubbleText: '#2d2d2d', // Assistant message text (default: text)
error: '#ef4444', // Error text color (default: red)
errorBg: '#fef2f2', // Error background (default: light red)
}}
/>Light Mode Example
Create a clean light theme:
const lightTheme = {
primary: '#2563eb', // Blue 600
background: '#ffffff', // White
surface: '#f8fafc', // Slate 50
surfaceHover: '#f1f5f9', // Slate 100
text: '#1e293b', // Slate 800
textSecondary: '#64748b', // Slate 500
border: '#e2e8f0', // Slate 200
userBubble: '#2563eb', // Blue 600
userBubbleText: '#ffffff', // White
assistantBubble: '#f1f5f9', // Slate 100
assistantBubbleText: '#1e293b', // Slate 800
error: '#dc2626', // Red 600
errorBg: '#fef2f2', // Red 50
};
<ChatWidget apiUrl="/api/chat" theme={lightTheme} />Brand Color Example
Match your brand colors:
// Example: Teal brand with cream accents
const brandTheme = {
primary: '#7bc7c1', // Brand teal
primaryHover: '#5ba8a2', // Darker teal
background: '#ffffff', // White
surface: '#fdf4e0', // Cream
surfaceHover: '#fefbf5', // Light cream
text: '#2d2d2d', // Dark gray
textSecondary: '#666666', // Medium gray
border: 'rgba(123, 199, 193, 0.3)', // Teal with opacity
userBubble: '#7bc7c1', // Teal
userBubbleText: '#ffffff', // White
assistantBubble: '#fdf4e0', // Cream
assistantBubbleText: '#2d2d2d', // Dark gray
};
<ChatWidget apiUrl="/api/chat" theme={brandTheme} />All Customizable Props
Here's a complete reference of all props you can pass to ChatWidget:
Core Props
| Prop | Type | Default | Required | Description |
|------|------|---------|----------|-------------|
| apiUrl | string | - | Yes | Your chat API endpoint URL |
| theme | PresetName \| ThemeConfig | 'midnight' | No | Theme preset name or custom colors |
| lang | string | 'en' | No | Language code (any: 'en', 'es', 'fr', 'de', etc.) |
Appearance Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| position | 'bottom-right' \| 'bottom-left' | 'bottom-right' | Widget position on screen |
| width | string | '380px' | Chat window width (CSS value) |
| height | string | '500px' | Chat window height (CSS value) |
| zIndex | number | 50 | CSS z-index for the widget |
| className | string | - | Additional CSS class for root container |
| fontFamily | string | 'Inter, system-ui, sans-serif' | Custom font family (see Fonts) |
Content Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| title | string \| false | 'Chat Assistant' | Chat window header title. Set to '' or false to hide |
| headerIcon | ReactNode | - | Icon/image displayed left of the title (see Header) |
| placeholder | string | 'Type your message...' | Input field placeholder |
| greeting | string | - | Initial assistant message (optional) |
| labels | Partial<Labels> | - | Custom label overrides |
Behavior Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| defaultOpen | boolean | false | Start with chat window open |
| showSuggestions | boolean | true | Show clickable suggestion boxes below AI responses |
| rateLimitOptions | RateLimitOptions | { autoRetry: false } | Rate limit error handling options (see Rate Limit Handling) |
Resizing Props
The chat window is resizable by default. Users can drag any corner to resize.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| resizable | boolean | true | Enable user-resizable chat window |
| minWidth | string | '300px' | Minimum width constraint |
| maxWidth | string | '600px' | Maximum width constraint |
| minHeight | string | '400px' | Minimum height constraint |
| maxHeight | string | '80vh' | Maximum height constraint |
// Disable resizing
<ChatWidget apiUrl="/api/chat" resizable={false} />
// Custom constraints
<ChatWidget
apiUrl="/api/chat"
width="400px"
height="600px"
minWidth="320px"
maxWidth="800px"
minHeight="300px"
maxHeight="90vh"
/>Notes:
- Resize is automatically disabled on mobile devices (< 640px width)
- User-set sizes are persisted in localStorage
- The
widthandheightprops set the initial/default size
Icon Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| icon | CustomIcons | - | Custom icons for the floating button |
| icon.open | ReactNode | Chat bubble | Icon when chat is closed |
| icon.close | ReactNode | X icon | Icon when chat is open |
Usage Example with All Props
<ChatWidget
// Required
apiUrl="https://api.example.com/chat"
// Theme
theme="ocean"
// Language
lang="es"
// Appearance
position="bottom-left"
width="400px"
height="600px"
zIndex={100}
className="my-chat-widget"
// Content
title="Asistente de Soporte"
placeholder="Escribe tu pregunta..."
greeting="¡Hola! ¿En qué puedo ayudarte hoy?"
// Labels (partial override)
labels={{
thinking: 'Procesando',
error: 'Error de conexión',
retry: 'Intentar de nuevo',
}}
// Behavior
defaultOpen={false}
/>Customizing Labels
The widget includes built-in labels for English (en) and Spanish (es). For any other language, provide your own labels via the labels prop.
The lang prop accepts any string and is sent to your backend API, allowing your server to respond in the appropriate language.
Available Labels
| Label | English Default | Spanish Default | Description |
|-------|-----------------|-----------------|-------------|
| title | 'Chat Assistant' | 'Asistente de Chat' | Window header title |
| placeholder | 'Type your message...' | 'Escribe tu mensaje...' | Input placeholder |
| send | 'Send message' | 'Enviar mensaje' | Send button (accessibility) |
| close | 'Close chat' | 'Cerrar chat' | Close button (accessibility) |
| open | 'Open chat' | 'Abrir chat' | Open button (accessibility) |
| thinking | 'Thinking' | 'Pensando' | Loading indicator text |
| error | 'Something went wrong...' | 'Algo salió mal...' | Error message |
| retry | 'Retry' | 'Reintentar' | Retry button text |
| suggestionsTitle | 'Suggested questions' | 'Preguntas sugeridas' | Suggestions section title |
| restart | 'Restart chat' | 'Reiniciar chat' | Restart button (accessibility) |
| rateLimitError | 'Too many requests...' | 'Demasiadas solicitudes...' | Rate limit error message |
| rateLimitRetryIn | 'You can retry in' | 'Puedes reintentar en' | Retry countdown prefix |
| autoRetrying | 'Retrying in' | 'Reintentando en' | Auto-retry countdown prefix |
| cancelAutoRetry | 'Cancel' | 'Cancelar' | Cancel auto-retry button |
Partial Override
Only override the labels you need:
<ChatWidget
apiUrl="/api/chat"
lang="en"
labels={{
title: 'Support Bot',
thinking: 'Processing your request',
}}
/>Full Custom Labels
const customLabels = {
title: 'Help Center',
placeholder: 'Ask me anything...',
send: 'Send',
close: 'Close',
open: 'Get Help',
thinking: 'Looking up the answer',
error: 'Connection lost. Please try again.',
retry: 'Try Again',
suggestionsTitle: 'You might also ask:',
restart: 'Start Over',
};
<ChatWidget
apiUrl="/api/chat"
labels={customLabels}
/>Using Other Languages
For languages without built-in translations, provide all labels:
// French
<ChatWidget
apiUrl="/api/chat"
lang="fr"
labels={{
title: 'Assistant',
placeholder: 'Écrivez votre message...',
send: 'Envoyer',
close: 'Fermer',
open: 'Ouvrir le chat',
thinking: 'Réflexion',
error: 'Une erreur est survenue',
retry: 'Réessayer',
suggestionsTitle: 'Questions suggérées',
restart: 'Recommencer',
}}
/>
// German
<ChatWidget
apiUrl="/api/chat"
lang="de"
labels={{
title: 'Assistent',
placeholder: 'Nachricht eingeben...',
send: 'Senden',
close: 'Schließen',
open: 'Chat öffnen',
thinking: 'Denke nach',
error: 'Ein Fehler ist aufgetreten',
retry: 'Erneut versuchen',
suggestionsTitle: 'Vorgeschlagene Fragen',
restart: 'Neu starten',
}}
/>
// Portuguese (Brazil)
<ChatWidget
apiUrl="/api/chat"
lang="pt-BR"
labels={{
title: 'Assistente',
placeholder: 'Digite sua mensagem...',
send: 'Enviar',
close: 'Fechar',
open: 'Abrir chat',
thinking: 'Pensando',
error: 'Algo deu errado',
retry: 'Tentar novamente',
suggestionsTitle: 'Perguntas sugeridas',
restart: 'Reiniciar',
}}
/>Shortcut Props
For the most common customizations, use the shortcut props:
// These two are equivalent:
<ChatWidget title="Support" placeholder="Ask..." />
<ChatWidget labels={{ title: 'Support', placeholder: 'Ask...' }} />Custom Icons
The floating button icon is fully customizable. You can use any icon library or custom SVG.
Using Lucide React
import { ChatWidget } from '@joseantonionuevo/ai-chat-widget';
import { MessageCircle, X } from 'lucide-react';
<ChatWidget
apiUrl="/api/chat"
icon={{
open: <MessageCircle size={24} />,
close: <X size={24} />,
}}
/>Using Heroicons
import { ChatWidget } from '@joseantonionuevo/ai-chat-widget';
import { ChatBubbleLeftIcon, XMarkIcon } from '@heroicons/react/24/solid';
<ChatWidget
apiUrl="/api/chat"
icon={{
open: <ChatBubbleLeftIcon className="w-6 h-6" />,
close: <XMarkIcon className="w-6 h-6" />,
}}
/>Using FontAwesome
import { ChatWidget } from '@joseantonionuevo/ai-chat-widget';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faComments, faXmark } from '@fortawesome/free-solid-svg-icons';
<ChatWidget
apiUrl="/api/chat"
icon={{
open: <FontAwesomeIcon icon={faComments} size="lg" />,
close: <FontAwesomeIcon icon={faXmark} size="lg" />,
}}
/>Using Emoji or Text
<ChatWidget
apiUrl="/api/chat"
icon={{
open: '💬',
close: '✕',
}}
/>Using Custom SVG
<ChatWidget
apiUrl="/api/chat"
icon={{
open: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
</svg>
),
}}
/>Only Override One Icon
You can override just the open icon or just the close icon:
// Only custom open icon, default close icon
<ChatWidget
apiUrl="/api/chat"
icon={{ open: <MyCustomIcon /> }}
/>Default Icons
The widget exports its default icons for reuse:
import { ChatIcon, CloseIcon, SendIcon } from '@joseantonionuevo/ai-chat-widget';
// Use in your own components
<button><ChatIcon size={20} /></button>Customizing the Header
The chat window header can display an optional icon and/or title. Both are fully customizable and optional.
Header with Icon and Title
// With a logo image
<ChatWidget
apiUrl="/api/chat"
headerIcon={<img src="/logo.png" alt="Logo" style={{ width: 24, height: 24 }} />}
title="Support Chat"
/>
// With an icon component (Lucide, Heroicons, etc.)
import { Bot } from 'lucide-react';
<ChatWidget
apiUrl="/api/chat"
headerIcon={<Bot size={20} color="white" />}
title="AI Assistant"
/>
// With an emoji
<ChatWidget
apiUrl="/api/chat"
headerIcon="🤖"
title="Chat with Us"
/>Icon Only (No Title)
Hide the title to show only the icon:
<ChatWidget
apiUrl="/api/chat"
headerIcon={<img src="/logo.png" alt="" style={{ width: 24, height: 24 }} />}
title="" // or title={false}
/>Title Only (No Icon)
The default behavior—just specify a title:
<ChatWidget
apiUrl="/api/chat"
title="Help Center"
/>Minimal Header (No Icon, No Title)
For a clean, minimal look with just the close button:
<ChatWidget
apiUrl="/api/chat"
title={false}
/>Using Next.js Image
import Image from 'next/image';
<ChatWidget
apiUrl="/api/chat"
headerIcon={
<Image
src="/logo.png"
width={24}
height={24}
alt="Company Logo"
/>
}
title="Support"
/>Header Icon Sizing
The header icon container is 24x24 pixels. Your icon or image will be centered within this space:
- SVG icons: Use
size={20}or similar for best fit - Images: Will be contained within 24x24; use CSS for exact sizing
- Emoji: Displayed at 1.25rem for good visibility
Rate Limit Handling
The widget includes intelligent rate limit error detection and handling. When your API returns a 429 status or rate limit error, the widget automatically:
- Shows a specific "Too many requests" message instead of the generic error
- Displays a countdown timer showing when retry is available
- Optionally auto-retries with exponential backoff
Basic Usage
By default, rate limit errors show a specific message with manual retry:
// Rate limit messages are shown automatically, no config needed
<ChatWidget apiUrl="/api/chat" />Enable Auto-Retry
Enable automatic retry with exponential backoff:
<ChatWidget
apiUrl="/api/chat"
rateLimitOptions={{ autoRetry: true }}
/>Custom Retry Settings
Fine-tune the retry behavior:
<ChatWidget
apiUrl="/api/chat"
rateLimitOptions={{
autoRetry: true,
maxRetries: 5, // Max retry attempts (default: 3)
baseDelayMs: 2000, // Initial delay in ms (default: 1000)
maxDelayMs: 60000, // Maximum delay cap in ms (default: 30000)
}}
/>RateLimitOptions Reference
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| autoRetry | boolean | false | Enable automatic retry after rate limit |
| maxRetries | number | 3 | Maximum number of auto-retry attempts |
| baseDelayMs | number | 1000 | Base delay in milliseconds for exponential backoff |
| maxDelayMs | number | 30000 | Maximum delay cap in milliseconds |
How It Works
Error Detection: The widget detects rate limits via:
- HTTP status code 429
- Error messages containing "rate limit", "too many requests", "quota exceeded", etc.
Retry-Afterheader (if provided by your API)
Countdown Timer: Shows seconds until retry is available
Exponential Backoff: Each retry attempt doubles the wait time (with jitter):
- Attempt 1: ~1 second
- Attempt 2: ~2 seconds
- Attempt 3: ~4 seconds
- And so on, up to
maxDelayMs
Server Retry-After: If your API includes a
Retry-Afterheader, the widget respects it
Using the Utilities Directly
For advanced use cases, you can import the error classification utilities:
import { classifyError, isRateLimitError } from '@joseantonionuevo/ai-chat-widget';
// Quick check
if (isRateLimitError(error)) {
console.log('Rate limited!');
}
// Full classification
const errorInfo = classifyError(error);
if (errorInfo?.type === 'rate_limit') {
console.log('Retry after:', errorInfo.retryAfterSeconds, 'seconds');
console.log('HTTP status:', errorInfo.statusCode);
}Error Types
The classifier categorizes errors into these types:
| Type | Description |
|------|-------------|
| rate_limit | Too many requests (429 or matching message patterns) |
| network | Connection, timeout, or offline errors |
| server | Server errors (5xx status codes) |
| auth | Authentication/authorization errors (401, 403) |
| generic | All other errors |
Context-Based Suggestions
The widget can display clickable follow-up questions below each AI response. When a user clicks a suggestion, it's automatically sent as their next message.
How It Works
User: "What is your product about?"
AI: "Our product is an AI-powered wellness platform..."
┌─────────────────────────────────────────┐
│ 💡 Suggested questions: │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ What features does it include? │ │
│ └─────────────────────────────────────┘ │
│ ┌─────────────────────────────────────┐ │
│ │ How do I get started? │ │
│ └─────────────────────────────────────┘ │
│ ┌─────────────────────────────────────┐ │
│ │ Is there a free trial? │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘Enabling/Disabling Suggestions
Suggestions are enabled by default. To disable them:
<ChatWidget
apiUrl="/api/chat"
showSuggestions={false}
/>Backend Requirements for Suggestions
For suggestions to appear, your backend must send them as a data-suggestions part in the streaming response. The widget looks for this data type in the message parts:
// In your backend route handler
import { createUIMessageStream, createUIMessageStreamResponse, streamText } from 'ai';
export async function POST(req: Request) {
const { messages, lang } = await req.json();
const stream = createUIMessageStream({
execute: async ({ writer }) => {
const result = streamText({
model: yourModel,
messages,
async onFinish({ text }) {
// Generate suggestions based on the response
const suggestions = await generateSuggestions(text, lang);
// Send suggestions as a data part
if (suggestions.length > 0) {
writer.write({
type: 'data-suggestions',
data: suggestions, // Array of 3 strings
});
}
},
});
writer.merge(result.toUIMessageStream());
},
});
return createUIMessageStreamResponse({ stream });
}
async function generateSuggestions(aiResponse: string, lang: string): Promise<string[]> {
// Use your LLM to generate 3 follow-up questions
// based on the AI's response
return [
"What features does it include?",
"How do I get started?",
"Is there a free trial?",
];
}Suggestion Styling
Suggestions automatically use your theme colors:
- Background:
surfacecolor - Text:
textcolor - Border:
bordercolor - Hover:
primarycolor with white text
i18n for Suggestions
The suggestions title ("Suggested questions") is included in the built-in translations:
| Language | Label | |----------|-------| | English | "Suggested questions" | | Spanish | "Preguntas sugeridas" |
Override it via the labels prop:
<ChatWidget
apiUrl="/api/chat"
labels={{
suggestionsTitle: 'You might also ask:',
}}
/>Customizing Fonts
Default Font: Inter
The widget uses Inter as its default font—a modern, free, open-source typeface designed specifically for computer screens. Inter is:
- Optimized for readability at small sizes
- Free and open-source (SIL Open Font License)
- Loaded automatically from Google Fonts when the widget renders
No configuration needed—Inter loads automatically with weights 400, 500, and 600.
Using Your App's Font
If your app already uses a custom font, pass it to the widget to maintain visual consistency:
// Match your app's typography
<ChatWidget
apiUrl="/api/chat"
fontFamily="'Plus Jakarta Sans', sans-serif"
/>Important: When specifying a custom font, ensure it's already loaded in your app (via Google Fonts, @font-face, or your framework's font loading mechanism). The widget won't load external fonts automatically when fontFamily is provided.
Using System Fonts Only
To avoid loading any external fonts and use the user's system fonts:
// Zero external font requests
<ChatWidget
apiUrl="/api/chat"
fontFamily="system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
/>Font Examples
// Google Fonts (load in your app first)
<ChatWidget fontFamily="'Nunito', sans-serif" />
<ChatWidget fontFamily="'Poppins', sans-serif" />
<ChatWidget fontFamily="'Source Sans 3', sans-serif" />
// Monospace for developer tools
<ChatWidget fontFamily="'JetBrains Mono', 'Fira Code', monospace" />
// Elegant serif
<ChatWidget fontFamily="'Merriweather', Georgia, serif" />
// Default (Inter - no prop needed)
<ChatWidget apiUrl="/api/chat" />Loading Google Fonts in Your App
If using a custom Google Font, add it to your app:
Next.js (using next/font):
import { Nunito } from 'next/font/google';
const nunito = Nunito({ subsets: ['latin'] });
// In your layout
<ChatWidget fontFamily={nunito.style.fontFamily} />Astro (using <link>):
---
// In your Layout.astro <head>
---
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600&display=swap" rel="stylesheet" />
<!-- Then in your ChatWidget config -->
<script>
// fontFamily: "'Nunito', sans-serif"
</script>Plain HTML:
<head>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600&display=swap" rel="stylesheet">
</head>Backend Requirements
Your API endpoint must implement the Vercel AI SDK v6 streaming protocol.
Request Format
The widget sends POST requests with this JSON body:
{
"messages": [
{
"id": "msg_1",
"role": "user",
"parts": [{ "type": "text", "text": "Hello, how are you?" }]
},
{
"id": "msg_2",
"role": "assistant",
"parts": [{ "type": "text", "text": "I'm doing well! How can I help?" }]
}
],
"lang": "en"
}Response Format
Return a streaming response using toUIMessageStreamResponse():
// Next.js App Router example
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export async function POST(req: Request) {
const { messages, lang } = await req.json();
const result = streamText({
model: openai('gpt-4o'),
system: `You are a helpful assistant. Respond in ${lang === 'es' ? 'Spanish' : 'English'}.`,
messages,
});
return result.toUIMessageStreamResponse();
}Example with Different Providers
// OpenAI
import { openai } from '@ai-sdk/openai';
const model = openai('gpt-4o');
// Anthropic Claude
import { anthropic } from '@ai-sdk/anthropic';
const model = anthropic('claude-3-5-sonnet-20241022');
// Google Gemini
import { google } from '@ai-sdk/google';
const model = google('gemini-1.5-pro');
// Groq (fast inference)
import { groq } from '@ai-sdk/groq';
const model = groq('llama-3.3-70b-versatile');Framework Integration
Astro (Recommended Approach)
Astro requires a special approach because it's not a React framework by default. The recommended pattern uses script-based lazy loading for zero Core Web Vitals impact.
Why This Approach?
| Benefit | Explanation |
|---------|-------------|
| Zero LCP impact | Widget loads after main content is painted |
| Zero INP impact | Uses requestIdleCallback, doesn't block main thread |
| Zero CLS impact | Fixed position element, no layout shift |
| No SSR issues | Pure Astro component, no React renderer needed at build |
| Lazy loading | React and widget JS only load when browser is idle |
Step 1: Install Dependencies
# Install the widget and peer dependencies
pnpm add @joseantonionuevo/ai-chat-widget react react-dom @ai-sdk/react ai
# Optional: For markdown rendering
pnpm add react-markdown remark-gfmStep 2: Create the Loader Component
Create src/components/ChatWidgetLoader.astro:
---
/**
* ChatWidgetLoader - Performance-optimized chat widget loader
*
* Loads the chat widget using requestIdleCallback for zero Core Web Vitals impact.
*/
interface Props {
apiUrl: string;
lang?: 'es' | 'en';
}
const { apiUrl, lang = 'en' } = Astro.props;
---
<div id="chat-widget-container" data-api-url={apiUrl} data-lang={lang}></div>
<script>
function loadChatWidget() {
const container = document.getElementById('chat-widget-container');
if (!container) return;
const apiUrl = container.dataset.apiUrl;
const lang = container.dataset.lang as 'es' | 'en';
// Dynamically import React and the widget only when needed
Promise.all([
import('react'),
import('react-dom/client'),
import('@joseantonionuevo/ai-chat-widget')
]).then(([React, ReactDOM, { ChatWidget }]) => {
const root = ReactDOM.createRoot(container);
root.render(
React.createElement(ChatWidget, {
apiUrl,
lang,
position: 'bottom-right'
})
);
}).catch((err) => {
console.error('[ChatWidget] Failed to load:', err);
});
}
// Use requestIdleCallback for best performance, fallback to setTimeout
if ('requestIdleCallback' in window) {
requestIdleCallback(loadChatWidget, { timeout: 3000 });
} else {
setTimeout(loadChatWidget, 1000);
}
</script>Step 3: Add to Your Layout
In your src/layouts/Layout.astro:
---
import ChatWidgetLoader from "../components/ChatWidgetLoader.astro";
const chatApiUrl = import.meta.env.PUBLIC_CHAT_API_URL || '/api/chat';
---
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... your head content ... -->
</head>
<body>
<slot />
<ChatWidgetLoader apiUrl={chatApiUrl} lang="en" />
</body>
</html>Astro with Custom Theme
To use a custom theme (like brand colors), modify the loader:
---
interface Props {
apiUrl: string;
lang?: 'es' | 'en';
}
const { apiUrl, lang = 'en' } = Astro.props;
---
<div id="chat-widget-container" data-api-url={apiUrl} data-lang={lang}></div>
<script>
function loadChatWidget() {
const container = document.getElementById('chat-widget-container');
if (!container) return;
const apiUrl = container.dataset.apiUrl;
const lang = container.dataset.lang as 'es' | 'en';
// Custom theme matching your brand
const customTheme = {
primary: '#7BC7C1', // Your brand color
primaryHover: '#5BA8A2',
background: '#ffffff',
surface: '#FDF4E0', // Cream/beige
surfaceHover: '#FFF9F0',
text: '#2d2d2d',
textSecondary: '#666666',
border: 'rgba(123, 199, 193, 0.3)',
userBubble: '#7BC7C1',
userBubbleText: '#ffffff',
assistantBubble: '#FDF4E0',
assistantBubbleText: '#2d2d2d',
error: '#ef4444',
errorBg: '#fef2f2',
};
Promise.all([
import('react'),
import('react-dom/client'),
import('@joseantonionuevo/ai-chat-widget')
]).then(([React, ReactDOM, { ChatWidget }]) => {
const root = ReactDOM.createRoot(container);
root.render(
React.createElement(ChatWidget, {
apiUrl,
lang,
position: 'bottom-right',
theme: customTheme,
title: lang === 'es' ? 'Asistente' : 'Assistant',
greeting: lang === 'es'
? '¡Hola! ¿En qué puedo ayudarte?'
: 'Hi! How can I help you?',
})
);
}).catch((err) => {
console.error('[ChatWidget] Failed to load:', err);
});
}
if ('requestIdleCallback' in window) {
requestIdleCallback(loadChatWidget, { timeout: 3000 });
} else {
setTimeout(loadChatWidget, 1000);
}
</script>Performance Timeline
Page Load:
├── HTML arrives (no widget JS blocking)
├── CSS and critical JS load
├── LCP fires (main content visible)
├── Browser becomes idle
└── Widget loads via requestIdleCallback (~1-3s after load)
├── React imports dynamically (~50KB)
├── Widget imports dynamically (~15KB)
└── Chat button renders (user can now interact)Next.js (App Router)
Next.js requires a client component wrapper because the widget uses browser APIs and React hooks.
Why Use Dynamic Import with ssr: false?
- Avoids hydration errors: Widget uses browser APIs (window, document)
- Code splitting: Widget JS only loads on client
- Better performance: Doesn't block server rendering
- Prevents SSR issues: React hooks can't run on server
Step 1: Install Dependencies
# Install the widget and peer dependencies
pnpm add @joseantonionuevo/ai-chat-widget @ai-sdk/react ai
# Required for markdown rendering
pnpm add react-markdown remark-gfmStep 2: Create Client Component
Create app/components/ChatWidgetClient.tsx:
'use client';
/**
* ChatWidgetClient - Client-only wrapper for the chat widget
*
* Uses next/dynamic with ssr: false to load the widget only on the client.
* This avoids SSR issues with browser-only APIs used by the widget.
*/
import dynamic from 'next/dynamic';
// Dynamic import with SSR disabled - REQUIRED
const ChatWidget = dynamic(
() => import('@joseantonionuevo/ai-chat-widget').then((mod) => mod.ChatWidget),
{
ssr: false, // Critical: prevents server-side rendering
loading: () => null, // No loading state (button appears when ready)
}
);
interface ChatWidgetClientProps {
apiUrl: string;
lang?: 'en' | 'es';
}
export default function ChatWidgetClient({ apiUrl, lang = 'en' }: ChatWidgetClientProps) {
return (
<ChatWidget
apiUrl={apiUrl}
lang={lang}
position="bottom-right"
/>
);
}Step 3: Add to Root Layout
In app/layout.tsx:
import ChatWidgetClient from './components/ChatWidgetClient';
const chatApiUrl = process.env.NEXT_PUBLIC_CHAT_API_URL || '/api/chat';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
<ChatWidgetClient apiUrl={chatApiUrl} />
</body>
</html>
);
}Next.js with next-intl (i18n)
If you're using next-intl for internationalization:
'use client';
import dynamic from 'next/dynamic';
import { useLocale } from 'next-intl';
const ChatWidget = dynamic(
() => import('@joseantonionuevo/ai-chat-widget').then((mod) => mod.ChatWidget),
{ ssr: false, loading: () => null }
);
interface ChatWidgetClientProps {
apiUrl: string;
}
export default function ChatWidgetClient({ apiUrl }: ChatWidgetClientProps) {
const locale = useLocale();
const lang = locale === 'en' ? 'en' : 'es';
return (
<ChatWidget
apiUrl={apiUrl}
lang={lang}
position="bottom-right"
/>
);
}Next.js: Hide Widget on Specific Routes
To hide the widget on certain pages (e.g., if you have a dedicated chat page):
'use client';
import dynamic from 'next/dynamic';
import { usePathname } from 'next/navigation';
const ChatWidget = dynamic(
() => import('@joseantonionuevo/ai-chat-widget').then((mod) => mod.ChatWidget),
{ ssr: false, loading: () => null }
);
interface ChatWidgetClientProps {
apiUrl: string;
}
export default function ChatWidgetClient({ apiUrl }: ChatWidgetClientProps) {
const pathname = usePathname();
// Hide widget on specific routes
const hiddenRoutes = ['/chat', '/admin', '/checkout'];
const shouldHide = hiddenRoutes.some(route => pathname?.startsWith(route));
if (shouldHide) {
return null;
}
return (
<ChatWidget
apiUrl={apiUrl}
position="bottom-right"
/>
);
}Next.js with Custom Theme
'use client';
import dynamic from 'next/dynamic';
const ChatWidget = dynamic(
() => import('@joseantonionuevo/ai-chat-widget').then((mod) => mod.ChatWidget),
{ ssr: false, loading: () => null }
);
// Define your brand theme
const brandTheme = {
primary: '#7BC7C1',
primaryHover: '#5BA8A2',
background: '#ffffff',
surface: '#FDF4E0',
surfaceHover: '#FFF9F0',
text: '#2d2d2d',
textSecondary: '#666666',
border: 'rgba(123, 199, 193, 0.3)',
userBubble: '#7BC7C1',
userBubbleText: '#ffffff',
assistantBubble: '#FDF4E0',
assistantBubbleText: '#2d2d2d',
};
interface ChatWidgetClientProps {
apiUrl: string;
lang?: 'en' | 'es';
}
export default function ChatWidgetClient({ apiUrl, lang = 'en' }: ChatWidgetClientProps) {
return (
<ChatWidget
apiUrl={apiUrl}
lang={lang}
position="bottom-right"
theme={brandTheme}
title={lang === 'es' ? 'Asistente' : 'Assistant'}
greeting={lang === 'es'
? '¡Hola! ¿En qué puedo ayudarte?'
: 'Hi! How can I help you?'
}
/>
);
}Next.js: Creating the Backend API
Create app/api/chat/route.ts:
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export const runtime = 'edge'; // Optional: Use edge runtime for faster responses
export async function POST(req: Request) {
const { messages, lang } = await req.json();
const result = streamText({
model: openai('gpt-4o'),
system: `You are a helpful assistant. Respond in ${lang === 'es' ? 'Spanish' : 'English'}.`,
messages,
});
return result.toUIMessageStreamResponse();
}Vite / Create React App
For standard React apps:
// App.tsx
import { ChatWidget } from '@joseantonionuevo/ai-chat-widget';
function App() {
return (
<>
<YourAppContent />
<ChatWidget
apiUrl={import.meta.env.VITE_CHAT_API_URL || '/api/chat'}
theme="midnight"
/>
</>
);
}
export default App;Remix
// app/root.tsx
import { ChatWidget } from '@joseantonionuevo/ai-chat-widget';
export default function App() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Outlet />
<ChatWidget apiUrl="/api/chat" />
<Scripts />
</body>
</html>
);
}Advanced Usage
Accessing Theme Presets Programmatically
import { presets, resolveTheme } from '@joseantonionuevo/ai-chat-widget';
// Get a preset directly
console.log(presets.midnight);
// { name: 'midnight', primary: '#6366f1', ... }
// Resolve any theme (preset name or custom config)
const resolved = resolveTheme('ocean');
const custom = resolveTheme({ primary: '#ff0000', background: '#000', surface: '#111', text: '#fff' });Using Individual Components
For complete customization, import individual components:
import {
ChatButton,
ChatWindow,
MessageBubble,
MessageInput,
LoadingIndicator,
resolveTheme,
getLabels,
} from '@joseantonionuevo/ai-chat-widget';
// Build your own chat interface
function CustomChat() {
const theme = resolveTheme('ocean');
const labels = getLabels('en');
// Your custom implementation...
}Using the Rate Limit Hook
For custom error handling, use the useRateLimitRetry hook:
import { useRateLimitRetry, classifyError } from '@joseantonionuevo/ai-chat-widget';
function CustomErrorHandler({ error, onRetry }) {
const errorInfo = classifyError(error);
const {
countdown,
isAutoRetrying,
cancelAutoRetry,
manualRetry,
} = useRateLimitRetry({
errorInfo,
onRetry,
autoRetry: true,
maxRetries: 3,
});
if (errorInfo?.type === 'rate_limit') {
return (
<div>
<p>Rate limited! {countdown > 0 && `Retry in ${countdown}s`}</p>
{isAutoRetrying ? (
<button onClick={cancelAutoRetry}>Cancel</button>
) : (
<button onClick={manualRetry}>Retry Now</button>
)}
</div>
);
}
return <p>Error: {error.message}</p>;
}Conditional Rendering
import { ChatWidget } from '@joseantonionuevo/ai-chat-widget';
import { usePathname } from 'next/navigation';
function ConditionalChat() {
const pathname = usePathname();
// Hide on certain pages
if (pathname.startsWith('/admin') || pathname.startsWith('/checkout')) {
return null;
}
return <ChatWidget apiUrl="/api/chat" />;
}TypeScript
Full TypeScript support is included. Import types as needed:
import type {
// Main props
ChatWidgetProps,
// Theme types
ThemeConfig,
ThemeProp,
PresetThemeName,
ResolvedTheme,
// i18n types
Lang,
Labels,
// Error handling types
RateLimitOptions,
ErrorInfo,
ErrorType,
// Misc
Position,
} from '@joseantonionuevo/ai-chat-widget';
// Example: Type-safe custom theme
const myTheme: ThemeConfig = {
primary: '#ff6b6b',
background: '#1a1a2e',
surface: '#16213e',
text: '#eaeaea',
};
// Example: Type-safe props
const widgetProps: ChatWidgetProps = {
apiUrl: '/api/chat',
theme: myTheme,
lang: 'en',
position: 'bottom-right',
};Dependencies
This package is built with:
| Dependency | Version | Purpose | |------------|---------|---------| | Vercel AI SDK | v6.0.62 | Streaming chat, LLM integrations | | React | v19 | UI framework | | TypeScript | v5.9 | Type safety |
Optional dependencies for enhanced features:
| Dependency | Purpose | |------------|---------| | react-markdown | Markdown rendering | | remark-gfm | GitHub Flavored Markdown |
Contributing
Contributions are welcome! Please read our Contributing Guide for details on:
- Development setup
- Code style guidelines
- How to add new themes
- How to add new languages
- Pull request process
License
MIT License - see LICENSE for details.
Support
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Made with love by Jose Antonio Nuevo
