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

react-agent-widget

v0.2.4

Published

Drop-in React chatbot and AI agent widget — streaming chat UI for OpenAI, Anthropic, AWS Bedrock, Azure OpenAI, or any custom API. Fully themeable, TypeScript-first, zero CSS-in-JS.

Readme

react-agent-widget

npm version npm downloads bundle size license TypeScript Open in CodeSandbox

→ Live demo on CodeSandbox — no install, no API key, works in your browser.

The fastest way to add a production-ready AI chatbot to any React app.

Drop in one component. Connect to OpenAI, Anthropic Claude, AWS Bedrock, Azure OpenAI, or your own API. Get a streaming chat UI with a floating launcher button, full theming, and zero style conflicts — in under 10 lines of code.


Why react-agent-widget?

| Feature | react-agent-widget | |---|---| | Works with any AI provider | OpenAI, Anthropic, AWS Bedrock, Azure OpenAI, custom HTTP | | Streaming responses | Built-in SSE parser, token-by-token rendering | | File & image upload | Images, videos, and files — thumbnails in chat, base64 sent to your adapter | | Voice input | One-prop mic button, live transcription, any language via Web Speech API | | Fully themeable | CSS custom properties — change every color, radius, shadow, font | | Generative UI | Agent renders React components (cards, forms, charts) inside the chat | | TypeScript-first | Full type safety, no any in the public API | | Zero CSS conflicts | Scoped raw- class prefix, no global styles, no CSS-in-JS runtime | | Headless mode | Use your own UI with the built-in state/streaming hooks | | Lightweight | ~44 KB minified ESM, React 18 peer dep, no mandatory runtime deps | | SSR-safe | No window access at render time — works with Next.js App Router |


Installation

npm install react-agent-widget
# or
yarn add react-agent-widget
# or
pnpm add react-agent-widget

Requires React 18+ as a peer dependency.


Quick start — 10 lines

import { AgentWidget, createHttpAdapter } from 'react-agent-widget'

const adapter = createHttpAdapter({ url: '/api/chat' })

export default function App() {
  return (
    <AgentWidget
      adapter={adapter}
      welcomeMessage="Hi! How can I help you today?"
    />
  )
}

Your /api/chat endpoint receives { messages, stream } and returns a streaming SSE response or plain JSON. No API keys go in the browser — your server adds credentials.

That's it. A floating chat button appears at the bottom-right of the page.


Connecting to AI providers

All adapters point at a proxy endpoint you control on your server. Your server adds the API key, SigV4 signature, or managed identity — never the browser.

OpenAI / ChatGPT

import { AgentWidget, createOpenAIAdapter } from 'react-agent-widget'

<AgentWidget
  adapter={createOpenAIAdapter({
    proxyUrl: '/api/openai',   // your server forwards to api.openai.com + adds Authorization header
    model: 'gpt-4o',
  })}
/>

Anthropic Claude

import { AgentWidget, createAnthropicAdapter } from 'react-agent-widget'

<AgentWidget
  adapter={createAnthropicAdapter({
    proxyUrl: '/api/anthropic',   // your server adds x-api-key + Anthropic-Version headers
    model: 'claude-3-5-sonnet-20241022',
  })}
/>

AWS Bedrock

import { AgentWidget, createBedrockAdapter } from 'react-agent-widget'

<AgentWidget
  adapter={createBedrockAdapter({
    proxyUrl: '/api/bedrock',   // your server adds SigV4 auth
    model: 'anthropic.claude-3-5-sonnet-20241022-v2:0',
  })}
/>

Azure OpenAI

import { AgentWidget, createAzureOpenAIAdapter } from 'react-agent-widget'

<AgentWidget
  adapter={createAzureOpenAIAdapter({
    proxyUrl: '/api/azure',   // your server adds api-key / managed identity
    deploymentName: 'gpt-4o',
  })}
/>

Custom HTTP endpoint / BFF

import { AgentWidget, createHttpAdapter } from 'react-agent-widget'

<AgentWidget
  adapter={createHttpAdapter({
    url: '/api/chat',
    // optional: customize the request body
    formatRequest: (messages, options) => ({
      history: messages,
      config: { stream: true },
    }),
  })}
/>

Full API

<AgentWidget
  // required
  adapter={adapter}

  // launcher button
  launcher={{
    position: { side: 'right' },   // 'left' | 'right'
    label: 'Chat with AI',
    icon: <MyIcon />,              // custom icon
    badge: 3,                      // unread count bubble
  }}

  // theme — override any token
  theme={{
    mode: 'dark',                   // 'light' | 'dark'
    colors: {
      primary: '#7c3aed',
      userBubble: '#7c3aed',
      background: '#09090b',
    },
    radius: { panel: '20px', launcher: '12px' },
  }}

  // per-slot class names (add your own CSS on top)
  classNames={{
    panel: 'my-panel',
    messageUser: 'my-user-msg',
    messageAgent: 'my-agent-msg',
    composer: 'my-composer',
  }}

  // chat behavior
  welcomeMessage="Hi! How can I help?"
  suggestedPrompts={['Summarize this page', 'Help me write an email']}
  systemPrompt="You are a helpful assistant for Acme Corp."
  placeholder="Ask me anything…"
  renderMarkdown={true}            // render markdown in agent replies (default: true)

  // generative UI — agent renders React components
  components={{
    weatherCard: WeatherCard,
    approvalPrompt: ApprovalPrompt,
  }}

  // events
  onMessage={(msg) => analytics.track('chat_message', msg)}
  onError={(err) => errorReporter.capture(err)}
  onOpen={() => analytics.track('chat_opened')}
  onClose={() => analytics.track('chat_closed')}

  // file & image upload
  upload={{
    accept: 'image/*,video/*,.pdf,.docx',  // passed to <input accept>
    maxFiles: 5,                            // default: 5
    maxFileSizeBytes: 10 * 1024 * 1024,    // 10 MB limit per file
  }}

  // voice input (Web Speech API — no external dependency)
  voice={{
    // lang omitted → resolves to navigator.language automatically
    // lang: 'en-US'  explicit BCP-47 tag to override auto-detection
  }}

  // controlled open state (optional)
  open={isOpen}
  onOpenChange={setIsOpen}
/>

Theming

The widget uses CSS custom properties for all visual tokens. Pass a theme object — the widget applies the values as inline CSS variables on its root element, so your host-app styles are never affected.

// Dark purple theme example
<AgentWidget
  adapter={adapter}
  theme={{
    mode: 'dark',
    colors: {
      primary: '#8b5cf6',
      primaryForeground: '#ffffff',
      background: '#09090b',
      surface: '#18181b',
      userBubble: '#8b5cf6',
      agentBubble: '#27272a',
      agentBubbleForeground: '#fafafa',
    },
    radius: {
      panel: '24px',
      message: '20px',
      launcher: '16px',
    },
  }}
/>

Available CSS variables (for stylesheet overrides)

/* Colors */
--raw-color-primary        --raw-color-primary-fg
--raw-color-background     --raw-color-surface      --raw-color-surface-fg
--raw-color-border         --raw-color-muted        --raw-color-muted-fg
--raw-color-error          --raw-color-error-fg
--raw-color-user-bubble    --raw-color-user-bubble-fg
--raw-color-agent-bubble   --raw-color-agent-bubble-fg

/* Typography */
--raw-font-family          --raw-font-size-sm   --raw-font-size-md   --raw-font-size-lg

/* Radius */
--raw-radius-panel         --raw-radius-message
--raw-radius-launcher      --raw-radius-composer

/* Shadow / z-index */
--raw-shadow-panel         --raw-shadow-launcher
--raw-z-panel              --raw-z-launcher

Generative UI

Let the AI render interactive React components directly inside the chat — not just text.

// 1. Define your component
function WeatherCard({ payload, onResult }) {
  const { city, temperature, condition } = payload.props
  return (
    <div style={{ padding: 16, borderRadius: 12, background: '#f0f9ff' }}>
      <h3>{city} — {temperature}°C, {condition}</h3>
      <button onClick={() => onResult?.({ action: 'show_forecast', city })}>
        Show 7-day forecast
      </button>
    </div>
  )
}

// 2. Register it
<AgentWidget
  adapter={adapter}
  components={{ weatherCard: WeatherCard }}
/>
// 3. Your agent/tool emits this SSE event:
data: {"type":"component","payload":{"type":"weatherCard","props":{"city":"London","temperature":15,"condition":"Cloudy"}}}

The widget renders <WeatherCard> in-stream. When the user clicks the button, onResult fires and the widget sends the result back into the conversation automatically.


File & image upload

Enable the paperclip button in the composer with the upload prop.

<AgentWidget
  adapter={adapter}
  upload={{
    accept: 'image/*,video/*,.pdf,.docx,.txt',
    maxFiles: 5,
    maxFileSizeBytes: 10 * 1024 * 1024,   // 10 MB
  }}
/>

What the user sees: a paperclip icon in the composer. Selecting files shows a preview strip — images as thumbnails, videos and documents as chips with name and size. Each attachment has an ✕ button to remove it before sending.

What your adapter receives: message.attachments on every UserMessage, typed as MessageAttachment[]:

interface MessageAttachment {
  id: string
  name: string
  size: number
  mimeType: string
  kind: 'image' | 'video' | 'file'
  dataUrl?: string   // base64 data URL for files ≤ 10 MB
}

Forward dataUrl directly to a multimodal model (GPT-4o Vision, Claude, etc.):

// In your /api/chat proxy:
const { messages } = await req.json()

const openaiMessages = messages.map(msg => {
  if (msg.role !== 'user' || !msg.attachments?.length) return msg
  return {
    role: 'user',
    content: [
      { type: 'text', text: msg.content },
      ...msg.attachments
        .filter(a => a.kind === 'image' && a.dataUrl)
        .map(a => ({
          type: 'image_url',
          image_url: { url: a.dataUrl },
        })),
    ],
  }
})

In the chat bubble: images render as <img>, videos as <video controls>, other files as a filename chip.


Voice input

Enable the mic button with the voice prop. Uses the browser's Web Speech API — no external library, no API key, no extra bundle size.

<AgentWidget
  adapter={adapter}
  voice={{}}           // lang omitted → auto-detected from navigator.language
  // voice={{ lang: 'en-US' }}  explicit BCP-47 tag for a specific language
/>

How it works:

  1. A mic button appears next to the composer input
  2. Click to start — the button pulses red, shows a crossed-out mic icon, the textarea border turns red, placeholder changes to "Listening…"
  3. As you speak, partial words appear below the textarea in real time
  4. Each finalised phrase is appended to the textarea
  5. Click the mic again (crossed-out icon) to stop recording without sending, or press Enter / click Send to stop and submit immediately

Language detection resolves in this priority order:

| Priority | Source | Example | |---|---|---| | 1 | Explicit lang prop | lang: 'fr-FR' | | 2 | navigator.language (browser/OS language) | 'en-GB', 'ja-JP' | | 3 | Browser recognition service default | — |

Omitting lang (or setting it to '') automatically uses the browser's configured language, so it works correctly for users around the world without any extra configuration.

For a fixed language, pass a BCP-47 tag:

voice={{ lang: 'fr-FR' }}  // French
voice={{ lang: 'ja-JP' }}  // Japanese
voice={{ lang: 'ar-SA' }}  // Arabic
voice={{ lang: 'zh-CN' }}  // Mandarin

Headless usage — use the hook directly in your own UI:

import { useSpeechRecognition } from 'react-agent-widget'

function MyMicButton() {
  const { isListening, isSupported, interimTranscript, start, stop } =
    useSpeechRecognition({
      // lang omitted → resolves to navigator.language automatically
      onFinalTranscript: (text) => console.log('said:', text),
    })

  if (!isSupported) return null

  return (
    <button onClick={isListening ? stop : start}>
      {isListening ? `Hearing: ${interimTranscript}` : 'Speak'}
    </button>
  )
}

Browser support: Chrome, Edge, and Safari 14.1+. The mic button is automatically hidden on browsers that don't support the API.


Headless mode

Bring your own UI — use just the state management and streaming logic.

import {
  WidgetProvider,
  useWidget,
  ThemeProvider,
  createOpenAIAdapter,
} from 'react-agent-widget'

const adapter = createOpenAIAdapter({ proxyUrl: '/api/openai' })

function MyChatUI() {
  const { messages, sendMessage, isStreaming, isOpen, setIsOpen } = useWidget()

  return (
    <div>
      {messages.map(msg => (
        <div key={msg.id} className={msg.role}>
          {msg.content}
        </div>
      ))}
      <input
        onKeyDown={(e) => {
          if (e.key === 'Enter') void sendMessage(e.currentTarget.value)
        }}
      />
    </div>
  )
}

export function App() {
  return (
    <ThemeProvider>
      <WidgetProvider adapter={adapter}>
        <MyChatUI />
      </WidgetProvider>
    </ThemeProvider>
  )
}

Writing a proxy endpoint

Your backend receives the messages and forwards them to the AI provider. Here is a minimal Next.js example for OpenAI:

// app/api/openai/route.ts
import OpenAI from 'openai'

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })

export async function POST(req: Request) {
  const { messages } = await req.json()

  const stream = openai.beta.chat.completions.stream({
    model: 'gpt-4o',
    messages,
  })

  return new Response(stream.toReadableStream(), {
    headers: { 'Content-Type': 'text/event-stream' },
  })
}

The widget's SSE parser understands OpenAI's native streaming format automatically.


SSE format (custom backends)

If you write your own backend, use this simple format:

data: {"type":"text-delta","delta":"Hello"}
data: {"type":"text-delta","delta":", world!"}
data: {"type":"component","payload":{"type":"weatherCard","props":{...}}}
data: {"type":"done"}

Or just return data: [DONE] as the final event (OpenAI convention — also understood).


Building a custom Adapter

Implement the Adapter interface to connect any AI service:

import type { Adapter } from 'react-agent-widget'

export const myAdapter: Adapter = {
  name: 'my-provider',
  capabilities: { streaming: true, tools: false, multimodal: false },
  async *send(messages, options) {
    const res = await fetch('/api/my-llm', {
      method: 'POST',
      body: JSON.stringify({ messages }),
      signal: options?.signal,
    })
    // yield chunks…
    yield { type: 'text-delta', delta: 'Hello' }
    yield { type: 'done' }
  },
}

TypeScript

Full type safety — no any in the public API.

import type {
  Adapter,
  AdapterCapabilities,
  StreamChunk,
  Message,
  UserMessage,
  AssistantMessage,
  MessageAttachment,
  Theme,
  ClassNamesConfig,
  AgentWidgetProps,
  GenerativeUIComponent,
  UploadConfig,
  VoiceConfig,
  SendMessageOptions,
} from 'react-agent-widget'

Accessibility

  • Keyboard navigable — Tab, Enter, Escape to close, full focus management
  • ARIA: role="dialog", aria-modal, aria-live="polite" on message list
  • Respects prefers-reduced-motion (animations disabled automatically)
  • Default theme meets WCAG 2.1 AA contrast ratios

Browser & framework support

| Environment | Supported | |---|---| | React | 18+ | | Next.js (App Router) | Yes — SSR-safe | | Next.js (Pages Router) | Yes | | Vite | Yes | | Remix | Yes | | Webpack | Yes | | Browsers | All modern evergreen (Chrome, Firefox, Safari, Edge) |


License

MIT — TilangaPramith