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

@vpnsin-labs/react-faq-chatbot

v0.1.4

Published

A framework-agnostic, themeable FAQ support chatbot widget for React, Vite and Next.js. Token + synonym search, optional pluggable AI fallback, zero icon-library dependency.

Downloads

602

Readme

@vpnsin-labs/react-faq-chatbot

A small, framework-agnostic FAQ support chatbot widget for React. Works in Vite, Next.js, CRA, Remix, Astro — anywhere React 16.8+ runs.

  • 🔎 Smart FAQ search — token + synonym matching with confidence scoring (no exact-match brittleness).
  • 🏷️ Portal presets — one prop tunes labels, accent and quick topics for support, ecommerce, saas, healthcare, education, realestate or hospitality.
  • 💬 WhatsApp chat — opt-in deep link as a panel CTA, a standalone launcher and/or a handoff channel.
  • 🤖 Optional pluggable AI fallback — bring your own LLM/backend; used only when no FAQ matches.
  • 🎨 Themeable, self-contained CSS — one stylesheet, theme via CSS variables. No Tailwind required.
  • 🪶 Zero icon-library dependency — ships tiny inline SVGs (override any of them).
  • Accessible — dialog semantics, aria-live log, focus rings, reduced-motion, ESC to close.
  • 💾 Persistent threadssession / local / none.
  • 📦 Tiny peer surface — only react + react-dom.

Install

npm i @vpnsin-labs/react-faq-chatbot

Quick start

import { Chatbot } from '@vpnsin-labs/react-faq-chatbot';
import '@vpnsin-labs/react-faq-chatbot/styles.css'; // import once, anywhere

const faqs = [
  { id: 1, question: 'How do I create an account?', answer: 'Click Sign Up…' },
  { id: 2, question: 'How much does it cost?', answer: 'Plans start at $9/mo…' },
];

export default function App() {
  return <Chatbot faqs={faqs} labels={{ title: 'Support' }} />;
}

That's it — a floating launcher appears bottom-right.

Next.js: the widget uses browser APIs, so render it from a Client Component ("use client"). See examples/nextjs-app.tsx.


Portal presets

Tailor the widget to a portal type with a single preset prop. Each preset seeds tuned default labels, an accent theme and starter quick topics for that domain — and every piece stays overridable (any labels, theme or quickTopics you pass wins over the preset).

<Chatbot faqs={faqs} preset="ecommerce" />
// → "Store Help", orange accent, topics: Track my order / Returns & refunds / …

<Chatbot
  faqs={faqs}
  preset="healthcare"
  labels={{ title: 'Acme Clinic' }} // override just the title; keep the rest
/>

Available presets: support · ecommerce · saas · healthcare · education · realestate · hospitality. Read or extend them via the exported PORTAL_PRESETS map.

Organising a large knowledge base

FAQItem accepts two optional fields for portals with big or jargon-heavy FAQs:

const faqs = [
  {
    id: 1,
    question: 'How do I reset my password?',
    answer: 'Use the “Forgot password” link on the sign-in page.',
    category: 'Account', // shown as a tag on suggestions
    keywords: ['login', 'credentials', 'cant sign in'], // extra search terms
  },
];
  • category groups entries and renders as a small tag on the "did you mean" suggestions.
  • keywords are author-curated aliases (synonyms, product names, common misspellings) indexed at question weight, so users find the entry even when their words aren't in the question.

Theming

Override any --rfc-* token inline via the theme prop:

<Chatbot faqs={faqs} theme={{ primary: '#7c3aed', radius: '12px', panelWidth: '400px' }} />

Dark mode follows the OS preference automatically. Force it by setting data-rfc-theme="dark" (or "light") on the widget or any ancestor element.

You can also override tokens in your own CSS:

.rfc-root {
  --rfc-primary: #16a34a;
}

AI fallback (optional)

When the FAQ search finds no confident match, the widget calls your aiAdapter. Keep API keys on the server — point the adapter at your own endpoint:

import type { AiAdapter } from '@vpnsin-labs/react-faq-chatbot';

const aiAdapter: AiAdapter = async ({ message, history, faqContext }) => {
  const res = await fetch('/api/chat', {
    method: 'POST',
    body: JSON.stringify({ message, history, faqContext }),
  });
  if (!res.ok) return null; // null → falls back to the contact card
  return (await res.json()).answer;
};

<Chatbot faqs={faqs} aiAdapter={aiAdapter} />;

faqContext contains the top-ranked FAQ entries for the query — use them to ground the model (RAG-style). If aiAdapter is omitted, the widget is pure search + contact handoff (no backend needed).


Quick topics & contact handoff

import { Chatbot, CONTACT_INTENT } from '@vpnsin-labs/react-faq-chatbot';

<Chatbot
  faqs={faqs}
  quickTopics={[
    { label: 'Pricing', seed: 'pricing plans cost' },
    { label: 'Talk to us', seed: CONTACT_INTENT }, // opens the contact card
  ]}
  contactChannels={[
    { type: 'email', label: 'Email us', value: '[email protected]' },
    { type: 'phone', label: 'Call us', value: '+1 555 010 2030' },
    { type: 'whatsapp', label: 'WhatsApp', value: '+1 555 010 2030', prefill: 'Hi!' },
    { type: 'link', label: 'Help center', value: 'https://help.acme.com' },
  ]}
/>;

By default link (and whatsapp) channels open in a new tab. In a single-page app you can keep the user in place — set target: '_self' on a link channel, and/or intercept the click with onContactNavigate to route via your own SPA router (call event.preventDefault() to take over navigation, so there's no reload and no new tab):

import { useRouter } from 'next/navigation'; // or react-router's useNavigate

const router = useRouter();

<Chatbot
  faqs={faqs}
  contactChannels={[{ type: 'link', label: 'Contact support', value: '/support', target: '_self' }]}
  onContactNavigate={(channel, event) => {
    if (channel.type === 'link') {
      event.preventDefault(); // cancel the default navigation…
      router.push(channel.value); // …and route client-side instead
    }
  }}
/>;

WhatsApp chat

Let users continue in WhatsApp with the whatsapp prop. It builds a https://wa.me/<number> deep link (with an optional pre-filled message) and can surface in up to three places via placement:

<Chatbot
  faqs={faqs}
  whatsapp={{
    phone: '+1 555 010 2030',
    message: 'Hi! I have a question about my order.',
    label: 'Chat on WhatsApp', // optional button copy
    placement: ['panel', 'launcher'], // default: "panel"
  }}
/>

| Placement | Where it shows | | ------------ | -------------------------------------------------------------------------- | | "panel" | a persistent "Chat on WhatsApp" button above the composer (default) | | "launcher" | a standalone WhatsApp button stacked above the chat launcher | | "contact" | added as a channel in the human-handoff card (deduped if you add your own) |

The button colour follows the --rfc-whatsapp / --rfc-whatsapp-foreground tokens (an accessible green with white text by default, clearing WCAG AA). Use the classic brand green with theme={{ whatsapp: '#25d366', whatsappForeground: '#05330f' }}. Clicks emit a whatsapp_clicked event via onEvent.

Domain synonyms

Improve recall for your jargon (merged over the built-in defaults):

<Chatbot
  faqs={faqs}
  synonyms={{ moq: ['minimum', 'order', 'quantity'], sku: ['item', 'product'] }}
/>

Props

| Prop | Type | Default | Description | | ----------------------- | ---------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------- | | faqs | FAQItem[] \| () => FAQItem[] \| Promise<FAQItem[]> | — | Required. Knowledge base (array or async loader). | | preset | PortalType | — | Portal flavour seeding labels/theme/topics (e.g. "ecommerce"). Explicit props override it. | | aiAdapter | AiAdapter | — | Optional AI fallback when no FAQ matches. | | synonyms | Record<string,string[]> | built-ins | Domain vocabulary expansion. | | quickTopics | QuickTopic[] | preset / [] | Starter chips on a fresh thread. | | contactChannels | ContactChannel[] | [] | Human-handoff card links. Each link/whatsapp opens a new tab unless target: '_self'. | | onContactNavigate | (channel, event) => void | — | Fires on a contact-link click before navigation; event.preventDefault() to route in-app yourself. | | whatsapp | WhatsAppConfig | — | Enable WhatsApp chat (panel CTA, launcher and/or handoff channel). | | labels | Partial<ChatbotLabels> | English defaults | Copy overrides. | | theme | ChatbotTheme | — | --rfc-* CSS-variable overrides. | | position | "bottom-right" \| "bottom-left" | "bottom-right" | Dock corner. | | persistence | "session" \| "local" \| "none" | "session" | Where to persist the thread. | | storageKey | string | "rfc.chat.v1" | Persistence key. | | defaultOpen | boolean | false | Start open. | | open / onOpenChange | boolean / (b)=>void | — | Controlled open state. | | showLauncher | boolean | true | Render the floating button. | | typingDelayMs | number | 600 | Simulated reply delay. | | nudge | { text; delayMs? } \| false | — | One-time welcome bubble. | | confidence | { answerCoverage?; suggestCount? } | — | Search resolver tuning. | | onEvent | (e: ChatbotEvent) => void | — | Analytics hook (open, message_sent, faq_answered, ai_answered, contact_clicked, whatsapp_clicked, …). | | icons | IconSet | inline SVGs | Override any glyph. |


Headless usage

Build your own UI on the engine:

import { searchFAQs, resolveFaqQuery, useChatbot } from '@vpnsin-labs/react-faq-chatbot';

const hits = searchFAQs('how do i pay', faqs); // ScoredFAQ[]
const result = resolveFaqQuery('how do i pay', faqs); // answer | suggestions | none

Development

npm install
npm test            # Vitest unit + component suite (search, presets, WhatsApp, UI)
npm run build       # bundle to dist/ with tsup

For a real-browser visual check, the examples/playground Vite app renders the widget against the built dist/ (with an optional puppeteer screenshot driver). See its README.


License

MIT © vpnsin-labs