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

@cognipeer/chat-ui

v0.10.0

Published

A customizable React chat UI component for AI agents

Readme

@cognipeer/chat-ui

A customizable React chat UI component library for AI agents. Features a ChatGPT-like dark theme, streaming support, file uploads, tool call visualization, and seamless integration with @cognipeer/agent-server.

Documentation: https://docs.cognipeer.com/chat-ui/guide/getting-started

Features

  • 🎨 Customizable Theme - Dark/light modes with full color customization
  • 📡 Streaming Support - Real-time SSE streaming with text updates
  • 📁 File Uploads - Pick, drag and drop or paste files; image thumbnails; send files without text
  • 🔧 Tool Call UI - Expandable tool call visualization
  • ✅ Approvals and Questions - Approve tool calls and answer agent questions inline (agent-server 0.3.0+, see docs/guide/approvals.md)
  • 🔁 Runs that outlive the page - Reload in the middle of a turn and it is picked up again, a dropped connection is not an error, and Stop really stops the run on the server (see docs/guide/runs.md)
  • 💸 Usage limits - A meter, a "limit reached" notice and, where the host allows, raise-and-continue (see docs/guide/budgets.md)
  • 💬 Chat History - Sidebar with conversation history, status badges (working, waiting for you, limit reached) and keyboard-operable rows
  • 🎯 Customizable Actions - Add feedback buttons or custom actions to messages
  • 🏢 Portal experience - Copy / regenerate / edit, thumbs with reasons, rename / pin / bulk-delete history, starters and a Markdown welcome, deep links, AI label and legal links, announcements, a terms gate, export and background alerts - each off until you switch it on (see docs/guide/portal-experience.md)
  • 🪟 Embeddable - EmbedChat runs in an iframe signed in with an identity token your server signs: strict postMessage origin checks, an in-memory bearer session that renews itself, events for the host page and tools of the host page the agent can call (see docs/guide/embed.md)
  • 🌍 Internationalization - English and Turkish built in, your own strings and locales on top; friendly messages for server error codes
  • ♿ Accessible - Labelled controls, a live message log, focus moved to approval and question cards
  • ⚛️ React 18+ - Modern React with hooks
  • 🎨 Tailwind CSS - Utility-first styling

Installation

npm install @cognipeer/chat-ui

Quick Start

Basic Usage with Agent Server

import { Chat } from "@cognipeer/chat-ui";
import "@cognipeer/chat-ui/styles.css";

function App() {
  return (
    <div className="h-screen">
      <Chat
        baseUrl="http://localhost:3000/api"
        agentId="my-agent"
        theme="dark"
      />
    </div>
  );
}

With Authentication

<Chat
  baseUrl="http://localhost:3000/api"
  agentId="my-agent"
  authorization="Bearer your-token-here"
  theme="dark"
/>

Without History Sidebar

import { ChatMinimal } from "@cognipeer/chat-ui";

<ChatMinimal
  baseUrl="http://localhost:3000/api"
  agentId="my-agent"
/>

Custom Theme

Using Theme Provider

import { Chat } from "@cognipeer/chat-ui";

<Chat
  baseUrl="http://localhost:3000/api"
  agentId="my-agent"
  theme="dark"
  themeColors={{
    bgPrimary: "#1a1a2e",
    bgSecondary: "#16213e",
    bgTertiary: "#0f3460",
    textPrimary: "#eaeaea",
    textSecondary: "#a0a0a0",
    accentPrimary: "#e94560",
  }}
/>

Direct Theme Provider Usage

import { ChatThemeProvider, useChatTheme } from "@cognipeer/chat-ui";

function MyChat() {
  const { theme, setTheme } = useChatTheme();
  
  return (
    <button onClick={() => setTheme({ mode: theme.mode === "dark" ? "light" : "dark" })}>
      Toggle Theme
    </button>
  );
}

function App() {
  return (
    <ChatThemeProvider defaultMode="dark">
      <MyChat />
    </ChatThemeProvider>
  );
}

Internationalization (i18n)

The library includes built-in internationalization support with English and Turkish translations. Language is detected from the URL query parameter (?lang=en or ?lang=tr). If no query parameter is provided, it defaults to English (or the specified defaultLocale).

Auto-detect from URL Query Parameter

// URL: https://example.com/chat?lang=tr
<Chat
  baseUrl="http://localhost:3000/api"
  agentId="my-agent"
/>
// Will automatically use Turkish

// URL: https://example.com/chat (no query parameter)
<Chat
  baseUrl="http://localhost:3000/api"
  agentId="my-agent"
/>
// Will use English (default)

Set Default Locale

<Chat
  baseUrl="http://localhost:3000/api"
  agentId="my-agent"
  defaultLocale="tr"
/>

Controlled Locale

import { useState } from "react";
import { Chat, type SupportedLocale } from "@cognipeer/chat-ui";

function App() {
  const [locale, setLocale] = useState<SupportedLocale>("en");
  
  return (
    <>
      <button onClick={() => setLocale(locale === "en" ? "tr" : "en")}>
        Switch Language
      </button>
      <Chat
        baseUrl="http://localhost:3000/api"
        agentId="my-agent"
        locale={locale}
        onLocaleChange={setLocale}
      />
    </>
  );
}

Using i18n Hook

import { useI18n } from "@cognipeer/chat-ui";

function MyComponent() {
  const { t, locale, setLocale } = useI18n();
  
  return (
    <div>
      <p>{t("chat.input.placeholder")}</p>
      <p>Current language: {locale}</p>
      <button onClick={() => setLocale(locale === "en" ? "tr" : "en")}>
        {locale === "en" ? "Türkçe" : "English"}
      </button>
    </div>
  );
}

Custom Messages

Replace or add strings with the customMessages prop of Chat, or with a ChatI18nProvider around it. Chat honours an enclosing provider: its locale applies unless locale is given, and its custom messages are merged under the ones given to Chat.

import { Chat } from "@cognipeer/chat-ui";

<Chat
  baseUrl="..."
  agentId="..."
  customMessages={{
    en: { "chat.input.placeholder": "Write your message here..." },
    tr: { "chat.input.placeholder": "Mesajınızı buraya yazın..." },
  }}
/>
import { Chat, ChatI18nProvider } from "@cognipeer/chat-ui";

<ChatI18nProvider locale="tr" customMessages={{ tr: { "chat.input.placeholder": "Bir şey sorun..." } }}>
  <Chat baseUrl="..." agentId="..." />
</ChatI18nProvider>

Another language

Any locale code works once its strings are supplied; keys you leave out fall back to English, key by key:

<Chat locale="de" customMessages={{ de: { "chat.input.placeholder": "Nachricht eingeben...", "chat.input.send": "Senden" } }} />

The message keys are in src/locales/en.json.

Server error messages

A failed request whose code the library knows (conversation_busy, rate_limited, action_expired, file_too_large, ...) is shown as a localized, friendly message (chat.error.code.<code>) instead of the server's English text. The error keeps its code and status, and the server's own wording is on serverMessage. Codes without a string show the server's message. Override a string like any other (customMessages).

Supported Locales

  • en - English (default)
  • tr - Turkish
  • any other code, with customMessages

Language Detection:

  1. The locale prop, or the locale of an enclosing ChatI18nProvider
  2. URL query parameter (?lang=en or ?lang=tr)
  3. Falls back to defaultLocale prop (default: en)

All UI strings are automatically translated including:

  • Input placeholders
  • Button labels
  • Messages
  • Tooltips
  • Error messages

Custom Message Actions (Feedback)

import { Chat, ThumbsUpIcon, ThumbsDownIcon } from "@cognipeer/chat-ui";

function MessageActions({ message, isStreaming }) {
  if (isStreaming) return null;
  
  const handleFeedback = (type: "up" | "down") => {
    console.log(`Feedback ${type} for message:`, message.id);
    // Send feedback to your API
  };
  
  return (
    <div className="flex gap-2">
      <button
        onClick={() => handleFeedback("up")}
        className="p-1 hover:bg-gray-700 rounded"
      >
        <ThumbsUpIcon className="w-4 h-4" />
      </button>
      <button
        onClick={() => handleFeedback("down")}
        className="p-1 hover:bg-gray-700 rounded"
      >
        <ThumbsDownIcon className="w-4 h-4" />
      </button>
    </div>
  );
}

<Chat
  baseUrl="http://localhost:3000/api"
  agentId="my-agent"
  renderMessageActions={(props) => <MessageActions {...props} />}
/>

Choosing a model

When the agent advertises two or more models (AgentInfo.models, agent-server >= 0.4.0), the composer shows a ModelPicker pill left of the send button. The pick is kept per conversation (a reopened conversation shows the model of its last answer; a new chat starts on the default) and is sent as model with every message, regenerate and edit.

<Chat baseUrl="/api/agents" agentId="support" showAnsweredWith /> {/* "Answered with <model>" under answers (default true) */}

The picker is keyboard accessible (arrows, Home/End, Enter, Escape), follows the theme tokens and becomes a bottom sheet on narrow screens. A divider marks a change of model in the thread. Use it on its own: <ModelPicker models={...} value={key} onChange={setKey} />; with useChat, pass getModel: () => key. Answers report their model in metadata.model (also metadata.agentModel, metadata.cognipeer.agent_model).

Reasoning ("Thinking")

When the server streams the model's reasoning (agent-server >= 0.5.0, stream.thinking), it is kept apart from the answer: each assistant message carries reasoning, shown in a collapsible, keyboard accessible "Thinking" block above the answer (open while the model is still reasoning, folded once the answer starts). Reasoning is plain text and follows the theme tokens; strings are chat.reasoning.* (English, Turkish). Stored messages that carry metadata.reasoning / metadata.thinking show it too.

<Chat baseUrl="/api/agents" agentId="support" showReasoning={false} /> {/* hide the block (default true) */}
<Chat baseUrl="/api/agents" agentId="support" reasoningEffort="high" /> {/* sent with each message; no picker yet */}

An agent with AgentInfo.showReasoning === false never shows the block. With useChat, streamingReasoning holds the reasoning of the running turn and getReasoningEffort: () => "low" sets the effort sent (none | minimal | low | medium | high, also SendMessageOptions.reasoningEffort). Use the block on its own: <ThinkingBlock reasoning={text} />. A custom StreamCallbacks gets onThinking(text, fullThinking).

Appearance (classic / modern)

appearance chooses how the chat is drawn. The default, classic, is the look the chat always had, so an existing integration changes nothing. modern is opt-in:

  • the person's messages are bubbles on the right; answers are plain text, without avatars or a shaded band;
  • the steps of a turn (reasoning, tool calls, answered approvals and questions) fold into one line, "Worked for 24s · Web Search ×4", which opens a timeline; an approval or question that still waits stays a card;
  • a slim header names the agent; the sidebar groups chats by date (Today, Yesterday, Previous 7 days, ...), lists the agents on top, and can show a brand (title, logo) and the signed-in account with a menu (sign out);
  • the empty chat shows the agents as cards when there is a choice, then the chosen agent's welcome and starters;
  • the composer is one rounded box with a single line of notes under it;
  • the chat uses the system UI font at 15px (set --chat-font-family / --chat-font-size to change it) instead of inheriting the page's font.
<Chat
  baseUrl="/api/agents"
  appearance="modern"
  brand={{ title: "Acme assistants", logo: "/logo.svg" }}
  account={{ name: "Ayşe Yılmaz", detail: "[email protected]", onSignOut: () => signOut() }}
/>

Components drawn outside <Chat> read the appearance from ChatAppearanceProvider. TurnActivity and buildTurnBlocks are exported for hosts that draw their own message list.

Using Hooks Directly

For more control, use the hooks directly. A custom layout shows approval and question cards through actions, and the composer follows composerState (one value for a waiting card, a turn running elsewhere, a used-up usage limit, a read-only chat):

import { useChat, ChatMessageList, ChatInput } from "@cognipeer/chat-ui";

function CustomChat() {
  const chat = useChat({
    baseUrl: "http://localhost:3000/api",
    agentId: "my-agent",
    streaming: true,
    onMessageReceived: (message) => {
      console.log("New message:", message);
    },
  });

  return (
    <div className="flex flex-col h-full">
      <ChatMessageList
        messages={chat.messages}
        isStreaming={chat.isStreaming}
        streamingText={chat.streamingText}
        // Without `actions`, approvals and questions show as read-only
        actions={{ onResolve: chat.resolveAction, canResolve: chat.canResolveActions }}
        // Pictures and downloads of a loaded conversation
        resolveFileUrl={chat.resolveFileUrl}
      />
      <ChatInput
        onSend={(text) =>
          chat.answerableQuestion && chat.composerState === "answer"
            ? chat.resolveAction(chat.answerableQuestion.actionId, {
                type: "user_question",
                answers: { [chat.answerableQuestion.question.question]: text },
              })
            : chat.sendMessage(text)
        }
        onStop={chat.stop}
        isLoading={chat.isStreaming}
        isUploading={chat.isUploading}
        state={chat.composerState}
        pendingFiles={chat.pendingFiles}
        onFilesAdd={chat.addFiles}
        onFileRemove={chat.removeFile}
        onFileRetry={chat.retryFile}
      />
    </div>
  );
}

File Uploads

<Chat
  baseUrl="http://localhost:3000/api"
  agentId="my-agent"
  enableFileUpload={true}
  allowedFileTypes={["image/*", ".pdf", ".txt"]}
  maxFileSize={10 * 1024 * 1024} // 10MB
  maxFiles={5}
/>

Files are attached with the paperclip, by dropping them on the composer, or by pasting (a screenshot or a copied file). Images show as thumbnails in the composer and in the conversation. A message can carry files and no text (attachmentsOnlyMessage sets the text that goes with it).

With fileTransport="upload-on-select" each file is stored the moment it is picked, as itself (raw body, no base64) with a progress bar on its chip; "upload" stores them when the message is sent. The agent says what it takes (capabilities.attachments: limits per conversation, accepted types, whether it reads images, how long files are kept), the chat counts what the conversation holds ("3 of 10 files in this chat") and marks expired files and files whose text could not be read. An agent that does not read images gets a warning when an image is attached. See docs/guide/file-uploads.md.

Custom Callbacks

<Chat
  baseUrl="http://localhost:3000/api"
  agentId="my-agent"
  onMessageSent={(message) => {
    console.log("User sent:", message);
  }}
  onMessageReceived={(message) => {
    console.log("Agent replied:", message);
  }}
  onStreamText={(chunk, fullText) => {
    console.log("Streaming:", chunk);
  }}
  onToolCall={(toolName, args) => {
    console.log("Tool called:", toolName, args);
  }}
  onToolResult={(toolName, result) => {
    console.log("Tool result:", toolName, result);
  }}
  onError={(error) => {
    console.error("Chat error:", error);
  }}
  onConversationCreated={(conversation) => {
    console.log("New conversation:", conversation);
  }}
  onConversationSelected={(conversation) => {
    console.log("Selected conversation:", conversation);
  }}
/>

Custom API Integration

For non-agent-server backends, use the API client or hooks with custom implementations:

import { useChat, AgentServerClient } from "@cognipeer/chat-ui";

// Create a custom client if needed
const customClient = new AgentServerClient({
  baseUrl: "https://your-api.com",
  agentId: "agent",
  headers: {
    "X-Custom-Header": "value",
  },
});

// Or extend the client class for fully custom implementations

Components API

<Chat>

Main chat component with all features.

| Prop | Type | Default | Description | |------|------|---------|-------------| | baseUrl | string | required unless transport | Base URL for the API | | transport | ChatTransport | - | Another backend (see docs/guide/transports.md) | | agentId | string | first agent | Agent ID to use | | authorization | string | - | Authorization header value | | headers | object | - | Custom headers | | getHeaders | function | - | Fresh headers before every request | | streaming | boolean | true | Enable streaming | | showHistory | boolean | true | Show history sidebar | | theme | "light" \| "dark" | "dark" | Theme mode | | themeColors | ChatTheme["colors"] | - | Custom colors, including success / danger / warning / info (and ...Bg) | | locale | string | enclosing provider, else auto-detect | Override locale | | defaultLocale | string | "en" | Default locale | | onLocaleChange | function | - | Callback when locale changes | | customMessages | Record<locale, Record<key, string>> | - | Replace or add UI strings | | enableFileUpload | boolean | true | Enable file uploads | | allowedFileTypes, maxFileSize, maxFiles | | any, 10 MB, 10 | Upload limits (an agent's capabilities.attachments or metadata can narrow them) | | maxFilesScope | "message" \| "conversation" | "message" | What maxFiles counts (an agent's own maxFiles counts per conversation) | | fileTransport | "inline" \| "upload" \| "upload-on-select" | "inline" | Files in the message body, uploaded when sending, or uploaded as they are picked | | attachmentsOnlyMessage | string | localized "review the attached files" | Text sent with files and no typed text ("" sends none) | | enableToolApprovals, enableQuestions | boolean | true | Answer approvals / questions in this app | | allowArgumentEdit | boolean | false | Edit a tool call's arguments in the approval card | | expiryWarningSeconds | number | 600 | Cards turn amber with this much time left | | pendingActionComposerMode | "block" \| "cancel" | "block" | Composer while a card waits | | readOnly | boolean | false | A transcript: the composer is closed | | showStatus | boolean | true | Status badges in the history | | showBudgetMeter | boolean | true | The spent / limit bar of a conversation with a usage limit | | allowBudgetContinue, allowBudgetEdit | boolean | false | Let a person continue / raise a used-up limit (an agent's capabilities.budget overrides them) | | renderStopNotice, renderBudgetBanner | function | - | Replace the notice under a stopped answer / the usage-limit banner | | renderToolApproval, renderQuestion | function | - | Replace a card | | renderImage | function | - | Draw images yourself (e.g. fetch with credentials) | | renderMessageActions | function | - | Custom message actions | | renderAvatar | function | - | Custom avatar | | renderEmptyState | function | - | Custom empty state | | renderHeader | function | - | Custom header | | appearance | "classic" \| "modern" | "classic" | How the chat is drawn (see Appearance) | | brand | ChatBrand | - | modern: title and logo at the top of the sidebar | | account | ChatAccount | - | modern: the signed-in person at the foot of the sidebar, with a menu (onSignOut, menu) |

<ChatMinimal>

Simplified chat without history sidebar.

<ChatMessage>

Individual message component.

<ChatMessageList>

List of messages with auto-scroll.

<ChatInput>

Input component with file upload support.

<ChatHistory>

History sidebar component.

<ToolCall> / <ToolCalls>

Tool call visualization components.

Hooks

useChat(options)

Main chat state management hook.

Returns:

  • messages - Current messages
  • conversation - Current conversation
  • isLoading - Loading state
  • isFollowing - Only watching a turn that runs elsewhere (another window, or this page before a reload)
  • streamingText - Current streaming text
  • error - Current error
  • pendingFiles - Files pending upload (images carry a local previewUrl; files stored on their own carry status and progress)
  • isUploading - Files are going up (for the message being sent, or stored as they were picked)
  • heldFiles, retryFile(id), resolveFileUrl - Files the conversation holds, retry a failed upload, a URL for a stored file
  • composerState - "open" | "blocked" | "cancel" | "answer" | "following" | "budget_paused" | "readonly"
  • budget, spentUsd, budgetExhausted, canContinue, canUpdateBudget, continueConversation(), updateBudget(budget, { continue }) - Usage limit (see docs/guide/budgets.md)
  • activeToolCalls - Active tool calls
  • pendingActions, awaitingAction, isResolvingAction, canResolveActions, answerableQuestion, resolveAction(actionId, resolution) - Approvals and questions (see docs/guide/approvals.md)
  • sendMessage(content) - Send a message
  • addFiles(files) - Add files
  • removeFile(id) - Remove a file
  • clearMessages() - Clear messages
  • loadConversation(id) - Load a conversation
  • createConversation(title?) - Create new conversation
  • retry() - Retry last message
  • stop() - Stop generation: cancels the run on the server where the transport can (agent-server), keeps the partial answer as stopped; otherwise it only closes the connection

useChatHistory(options)

History management hook.

Returns:

  • conversations - Conversation list
  • isLoading - Loading state
  • hasMore - Has more items
  • load() - Load conversations
  • loadMore() - Load more
  • refresh() - Refresh list
  • deleteConversation(id) - Delete conversation

useI18n()

Internationalization hook.

Returns:

  • t(key, values?) - Translate a key
  • locale - Current locale code
  • has(key) - Whether a message exists for the key
  • setLocale(locale) - Change locale (also updates URL query param)
  • formatNumber(value, options?) - Format a number
  • formatDate(value, options?) - Format a date
  • formatRelativeTime(value, unit) - Format relative time

Outside a ChatI18nProvider the hook answers in English.

Styling with Tailwind

The library uses CSS variables for theming. You can override them:

:root {
  --chat-bg-primary: #212121;
  --chat-bg-secondary: #171717;
  --chat-bg-tertiary: #2f2f2f;
  --chat-bg-hover: #3f3f3f;
  --chat-text-primary: #ececec;
  --chat-text-secondary: #b4b4b4;
  --chat-text-tertiary: #8e8e8e;
  --chat-text-inverse: #171717;
  --chat-border-primary: #3f3f3f;
  --chat-border-secondary: #2f2f2f;
  --chat-accent-primary: #10a37f;
  --chat-accent-secondary: #1a7f64;
  /* status colours, each with a tinted background */
  --chat-success: #34d399;
  --chat-success-bg: rgba(16, 185, 129, 0.12);
  --chat-danger: #f87171;
  --chat-danger-bg: rgba(239, 68, 68, 0.12);
  --chat-warning: #fbbf24;
  --chat-warning-bg: rgba(245, 158, 11, 0.14);
  --chat-info: #60a5fa;
  --chat-info-bg: rgba(59, 130, 246, 0.12);
}

Next.js Integration

// app/chat/page.tsx
"use client";

import { Chat } from "@cognipeer/chat-ui";
import "@cognipeer/chat-ui/styles.css";

export default function ChatPage() {
  return (
    <div className="h-screen">
      <Chat
        baseUrl="/api/agents"
        agentId="my-agent"
      />
    </div>
  );
}

License

MIT