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

reactuibotwidget

v0.2.0

Published

A small, dependency-free floating chatbot widget for React.

Readme

reactuibotwidget

Floating chatbot widget for React. Replies can be plain text or rich parts — product cards, quick-reply chips, status cards, approve/reject gates — rendered by components you supply. Styles included, React is a peer dependency.

Install

npm install reactuibotwidget

Basic usage

Add it once near the root of your app. It pins itself to the bottom-right corner.

import { ChatBot } from 'reactuibotwidget'

async function sendToBackend(message) {
  const res = await fetch('/api/chat', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ message }),
  })
  const { reply } = await res.json()
  return reply
}

export default function App() {
  return <ChatBot onSendMessage={sendToBackend} />
}

Backend — return { reply: "..." }:

app.post('/api/chat', (req, res) => {
  res.json({ reply: `You said: ${req.body.message}` })
})

User message → onSendMessage → your API → widget shows the reply. If onSendMessage throws, the error text appears in the thread as a bot message.

No CSS import needed.


Rich replies

Instead of a string, return an array of parts:

[
  { "type": "text", "text": "Here are three picks:" },
  { "type": "products", "items": [{ "id": "sku-1", "name": "Trail Runner" }] }
]

Then tell the widget which component renders each type:

<ChatBot
  onSendMessage={sendToBackend}
  components={{ products: ProductList }}
/>

{ type: 'text' } is built in. Every other type is looked up in components and rendered full-width, receiving the part's own fields plus send. A type with no registered component renders a visible warning instead of failing silently.

Parts are plain JSON on purpose — a React component can't survive an HTTP round trip, but { type: 'products', items: [...] } can. That's what lets your server decide what the user sees.

send(input, label)

Every registered component gets a send function to start the next turn.

send('Track my order')                              // acts like the user typed it
send({ action: 'add_to_cart', id: 'sku-1' }, 'Add') // payload; thread shows "Add"
send({ action: 'ping' }, null)                      // sends without a user bubble
  • input reaches onSendMessage unchanged — so a button can send an object, not just text
  • label is what appears in the thread. Omit it to reuse a string input; pass null to send silently

Because input may be an object, handle both on the way in:

async function sendToBackend(input) {
  const res = await fetch('/api/chat', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(typeof input === 'string' ? { message: input } : input),
  })
  return (await res.json()).reply   // a string, a part, or an array of parts
}

Example: quick-reply chips

Server returns:

[
  { "type": "text", "text": "How can I help?" },
  { "type": "choices", "options": ["Track my order", "I want a refund"] }
]

Component — sends the label as if the user typed it, then hides itself so old chips can't be clicked later:

import { useState } from 'react'

function Choices({ options, send }) {
  const [used, setUsed] = useState(false)
  if (used) return null

  return (
    <div className="choices">
      {options.map((o) => (
        <button key={o} onClick={() => { setUsed(true); send(o) }}>
          {o}
        </button>
      ))}
    </div>
  )
}

Register it: components={{ choices: Choices }}

Example: product cards

Any custom component works the same way — the widget knows nothing about products, it just hands the part's fields to your component.

{
  "type": "products",
  "items": [
    { "id": "sku-1", "name": "Trail Runner", "price": "₹4,499", "emoji": "👟" }
  ]
}
function ProductList({ items, send }) {
  return (
    <div className="cards">
      {items.map((p) => (
        <article key={p.id} className="card">
          <span>{p.emoji}</span>
          <div>
            <h4>{p.name}</h4>
            <p>{p.price}</p>
          </div>
          <button onClick={() => send({ action: 'add_to_cart', id: p.id }, `Add ${p.name}`)}>
            Add
          </button>
        </article>
      ))}
    </div>
  )
}

The button sends a payload, so your backend gets { action: 'add_to_cart', id: 'sku-1' } rather than having to parse a sentence.

Style these however you like — the widget applies no styling to custom parts, so they inherit your app's design.

Example: status cards

{
  "type": "order",
  "id": "#1042",
  "status": "Shipped",
  "eta": "Arrives Fri",
  "steps": ["Placed", "Packed", "Shipped", "Delivered"]
}
function OrderCard({ id, status, eta, steps, send }) {
  const reached = steps.indexOf(status)

  return (
    <article className="card">
      <header>
        <strong>Order {id}</strong> <span>{eta}</span>
      </header>
      <ol className="trail">
        {steps.map((s, i) => (
          <li key={s} className={i <= reached ? 'done' : ''}>{s}</li>
        ))}
      </ol>
      <button onClick={() => send('I want a refund', 'Request a refund')}>
        Request a refund
      </button>
    </article>
  )
}

Example: approve / reject gates

For anything you don't want run without a human saying yes — refunds, deletes, sending an email — have the server return an approval part instead of performing the action:

[
  { "type": "text", "text": "I can do that, but it needs confirmation." },
  {
    "type": "approval",
    "toolCallId": "call_refund_1042",
    "summary": "Refund ₹4,499 for order #1042?",
    "detail": "Goes back to the original payment method in 3–5 days."
  }
]
function Approval({ summary, detail, toolCallId, send }) {
  const [decision, setDecision] = useState(null)

  function answer(approved) {
    setDecision(approved)                                   // lock the card
    send({ action: 'approval', toolCallId, approved },
         approved ? 'Approve' : 'Reject')
  }

  if (decision !== null) return <p>{decision ? '✅ Approved' : '❌ Rejected'}</p>

  return (
    <article className="card">
      <strong>{summary}</strong>
      <p>{detail}</p>
      <button onClick={() => answer(true)}>Approve</button>
      <button onClick={() => answer(false)}>Reject</button>
    </article>
  )
}

Keep the answered state in the component (decision above) so an old card can't be clicked twice.

Backend resumes or cancels using toolCallId:

app.post('/api/chat', async (req, res) => {
  const { action, toolCallId, approved } = req.body

  if (action === 'approval') {
    if (!approved) return res.json({ reply: 'No problem — I cancelled it.' })
    const result = await runPendingTool(toolCallId)   // now it actually runs
    return res.json({ reply: `Done. ${result}` })
  }
  // ...normal message handling
})

Tool calling

The tool loop belongs on your server, not in the widget:

  1. Server sends the conversation to your model with your tool definitions
  2. Model asks for a tool
  3. Server runs it — unless it's sensitive, in which case it returns an approval part and parks the call
  4. Server turns results into parts and returns them
  5. Widget renders them; button clicks come back as the next turn

The widget only renders parts and ships payloads, so it stays the same whichever model or framework you use.

Call your model provider from your server, never from onSendMessage — that function runs in the browser, where an API key is visible in devtools.


Props

| Prop | Default | | |---|---|---| | onSendMessage | — | required; (input) => Reply \| Promise<Reply>. input is a string or a payload from send. Reply is a string, a part, or an array of parts | | components | {} | maps a part type to the component that renders it | | title | 'Chat Assistant' | header text | | welcomeMessage | 'Hi! How can I help you today?' | first bot message; '' for none | | placeholder | 'Type a message…' | input placeholder | | accentColor | '#4f46e5' | any CSS color — header, bubbles, buttons |

A bare string reply still works everywhere, so code written against the earlier version needs no changes.

Open the bubble and try hello, show me products, track my order, or I want a refund — one for each part type above. Source is in demo/.