Chat Kit
Cherry ships everything needed to build an AI-assistant chat UI, split into a headless provider and presentational components. ChatProvider owns panel state, focus management, the transcript, and streaming bookkeeping; the app owns the transport - Cherry never fetches, parses SSE, or assumes an endpoint.
The kit consists of:
- ChatProvider - headless state and the
useChat()hook, documented on this page. - ChatPanel - the shell: a side drawer, an inline container, or a fullscreen dialog.
- ChatMessageList, ChatMessage, ChatTyping, ChatSources - the transcript.
- ChatInput - the auto-growing composer, with an optional rainbow glow treatment.
- ChatLauncher - the "Ask AI" pill that toggles the panel.
Wiring it up
The provider takes your transport as onSend and hands it everything it needs: the question, an abort signal, the prior conversation, and a setAssistant callback that creates the reply bubble on its first call and patches the same bubble on every later call - so streaming updates one message in place.
"use client";
import React from "react";
import {
ChatInput,
ChatLauncher,
ChatMessage,
ChatMessageList,
ChatPanel,
ChatProvider,
ChatTyping,
Prose,
useChat,
} from "cherry-styled-components";
function Transcript() {
const { messages, loading } = useChat();
return (
<ChatMessageList>
{messages.map((message) => (
<ChatMessage key={message.id} $role={message.role}>
{message.content}
</ChatMessage>
))}
{loading && <ChatTyping />}
</ChatMessageList>
);
}
export default function Assistant() {
return (
<ChatProvider
onSend={async (question, { signal, history, setAssistant }) => {
const res = await fetch("/api/chat", {
method: "POST",
body: JSON.stringify({ question, history }),
signal,
});
let text = "";
for await (const chunk of readMyStream(res)) {
text += chunk;
setAssistant(text); // patches the same assistant bubble each call
}
setAssistant(<Prose $compact>{renderMarkdown(text)}</Prose>, {
text, // plain-text mirror used for the next request's history
sources: [{ id: "1", label: "Docs", href: "/docs" }],
});
}}
>
<ChatLauncher $glow />
<ChatPanel>
<Transcript />
<ChatInput $glow />
</ChatPanel>
</ChatProvider>
);
}While streaming, pass plain text to setAssistant; when the reply is complete, pass rich markup (typically <Prose $compact>) plus a plain-text text mirror, which is what the provider uses to build the history for the next request. A thrown error lands in error; an abort is swallowed.
ChatProvider
The transport: (question, { signal, history, setAssistant }) => void | Promise<void>. Optional when $showcase is on; required for real answers. Without one, non-command input gets a built-in hint message instead of a silent drop.
Assistant message seeded on first open and after reset(). Defaults to "Hey there, how can I assist you?" (a showcase-specific greeting when $showcase is on). Pass null for none.
Maximum number of prior messages in the history handed to onSend. Defaults to 20. Each entry is additionally capped at 4000 characters, so one giant pasted message cannot blow up the request. Only messages with a non-empty text mirror are included.
Cmd/Ctrl + this key toggles the panel globally. Defaults to "i". Pass null to disable.
Demo mode: chat input matching a showcase command is answered locally with a live rendering of that element. See below. Off by default.
useChat
Every value lives in ChatContext; useChat() is the hook that reads it.
Whether an overlay panel is open. Inline panels ignore it.
Panel controls. open(returnFocusTo?) records where focus should return; close(restoreFocus = true) restores it to the opener and returns that element. Focus lands on the composer whenever the panel opens.
The transcript: { id, role, content, text?, sources? } entries. content is rendered as-is - plain text while streaming, rich markup when done.
The composer's controlled value.
True while onSend is running.
Message of the last error thrown by onSend, cleared on the next send.
Sends text, or the current input when omitted. No-op while loading or when empty.
Opens the panel and hands question straight to the assistant - the bridge for launchers and search modals. While a reply is still streaming, the question is pre-filled in the composer instead, ready to send.
Aborts any in-flight reply and restores the initial greeting. Handy as a header action on the panel.
Ref to the composer's textarea, used by the kit for focus management.
Showcase mode
With $showcase on, chat input matching a showcase command is answered locally with a live rendering of that element; everything else still goes to onSend (which becomes optional). help lists the commands: list (the component catalog), callout, avatar, prose, sources, and typing. A leading slash and any casing work too.
It is the fastest way to try the kit without a backend, and doubles as a component showcase inside your own app. The command set is exported as showcaseCommands, and resolveShowcaseCommand(input) returns a command's rendered demo (or null) if you want the same behavior in a custom provider.
Exports
Alongside the components, the library exports the types ChatRole, ChatMessageData, ChatSourceData, ChatHistoryEntry, ChatSendContext, ChatSendHandler, ChatProviderProps, and ShowcaseCommand, plus the raw ChatContext for advanced use.