Chat
Preview

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 ChunkRow objects.
  • 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

FloatingSidebarFullscreen
Use caseFloating helper, quick questionsPersistent side panelDedicated chat page
LayoutFixed bottom-center, animatedFixed right panel, full heightFull viewport with resizable panels
ChromeEngagement-driven revealAlways visibleAlways visible + nav sidebar
PromptEngagement-driven expansionAlways expandedAlways expanded
PanelsSheet overlaySheet overlayResizable 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

TypeDescription
markdownStreamdown-rendered markdown with optional inline citations
reasoningCollapsible "thinking" accordion. Supports custom label (defaults to "Thinking...")
taskHierarchical task list with progress metadata
tableTabular data with optional headers
cardTitle/description/link summary
alertInline alert banner (info, warning, success, destructive)
referencesCollapsible source list linked to inline citations
artifactInline document preview card. Expands to a side panel (fullscreen) or Sheet overlay (floating/sidebar).

Need a custom chunk type? See the Chunk Registry.

Pages

On this page