ChatPreview
Composable chat surfaces with variant-driven layout and chunk-based message rendering. You own the data layer.
Overview
Cubitt's chat system ships three explicit layout variants — Floating, Sidebar, and Fullscreen — each composed from shared primitives. The library is data-layer agnostic — you own querying, streaming, and persistence. Cubitt renders whatever normalized rows you feed it.
- Each variant is its own compound component (
Chat.Floating,Chat.Sidebar,Chat.Fullscreen). - Message output is chunk-based — agent responses are rendered as a sequence of typed
ChunkRowobjects. - A per-instance chunk registry lets you extend the system with custom renderers.
- Mode transitions are consumer-controlled — swap variants using your own state.
Pick a Variant
| Floating | Sidebar | Fullscreen | |
|---|---|---|---|
| Use case | Floating helper, quick questions | Persistent side panel | Dedicated chat page |
| Layout | Fixed bottom-center, animated | Fixed right panel, full height | Full viewport with resizable panels |
| Chrome | Engagement-driven reveal | Always visible | Always visible + nav sidebar |
| Prompt | Engagement-driven expansion | Always expanded | Always expanded |
| Panels | Sheet overlay | Sheet overlay | Resizable side panels |
Quick Start
Minimal floating chat (example uses TanStack DB, but any data layer works):
import {
Chat,
useChatSelection,
MessageFeedScrollAnchor,
} from "@tilt-legal/cubitt-components/composites";
import { useLiveQuery } from "@tanstack/react-db";
function MyFloatingChat({ messagesCollection, chunksCollection, onModeChange }) {
const { conversationId, handleNew, handleSelect } = useChatSelection();
const { data: messages = [] } = useLiveQuery((q) =>
q
.from({ m: messagesCollection })
.where(({ m }) => m.conversationId === conversationId)
.orderBy(({ m }) => m.createdAt, "asc")
.select(({ m }) => m),
);
const isStreaming = messages.some((msg) => msg.status === "running");
return (
<Chat.Floating
conversationId={conversationId}
hasMessages={messages.length > 0}
isStreaming={isStreaming}
onCancelStreaming={() => {
/* cancel your stream */
}}
onNewConversation={handleNew}
onSend={(payload) => {
/* write to DB, start stream */
}}
>
<Chat.Floating.Shell>
<Chat.Floating.Header
conversations={history}
onConversationSelect={handleSelect}
onNewConversation={handleNew}
onModeChange={onModeChange}
/>
<Chat.Floating.Content>
<Chat.MessageList>
{messages.map((msg) => (
<MessageRow
key={msg.id}
messageId={msg.id}
chunksCollection={chunksCollection}
/>
))}
</Chat.MessageList>
<MessageFeedScrollAnchor />
</Chat.Floating.Content>
<Chat.Floating.Footer
files={{ available: myFiles }}
instructions={{ available: myInstructions }}
/>
</Chat.Floating.Shell>
</Chat.Floating>
);
}Built-in Chunks
| Type | Description |
|---|---|
markdown | Streamdown-rendered markdown with optional inline citations |
reasoning | Collapsible "thinking" accordion. Supports custom label (defaults to "Thinking...") |
task | Hierarchical task list with progress metadata |
table | Tabular data with optional headers |
card | Title/description/link summary |
alert | Inline alert banner (info, warning, success, destructive) |
references | Collapsible source list linked to inline citations |
artifact | Inline document preview card. Expands to a side panel (fullscreen) or Sheet overlay (floating/sidebar). |
Need a custom chunk type? See the Chunk Registry.
Pages
Architecture
Composition model, shared context, variant layout contexts, and data flow.
Data Contracts
ChatMessageView, ChunkRow, MessagePayload, callbacks, and useChatSelection.
Floating
Floating chat bubble with engagement-driven animation lifecycle.
Sidebar
Fixed side panel with always-visible chrome.
Fullscreen
Multi-panel page with resizable layout and conversation nav.
Messages
MessageEntry, ChunkList, message feeds, and citation sync.
Prompt Input
Footer, file picker, instruction picker, and payload structure.
Chunk Registry
Custom chunk renderers, registry API, and chunk-panel co-location.