npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@joseantonionuevo/ai-chat-widget

v1.5.1

Published

A ready-to-use, customizable chat widget for AI applications using Vercel AI SDK v6

Readme

AI Chat Widget

npm version license

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

# npm
npm install @joseantonionuevo/ai-chat-widget

# pnpm
pnpm add @joseantonionuevo/ai-chat-widget

# yarn
yarn add @joseantonionuevo/ai-chat-widget

Peer 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 ai

Optional Dependencies (for Markdown)

For rich markdown rendering in assistant responses:

npm install react-markdown remark-gfm

If 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 width and height props 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

  1. Error Detection: The widget detects rate limits via:

    • HTTP status code 429
    • Error messages containing "rate limit", "too many requests", "quota exceeded", etc.
    • Retry-After header (if provided by your API)
  2. Countdown Timer: Shows seconds until retry is available

  3. 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
  4. Server Retry-After: If your API includes a Retry-After header, 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: surface color
  • Text: text color
  • Border: border color
  • Hover: primary color 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-gfm

Step 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?

  1. Avoids hydration errors: Widget uses browser APIs (window, document)
  2. Code splitting: Widget JS only loads on client
  3. Better performance: Doesn't block server rendering
  4. 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-gfm

Step 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


Made with love by Jose Antonio Nuevo