Skip to main content
Normalized for Mintlify from knowledge-base/neurigraph-memory-architecture/neurigraph-tool-references/07-AI-Chat-UI-Component-Library.mdx.

Clean-Room Specification: AI Chat UI Component Library

Purpose of This Document

This document specifies the architecture, component hierarchy, runtime system, and implementation patterns for a headless React component library purpose-built for conversational AI interfaces. The library provides 20+ unstyled composable primitives organized around threads, messages, composers, and branching — with a protocol-driven runtime that abstracts over multiple AI provider backends. This specification enables full independent implementation from scratch.

1. Architecture Overview

1.1 Three-Layer Architecture

The system is organized into three distinct layers:
Primitives: Unstyled React components that render zero visual chrome — they emit bare semantic HTML elements (<div>, <p>, <form>, <button>) and rely entirely on the consumer to provide CSS/Tailwind classes. Each primitive is a namespace object containing sub-components (e.g., ThreadPrimitive.Root, ThreadPrimitive.Messages). React Bindings: Hooks and context providers that bridge the runtime layer to React’s rendering model. These use a custom reactivity system (not React state) to minimize re-renders. Core Runtime: Pure TypeScript classes that manage conversation state, message trees, streaming, tool execution, and provider communication. Provider-agnostic — adapters translate between specific AI SDK protocols and the internal representation.

1.2 Package Structure

The library is organized as a monorepo with these key packages:

2. Core Runtime System

2.1 AssistantRuntime

The root runtime that owns the entire conversation state tree.
Construction: Created via provider-specific factory functions. For the Vercel AI SDK adapter:

2.2 ThreadRuntime

Manages a single conversation thread with message tree, branching, and streaming.

2.3 Message Tree (Branching Model)

Messages are stored in a tree structure that supports branching (editing a user message creates a new branch, preserving the old one):
Key invariant: getMessages() always returns a linear sequence — the currently active path through the tree. Switching branches changes which path is active but preserves all other branches.

2.4 ThreadMessage Types

2.5 Content Part Types (9 Types)

2.6 Message Status

2.7 Streaming Protocol

The runtime consumes a stream of chunks that incrementally build the assistant message:
Chunk Types: Stream consumption: The runtime maintains a mutable draft message. Each chunk mutates the draft in place, then notifies subscribers. On stream completion (when status chunk arrives or stream closes), the draft is finalized into an immutable ThreadMessage and added to the repository.

3. Tap-Based Micro-Reactivity System

3.1 Problem Statement

Standard React state management (useState/useReducer) causes entire component subtrees to re-render when any state changes. For chat UIs with hundreds of messages, each containing multiple content parts that stream character-by-character, this creates catastrophic re-rendering.

3.2 Solution: Tap Subscriptions

The library implements a tap-based reactivity system that bypasses React’s state model entirely. Runtime objects are observable stores. React hooks subscribe to specific slices of state and only trigger re-renders when those specific slices change.

3.3 useAui / useAuiState / useAuiEvent Hooks

Key pattern: useAuiState(useThreadRuntime, t => t.isRunning) only causes a re-render when isRunning transitions between true/false, even though the thread’s messages, status, and other properties may be changing constantly during streaming.

4. React Primitives

4.1 ThreadPrimitive

Namespace object for thread-level components.
ThreadPrimitive.Viewport — Auto-scroll behavior:
ThreadPrimitive.Messages — Renders the linear message path:

4.2 MessagePrimitive

MessagePrimitive.Content — Maps content parts to components:

4.3 ComposerPrimitive

ComposerPrimitive.Input — Auto-resizing textarea:
ComposerPrimitive.Send — Auto-disable logic:

4.4 BranchPickerPrimitive

Allows navigating between message branches (when user edits a message, creating alternate conversation paths):

4.5 ActionBarPrimitive

Context-sensitive action buttons for messages:
ActionBarPrimitive.Root — Auto-hide behavior:

4.6 AttachmentPrimitive

4.7 Conditional Rendering with If Components

Every primitive namespace includes an If component for declarative conditional rendering:

5. Context System

5.1 Context Hierarchy

Contexts nest to provide scoped access to runtime instances:

5.2 Context Hooks

5.3 AssistantRuntimeProvider

Top-level provider that wraps the entire chat UI:

6. Vercel AI SDK Adapter

6.1 Bridge Hook

The primary adapter bridges Vercel AI SDK’s useChat hook to the runtime system:

6.2 Message Format Conversion


7. Tool Execution and Approval Flow

7.1 Tool Definition

7.2 Tool Approval (Human-in-the-Loop)

When a tool call requires human approval before execution:
  1. UI renders approval component — detected by checking message.status.type === "requires-action"
  2. User approves/rejects → calls threadRuntime.addToolResult({ toolCallId, result }) where result is either the execution output or an error/rejection message
  3. Runtime updates the ToolCallContentPart with the result and continues

8. Attachment System

8.1 Attachment Adapters

Attachments are handled via an adapter pattern that supports different upload strategies:

8.2 Built-in Adapters


9. Markdown Rendering

9.1 MarkdownText Component

The @assistant-ui/react-markdown package provides a component that renders assistant text content as rich markdown:

9.2 Smooth Streaming Animation

When smooth is enabled, text doesn’t appear all at once — instead characters are revealed progressively:

10. Tailwind CSS Integration

10.1 aui-* Variant Selectors

The Tailwind plugin provides custom variants that map to component states:
This registers variants: Usage example:

11. Speech (Text-to-Speech) Integration


12. Thread Management and Persistence

12.1 Multi-Thread Support

12.2 External Store Adapter

For persisting threads to a backend:

13. Putting It All Together — Full Composition Example


14. Behavioral Test Cases

Thread Operations

  1. Empty thread renders empty state: When messages array is empty, ThreadPrimitive.Empty children are rendered, ThreadPrimitive.Messages renders nothing.
  2. Message ordering: Messages render in the order returned by MessageRepository.getMessages() (linear active path).
  3. Auto-scroll on new content: When user is scrolled to bottom and new streaming text arrives, viewport scrolls to keep bottom visible.
  4. Auto-scroll disengage: When user scrolls up manually, auto-scroll stops. New messages do NOT force scroll.
  5. Auto-scroll re-engage: When user scrolls back to bottom, auto-scroll re-engages for subsequent messages.

Message Branching

  1. Edit creates branch: Editing a user message creates a new child of the same parent, preserving the original branch.
  2. Branch navigation: BranchPickerPrimitive.Previous/Next cycle through sibling branches at the branching point.
  3. Branch count accuracy: BranchPickerPrimitive.Count shows total siblings, Number shows 1-indexed current.
  4. Branch isolation: Switching branches replaces all messages after the branching point with the alternate path.
  5. Nested branches: Branches can exist at multiple depths — each operates independently.

Streaming

  1. Text delta accumulation: Multiple text-delta chunks for the same part index concatenate correctly.
  2. Tool call streaming: tool-call-begin followed by tool-call-delta chunks produces incrementally parsed args.
  3. Mixed content streaming: Text, tool calls, and reasoning parts can arrive interleaved — each routed to correct part index.
  4. Stream cancellation: ComposerPrimitive.Cancel calls threadRuntime.cancelRun(), which aborts the stream and sets status to incomplete/cancelled.
  5. Status finalization: Stream ending with status chunk finalizes the message; without it, status defaults to complete/unknown.

Composer

  1. Submit on Enter: Pressing Enter (without Shift) triggers form submission when text is non-empty.
  2. Newline on Shift+Enter: Shift+Enter inserts a newline without submitting.
  3. Disabled while running: Send button is disabled when thread.isRunning === true.
  4. Auto-resize: Textarea height grows with content up to max-height, then scrolls internally.
  5. Attachment flow: Adding file creates PendingAttachment → displayed in composer → on send, adapter.send() converts to CompleteAttachment.

Tool Execution

  1. Tool approval flow: Message with status requires-action renders approval UI; addToolResult resolves it and continues generation.
  2. Tool rejection: Calling addToolResult with isError: true sends rejection to model.
  3. Custom tool renderers: by_name component map renders specific components for named tools.
  4. Fallback tool renderer: Unknown tool names render with the Fallback component.

Reactivity

  1. Selective re-rendering: Changing thread.isRunning does NOT re-render message components that only subscribe to message content.
  2. Streaming efficiency: During text streaming, only the active TextContentPart component re-renders — not sibling parts or other messages.
  3. useAuiState equality: Custom equality functions prevent re-renders when selector output is structurally identical.

Action Bar

  1. Copy to clipboard: ActionBarPrimitive.Copy extracts all text content parts, joins them, copies to clipboard via navigator.clipboard.writeText.
  2. Autohide behavior: With autohide="not-last", only the last message’s action bar is visible; others appear on hover.
  3. Reload regenerates: ActionBarPrimitive.Reload removes the assistant message and calls startRun to regenerate.
  4. Feedback submission: Positive/Negative feedback updates message.metadata.feedback and emits event.

Markdown Rendering

  1. GFM support: Tables, strikethrough, task lists render correctly via remark-gfm.
  2. Code highlighting: Fenced code blocks with language annotation get syntax highlighting.
  3. LaTeX rendering: Inline $...$ and display $$...$$ render as mathematical notation.
  4. Smooth streaming: With smooth enabled, text reveals character-by-character via requestAnimationFrame.
  5. Link safety: External links render with target="_blank" rel="noopener".

Attachments

  1. Image preview: Image attachments show thumbnail in composer before sending.
  2. Text file reading: .txt, .md, .json files are read as text and included in message content.
  3. Remove before send: Clicking remove on a pending attachment calls adapter.remove() and removes from composer.
  4. Accept filter: File picker only shows files matching adapter’s accept filter.

Thread Persistence

  1. Export/Import roundtrip: thread.export() produces a serializable repository; thread.import() restores the full tree structure including branches.
  2. Thread switching: switchToThread(id) loads messages from external store and sets as active.
  3. New thread creation: switchToNewThread() creates an empty thread and sets as active.

Provider Adapter

  1. Vercel message conversion: Vercel AI SDK messages with toolInvocations correctly convert to ToolCallContentParts.
  2. Vercel stream mapping: Vercel’s streaming protocol chunks map to internal AssistantStreamChunks.
  3. Bidirectional sync: Adding a message via the runtime is reflected back to Vercel’s useChat state.