@meetreeve/chat-kit
v0.8.0
Published
React chat kit: transport adapters + primitives + drop-in presets for Reeve.Chat surfaces
Keywords
Readme
@meetreeve/chat-kit
React chat kit for Reeve.Chat surfaces — transport adapters, headless primitives, and drop-in presets.
@meetreeve/chat-kit is the canonical surface for embedding Reeve-powered chat in any React app. It ships:
- Transport adapters for WebSocket (Reeve gateway), SSE, REST, voice, and a no-network echo for demos.
- Headless primitives with stable class names + CSS custom properties — no opinion on styling.
- Opinionated presets that compose the above into ready-to-render surfaces (
<ReeveChat>,<ReeveChat.Terminal>, etc.).
You pick the abstraction level. All three tiers compose with each other.
Install
pnpm add @meetreeve/chat-kit
# or
npm install @meetreeve/chat-kit
# or
yarn add @meetreeve/chat-kitreact and react-dom (^18 || ^19) are peer dependencies.
Quick start
import { ReeveChat } from '@meetreeve/chat-kit';
import '@meetreeve/chat-kit/styles/tokens.css';
export function ChatPage() {
return (
<ReeveChat
mode="auto"
gatewayUrl="wss://gw.meetreeve.com"
wsToken={process.env.NEXT_PUBLIC_WS_TOKEN}
sessionKey="chat-kit-demo:main"
title="Chat with Reeve"
placeholder="Ask me anything…"
/>
);
}mode="auto" picks the WebSocket adapter when wsToken is present, and falls back to the echo adapter (a local synthetic reply) otherwise. Set mode="live" to require WS, or mode="echo" to force the demo transport.
Three escalation tiers
Tier 1 — opinionated preset
A single component, sensible defaults. Best for new surfaces and for replacing legacy chat panels with minimal glue.
| Preset | Default adapter | Use when |
|---|---|---|
| <ReeveChat> | WS or Echo (auto) | General-purpose Reeve chat surface |
| <ReeveChat.Terminal> | WS | Power-user / dev console with monospace lines, no bubbles |
| <ReeveChat.Readiness> | SSE (optional REST fallback) | Intake/qualification flow with progress meter + capture card |
| <ReeveChat.Voice> | WS wrapped by useChatVoice | Mic-first surface with tap-to-record or push-to-talk |
| <ReeveChat.Library> | REST | Book / library / knowledge-base style chat with custom bubble renderer |
import { ReeveChat } from '@meetreeve/chat-kit';
<ReeveChat.Readiness
endpoint="/api/v1/readiness-conversations"
token={authToken}
conversationId={conversationId}
onComplete={({ email, phone }) => saveLead({ email, phone })}
/>Tier 2 — compose primitives
When a preset is close but not quite, drop one level and compose. Every primitive reads from the ChatProvider context, so swapping adapters is a one-line change.
import {
ChatProvider,
ChatRoot,
ChatHeader,
ChatErrorBanner,
ChatMessageList,
ChatStreamIndicator,
ChatComposer,
} from '@meetreeve/chat-kit';
import { useChatWS } from '@meetreeve/chat-kit';
import '@meetreeve/chat-kit/styles/tokens.css';
export function CustomChat() {
const adapter = useChatWS({
gatewayUrl: 'wss://gw.meetreeve.com',
wsToken: getToken(),
sessionKey: 'custom:main',
});
return (
<ChatProvider adapter={adapter} theme={{ density: 'compact' }}>
<ChatRoot variant="panel">
<ChatHeader title="Custom chat" actions={<MyMenu />} />
<ChatErrorBanner />
<ChatMessageList renderBubble={(m) => <MyBubble message={m} />} />
<ChatStreamIndicator />
<ChatComposer
attachmentSlot={<MyDropzone />}
voiceSlot={<MyVoiceButton />}
autosize
/>
</ChatRoot>
</ChatProvider>
);
}Tier 3 — bring your own adapter
Any object that satisfies the ChatAdapter contract works as a drop-in for the bundled adapters. Useful when a custom backend doesn't fit WS / SSE / REST cleanly, or when wrapping an existing state store.
import type { ChatAdapter, ChatMessage } from '@meetreeve/chat-kit';
import { ChatProvider, ChatRoot, ChatMessageList, ChatComposer } from '@meetreeve/chat-kit';
function useMyAdapter(): ChatAdapter {
const [messages, setMessages] = useState<ChatMessage[]>([]);
return {
status: 'connected',
messages,
streamState: { phase: 'idle' },
error: null,
send: (text) => {
// Talk to your backend, push the reply into setMessages.
},
};
}
export function MyChat() {
const adapter = useMyAdapter();
return (
<ChatProvider adapter={adapter}>
<ChatRoot>
<ChatMessageList />
<ChatComposer />
</ChatRoot>
</ChatProvider>
);
}The full contract is in src/protocol/adapter.ts. Optional fields (suggestions, progress, stop, retry, clear, connect, disconnect, loadHistory) are read by primitives that need them and degrade gracefully when absent.
Adapters
| Adapter | Transport | Key options | Use when |
|---|---|---|---|
| useChatEcho | none (in-process) | reply?, replyDelayMs? | Storybook / demos / unauthenticated fallback |
| useChatWS | WebSocket (Reeve gateway, protocol v3) | gatewayUrl, wsToken, sessionKey?, autoConnect?, onStatusChange?, onTokenExpired? | Live Reeve.Chat streaming |
| useChatSSE | POST + EventStream | endpoint, token?, sessionKey?, headers? | AgentPik-style /chat2 SSE backends |
| useChatREST<TConv> | REST POST + optional GET polling | endpoint, conversationId, token?, toMessages, toSuggestions?, toProgress?, pollIntervalMs? | Non-streaming or custom-DTO backends (Freya, AgentPik v1) |
| useChatVoice | wraps a text adapter | textAdapter, transcribe, recordingMode, onAudioLevel?, cancelDragThresholdPx? | Mic input layered over any text adapter |
useChatWS, useChatSSE, and useChatREST also expose a __raw escape hatch (typed as UseChatWSReturn / UseChatSSEReturn / UseChatRESTReturn) for migration safety from the legacy hooks they were ported from.
Adapter notes
useChatWSspeaks the Reeve gateway protocol v3 (connect.challengehandshake,chat.send/chat.history,chatevents withstate: delta | final | error | aborted). Reconnect uses exponential backoff capped at 30s / 10 attempts. ReturningnullfromonTokenExpiredsurfaces "Authentication failed".useChatSSEposts to${endpoint}/${sessionKey}/messagesand parses the kit's canonical frame protocol (message-start,part-start,text-delta,part-end,message-end). The legacy AgentPik v1 part shape (part: { type, ... }) is accepted as a fallback.useChatRESTis generic overTConversation. You providetoMessages(required) and optionaltoSuggestions/toProgressprojectors so the kit stays agnostic to your DTO. Polling is opt-in viapollIntervalMs > 0and requires a non-nullconversationId.useChatVoiceis a recording-state machine; STT is injected viatranscribe(blob, mimeType). Pick'tap-to-record'or'push-to-talk'. Wrap anyChatAdapter— voice is composable with WS, SSE, REST, or Echo.
Primitives
All primitives consume the ChatProvider context (no prop drilling) and emit rkit-* class names you can target from your own stylesheet.
| Primitive | Key props | Behavior |
|---|---|---|
| ChatProvider | adapter, theme?, children | Context root. theme.density ∈ compact \| comfortable, theme.variant ∈ panel \| terminal \| floating \| fullscreen |
| ChatRoot | variant?, className? | Outer flex container; resolves variant from prop or theme |
| ChatHeader | title?, subtitle?, actions?, children? | Top bar with title/subtitle/actions; children overrides the default layout |
| ChatMessageList | renderBubble?, smartAutoScroll? | Renders adapter.messages; auto-scrolls only when user is near the bottom |
| ChatBubble | message, renderAttachment?, showTimestamp? | Single message bubble; shows streaming cursor when meta.isStreaming === true |
| ChatComposer | placeholder?, autosize?, maxLength?, attachmentSlot?, voiceSlot? | Textarea + send/stop button; Enter sends, Shift-Enter newline |
| ChatStatusPill | labels? | Renders adapter.status with a colored dot + label |
| ChatStreamIndicator | onStop?, onRetry?, onDismiss?, labels? | Shows current stream phase + elapsed timer + Stop / Retry / Dismiss actions |
| ChatErrorBanner | onRetry?, onDismiss? | Renders adapter.error.message with Retry / Dismiss; hides itself when no error |
| ChatProgressMeter | progress?, showLabelAt? | Renders adapter.progress; shows stage label once percent ≥ threshold |
| ChatSuggestionChips | chips?, showSkip?, onPick | Renders adapter.suggestions; optional synthetic Skip chip |
| ChatEmptyState | greeting?, hero?, suggestions?, onPick? | Rendered only when adapter.messages.length === 0 |
| ChatVoiceButton | voiceAdapter, mode?, cancelDragThresholdPx? | Mic button; drives a ChatVoiceAdapter; exposes --audio-level CSS var |
| ChatCaptureCard | onCapture, consentText?, tcpaText?, placeholder?, submitLabel?, onSkip? | Email/phone capture with consent + TCPA disclosure |
| ChatCompletionScreen | score?, stageTitle?, bullets?, emailGate?, children? | Post-flow summary with optional score + email gate |
Typed content blocks (DEV-1077)
ChatBubble walks message.content and dispatches each item via a BlockRenderer registry. String items render as text; structured items render via their registered renderer (with a JSON-pretty fallback for unknown types).
| Block | Source | Default renderer |
| --- | --- | --- |
| tool_call | studio-handler tool invocations | Collapsed header with condensed args summary + expand toggle for full JSON |
| tool_result | studio-handler tool results | Status pill + summary + body dispatched by tool_name (cost chip / render-status badge / brand card / chain preview / memory list / agent output / JSON fallback) |
| render_confirm_required | DEV-1074 cost-threshold gate | Total cost + per-stage breakdown + Approve / Cancel buttons |
| auto_run_confirm_required | DEV-1091 auto-run gate | URL + cost range + step list + est duration + Approve / Cancel |
| auto_run_progress | DEV-1091 webhook progress stream | Step / message header + <progress> bar when pct is known |
| chain_preview | DEV-1075 (materialize_chain_from_dna, load_chain_session) | Per-stage BFS chips + optional CTA buttons |
Action button clicks fire adapter.action({ run_id, action_id }). The WS adapter emits a chat.action event over the open socket; SSE / REST / custom adapters implement the optional action method themselves.
ChatStreamIndicator is tool-call aware. During the streaming phase, it inspects the last block of the most recent assistant message:
tool_call→ "Reeve is using<tool_name>…"auto_run_progress(non-terminal) → "<step>:<message>"- otherwise the default
Streaming…label
Overriding a renderer
Pass blockRenderers to <ChatProvider> to override individual renderers without losing the defaults for the rest:
import { ChatProvider, type BlockProps } from '@meetreeve/chat-kit';
function BidCard({ block }: BlockProps) {
// ...custom UI keyed to your app's bid_card / agent shape
return <div>...</div>;
}
<ChatProvider
adapter={adapter}
blockRenderers={{ tool_result: BidCard as never }}
>
{/* every other block type still uses the default */}
</ChatProvider>Presets
| Preset | Default transport | Layout | Use when |
|---|---|---|---|
| <ReeveChat> | useChatWS if wsToken, else useChatEcho | Header + messages + stream indicator + composer | General Reeve chat surface |
| <ReeveChat.Terminal> | useChatWS | Status pill bar + monospace lines + monospace composer | Power-user / dev console |
| <ReeveChat.Readiness> | useChatSSE (with restFallback → useChatREST) | Progress meter + messages + suggestion chips (with Skip) + capture card at 100% | Intake / qualification flow |
| <ReeveChat.Voice> | useChatWS wrapped by useChatVoice | Header with status pill + messages + composer with mic button | Voice-first chat surface |
| <ReeveChat.Library> | useChatREST | Header + empty state with starter prompts + messages with book-card bubble override | Book / library / knowledge-base chat |
Every preset accepts an adapter prop that, when provided, replaces the auto-configured one. Pass any ChatAdapter to override.
Theming
The kit ships an opt-in CSS file that defines every token under :root and a few data-rkit-* selectors. Import once at your app root:
import '@meetreeve/chat-kit/styles/tokens.css';Override tokens at any scope by re-declaring the CSS custom properties:
:root {
--rkit-accent: #2563eb; /* brand accent — used by user bubbles + voice button */
--rkit-bubble-radius: 0.5rem; /* tighten bubble corners */
--rkit-font-base: 'Inter', system-ui, sans-serif;
}
/* Scope a single surface */
.my-chat-panel {
--rkit-accent: #16a34a;
}Theme hooks
Toggle bundled themes by setting a data attribute on any ancestor of <ChatRoot> (typically <html> or <body>):
[data-rkit-theme='dark']— dark surface palette[data-rkit-density='compact']— tighter spacing + smaller base text[data-rkit-variant='terminal']— monospace + transparent bubbles (also applied automatically by<ReeveChat.Terminal>)
Token groups
| Group | Sample tokens | Purpose |
|---|---|---|
| Surface | --rkit-bg, --rkit-bg-raised, --rkit-bg-muted, --rkit-fg, --rkit-fg-muted, --rkit-border | Base background / foreground stack |
| Brand | --rkit-accent, --rkit-accent-fg | Primary action color |
| Bubbles | --rkit-bubble-user-bg, --rkit-bubble-assistant-bg, --rkit-bubble-system-bg, --rkit-bubble-radius, --rkit-bubble-max-width | Per-role bubble styling |
| Composer | --rkit-composer-bg, --rkit-composer-border, --rkit-composer-radius | Composer surface |
| Chips | --rkit-chip-bg, --rkit-chip-fg, --rkit-chip-radius | Suggestion / empty-state chips |
| Capture card | --rkit-capture-card-bg, --rkit-capture-card-radius | Lead capture card |
| Voice | --rkit-voice-idle-bg, --rkit-voice-recording-bg, --rkit-voice-ring-color | Voice button + audio ring |
| Status | --rkit-status-connected, --rkit-status-connecting, --rkit-status-authenticating, --rkit-status-reconnecting, --rkit-status-disconnected, --rkit-status-idle, --rkit-status-error, --rkit-status-demo | Status pill colors per ChatStatus |
| Spacing | --rkit-space-1 … --rkit-space-5 | Density scale (overridden by [data-rkit-density='compact']) |
| Type | --rkit-font-base, --rkit-font-mono, --rkit-text-sm, --rkit-text-base, --rkit-text-lg | Type stack + scale |
| Z indices | --rkit-z-panel, --rkit-z-banner, --rkit-z-overlay | Stacking |
See src/styles/tokens.css for the full enumerated list (~50 tokens across 12 groups, plus the dark / compact / terminal theme blocks).
Examples
examples/standalone.tsx— Tier 1:<ReeveChat>with auto-mode + token refresh.examples/composed.tsx— Tier 2: hand-composed primitives with a custom bubble renderer + custom voice slot.
License
MIT
