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

chatbot-page

v1.0.11

Published

A configurable chatbot-style page for personal sites, company pages, and LLM-backed Q&A.

Readme

chatbot-page

Reusable React components for building a chatbot-style homepage. Drop in ChatApp, give it a config, and choose where answers come from: canned Markdown, a real LLM, or a RAG vector store.

Install

npm install chatbot-page

react and react-dom (18.3+ or 19) are peer dependencies.

Basic concepts

  • ChatApp — the full chat UI. It takes a single config object.
  • Answer providers — where answers come from. Compose them: canned answers first, with an LLM as the fallback.
    • createCannedAnswerProvider — serves predefined answers.
    • createApiAnswerProvider — calls a backend route (/api/chat) for LLM/RAG answers, streaming over SSE.
  • Server helperschatbot-page/server provides the pieces for that route: request parsing, an OpenAI Responses model provider, RAG via vector stores, and notifiers.
import { ChatApp, createCannedAnswerProvider } from "chatbot-page";
import "chatbot-page/styles.css";

const config = {
  identity: { name: "Vladimir Haltakov", title: "AI builder", handle: "haltakov" },
  introMessage: "Hi, I'm **Vladimir Haltakov**. Ask me anything.",
  suggestions: [{ id: "contact", question: "How can I get in touch?" }],
  answerProvider: createCannedAnswerProvider({
    answers: { contact: "Email me at [[email protected]](mailto:[email protected])." },
  }),
};

export function Page() {
  return <ChatApp config={config} />;
}

Configuration

Canned answers (Markdown)

Write one Markdown file per question, with frontmatter for metadata:

---
id: about
question: Who is Vladimir Haltakov?
---

Vladimir Haltakov is an AI builder and computer vision engineer based in Munich.

Load the folder on the server and turn it into a provider:

import { loadCannedAnswerDirectory } from "chatbot-page/server";
const cannedAnswers = await loadCannedAnswerDirectory("content/chatbot");
import { createCannedAnswerCollection, createCannedAnswerProvider } from "chatbot-page";

const canned = createCannedAnswerCollection(cannedAnswers);
const answerProvider = createCannedAnswerProvider({ ...canned, fallbackProvider });

Canned answers are only used when a visitor clicks a suggested question; typed questions fall through to the fallback.

LLM + RAG (OpenAI)

In your /api/chat route, stream answers from the OpenAI Responses provider. Put the system prompt in an editable Markdown file and point the provider at it with systemPromptPath. Pass vectorStoreIds to enable RAG (file search over your documents):

import {
  createChatbotSseResponse,
  createChatbotErrorResponse,
  createOpenAIResponsesProvider,
  readChatbotRequest,
} from "chatbot-page/server";

const modelProvider = createOpenAIResponsesProvider({
  apiKey: process.env.OPENAI_API_KEY,
  model: process.env.OPENAI_MODEL ?? "gpt-5.4-mini",
  systemPromptPath: "content/system-prompt.md",
  vectorStoreIds: process.env.OPENAI_VECTOR_STORE_ID?.split(",").filter(Boolean),
});

export async function POST(request: Request) {
  try {
    const chatRequest = await readChatbotRequest(request, { maxMessageLength: 4000 });
    return createChatbotSseResponse(modelProvider.streamAnswer(chatRequest));
  } catch (error) {
    return createChatbotErrorResponse(error);
  }
}

System Prompt

The recommended setup is to keep the prompt outside your route code:

<!-- content/system-prompt.md -->

You are answering questions on my personal website.

- Be concise, friendly, and concrete.
- Answer from the provided context when available.
- If you do not know, say so plainly.

Then configure the OpenAI provider with a path relative to your app root:

const modelProvider = createOpenAIResponsesProvider({
  apiKey: process.env.OPENAI_API_KEY,
  model: process.env.OPENAI_MODEL ?? "gpt-5.4-mini",
  systemPromptPath: "content/system-prompt.md",
});

systemPromptPath can also be an absolute path. If you prefer to keep the prompt somewhere else, pass instructions directly as a string, or pass an async instructions(request) function if your prompt comes from a CMS, database, or another runtime source. Use either systemPromptPath or instructions, not both.

To use a different model, implement ChatbotModelProvider and yield text-delta chunks. readChatbotRequest validates shape and input size — add rate limiting at your deployment boundary for public LLM-backed routes.

Telegram notifications

Get notified when someone chats or leaves a contact email. Enable notifications in the config and handle the events in an /api/chatbot-events route:

const config = {
  // ...
  notifications: { enabled: true, endpoint: "/api/chatbot-events" },
};
import { createTelegramNotifier, readChatbotEventRequest } from "chatbot-page/server";

const notifier = createTelegramNotifier({
  botToken: process.env.TELEGRAM_BOT_TOKEN,
  chatId: process.env.TELEGRAM_CHAT_ID,
  siteName: "My Site",
});
await notifier.send(await readChatbotEventRequest(request));

To let a real person reply from Telegram, keep the in-memory live reply store in a shared server module, enable liveReplies in the client config, and wire a Telegram webhook:

const config = {
  // ...
  notifications: { enabled: true, endpoint: "/api/chatbot-events" },
  liveReplies: { enabled: true, endpoint: "/api/chatbot-live" },
};
// lib/chatbot-live-replies.ts
import { createInMemoryChatbotLiveReplyStore } from "chatbot-page/server";

export const chatbotLiveReplyStore = createInMemoryChatbotLiveReplyStore();
// app/api/chatbot-events/route.ts
const notifier = createTelegramNotifier({
  botToken: process.env.TELEGRAM_BOT_TOKEN,
  chatId: process.env.TELEGRAM_CHAT_ID,
  liveReplies: {
    store: chatbotLiveReplyStore,
    authorName: "Real Vlad",
  },
});
await notifier.send(await readChatbotEventRequest(request));
// app/api/chatbot-live/route.ts
import { createChatbotLiveReplySseResponse } from "chatbot-page/server";

export async function GET(request: Request) {
  return createChatbotLiveReplySseResponse(request, chatbotLiveReplyStore);
}
// app/api/telegram-webhook/route.ts
import { handleTelegramOperatorWebhook } from "chatbot-page/server";

export async function POST(request: Request) {
  const result = await handleTelegramOperatorWebhook(request, {
    store: chatbotLiveReplyStore,
    chatId: process.env.TELEGRAM_CHAT_ID!,
    adminUserId: process.env.TELEGRAM_ADMIN_USER_ID,
    secretToken: process.env.TELEGRAM_WEBHOOK_SECRET,
    authorName: "Real Vlad",
  });

  return Response.json(result);
}

If your framework has a Node.js startup hook, you can register the Telegram webhook there instead of running setWebhook manually:

import { registerTelegramWebhook } from "chatbot-page/server";

await registerTelegramWebhook({
  botToken: process.env.TELEGRAM_BOT_TOKEN,
  webhookUrl: process.env.TELEGRAM_WEBHOOK_URL,
  secretToken: process.env.TELEGRAM_WEBHOOK_SECRET,
  allowedUpdates: ["message"],
});

Some startup hooks, including Next.js instrumentation, may also run in an Edge bundle. In that case, avoid importing the full chatbot-page/server entry from the startup file; use a tiny fetch helper like the Next example app does.

Prompt notifications use a compact reply-friendly format: the conversation id, a blank line, then the visitor id in bold followed by the prompt. Reply to that Telegram notification to publish a human message into the active browser chat over SSE. That conversation switches to operator mode, so subsequent visitor messages notify Telegram but do not call the AI provider; suggested questions are hidden while the operator is active. Reply STOP in Telegram to hand the conversation back to AI mode. Operator messages are labeled in the chat UI, with a green status dot while operator mode is active and a gray dot after handing back to AI. The default store is intentionally in-memory; use a durable store if you need multi-instance deployments or guaranteed delivery after restarts.

You can also pass any async function as notifications to send events to your own webhook or analytics.

Other options

The config also supports a first-launch intro modal (ui.firstLaunch), a contact form (ui.contact), and pluggable storage (storage) — by default conversations, theme, and visitor id persist in localStorage. See the example app for all of these wired together.

Suggested questions are randomly chosen from config.suggestions. The UI shows 4 suggestions by default; set ui.suggestionCount to change the maximum number shown.

License

MIT