Skip to content

Latest commit

 

History

History
708 lines (570 loc) · 20.8 KB

File metadata and controls

708 lines (570 loc) · 20.8 KB

CLAUDE.md - AI Assistant Guide for reachat

Project Overview

reachat is a React UI library for building chat/LLM experiences. It provides customizable, composable components for building chat interfaces with support for markdown rendering, rich text input with mentions and slash commands, file uploads, session management, and theming via Tailwind CSS.

Tech Stack

Technology Version Purpose
React 18+ UI framework
TypeScript 4.9.5 Type safety
Tailwind CSS 4.x Styling
Vite 5.x Build tool & dev server
Storybook 8.x Component development
Vitest 1.x Testing
reablocks 9.x Base UI components
Tiptap 3.x Rich text editor framework
Floating UI 0.27.x Popup positioning
motion 12.x Animations
Zod 3.x / 4.x Runtime prop validation

Directory Structure

reachat/
├── src/                    # Source code
│   ├── index.ts           # Main entry point - exports all public APIs
│   ├── types.ts           # Core TypeScript interfaces
│   ├── theme.ts           # Theme system definitions
│   ├── Chat.tsx           # Root Chat component
│   ├── ChatContext.ts     # React context for chat state
│   ├── AppBar/            # App bar component
│   ├── ChatBubble/        # Chat bubble component
│   ├── ChatInput/         # Input field components
│   ├── ChatSuggestions/   # Suggestion chips component
│   ├── ComponentCatalog/  # Dynamic component rendering system
│   ├── Markdown/          # Markdown rendering (code, tables, etc.)
│   ├── MessageStatus/     # Loading/status indicators
│   ├── SessionMessages/   # Message display components
│   ├── SessionsList/      # Session list/grouping components
│   ├── utils/             # Utility functions
│   └── assets/            # SVG icons
├── stories/               # Storybook stories and examples
├── .storybook/            # Storybook configuration
├── dist/                  # Build output (generated)
└── scripts/               # Build scripts

Quick Commands

# Install dependencies
npm install

# Start Storybook development server (port 9009)
npm start

# Run tests
npm test

# Run tests with coverage
npm run test:coverage

# Build library for production
npm run build

# Lint code
npm run lint

# Fix lint issues
npm run lint:fix

# Format code with Prettier
npm run prettier

# Build Storybook for deployment
npm run build-storybook

Core Concepts

Component Architecture

The library uses a composable slot-based architecture. The main Chat component wraps children and provides context:

<Chat sessions={sessions} activeSessionId={activeId}>
  <SessionMessagePanel>
    <SessionMessages />
    <ChatInput />
  </SessionMessagePanel>
</Chat>

Key Data Types

A session is a flat, ordered list of role-tagged messages. This replaces the old question/response pair model so agentic transcripts (tool calls, consecutive assistant messages, system notices) can be represented.

// Core data structures in src/types.ts
type MessageRole = 'user' | 'assistant' | 'system' | 'tool' | (string & {});

/** Who sent a message — lets multiple people/agents share one session */
interface MessageAuthor {
  id?: string;
  name: string;
  /** Image URL or custom node */
  avatar?: string | ReactNode;
}

interface Message {
  id: string;
  /** What kind of participant wrote the message */
  role: MessageRole;
  /** Which participant wrote it — renders an avatar + name header */
  author?: MessageAuthor;
  /** Markdown content of the message */
  content: string;
  createdAt?: Date;
  updatedAt?: Date;
  /** Sources referenced by this message (typically assistant messages) */
  sources?: ConversationSource[];
  /** Files attached to this message (typically user messages) */
  files?: ConversationFile[];
  /** Arbitrary structured data, eg. `{ toolCallId, toolCallName, args }` */
  metadata?: Record<string, any>;
}

interface Session {
  id: string;
  title?: string;
  createdAt?: Date;
  updatedAt?: Date;
  messages?: Message[];
  /** @deprecated Use `messages` — auto-converted internally */
  conversations?: Conversation[];
}

MessageRole is open-ended — custom role strings are allowed and fall back to the assistant presentation in SessionMessage.

Multi-User / Multi-Agent Sessions

role and author are orthogonal: role picks the presentation (user bubble, assistant, system, tool), author identifies the participant. Multiple humans share role: 'user' with different authors; multiple agents either share role: 'assistant' or use custom role strings (eg. 'researcher'). When message.author is set, SessionMessage renders a MessageAuthorBadge header (avatar + name) above the default body (opt out with showAuthor={false}). Custom children replace the whole default template; compose the exported MessageAuthorBadge when a custom template should retain the author header. See stories/MultiParty.stories.tsx for multi-agent, group-chat and per-participant-styling demos.

Backwards Compatibility

The legacy Conversation shape ({ id, createdAt, question, response?, sources?, files? }) is still exported and still accepted on Session.conversations, but is @deprecated. Two helpers in src/utils/messages.ts (both public exports) handle the conversion:

/** Each conversation becomes a `user` message and, when a response exists, an `assistant` message. */
conversationsToMessages(conversations: Conversation[]): Message[];

/** Returns `session.messages`, falling back to converting `session.conversations`. */
getSessionMessages(session?: Session | null): Message[];

Generated ids are ${conversation.id}-question and ${conversation.id}-response. When both messages and conversations are present, messages wins. All internal consumers read messages exclusively through getSessionMessages() — do the same in new code rather than reading session.messages directly.

Message Rendering

SessionMessages normalizes the active session with getSessionMessages(), paginates over the resulting Message[], and renders one SessionMessage card per message. Its render prop receives the message list:

<SessionMessages>
  {(messages: Message[]) =>
    messages.map((message, i) => (
      <SessionMessage
        key={message.id}
        message={message}
        isLast={i === messages.length - 1}
      />
    ))
  }
</SessionMessages>

SessionMessage picks its presentation from message.role:

Role Presentation
user files + markdown + long-content expand overlay
assistant markdown + MessageSources + MessageActions + loading cursor when isLast && isLoading
system / tool assistant-style content with the role's theme class; no actions or cursor
custom (eg. agent roles) full assistant presentation — theme class falls back to assistant, and actions + loading cursor are included

Any message with an author additionally gets a MessageAuthorBadge header above its body.

When isLoading is true and the last message has role: 'user', SessionMessages renders a pending assistant placeholder with the blinking cursor after it.

MessageActions takes message={message} (copy copies message.content) and is rendered on assistant messages by default. MessageQuestion and MessageResponse remain as @deprecated thin wrappers over MessageContent so existing custom renderers keep compiling.

View Types

Three view modes are supported:

  • console - Full screen with sessions sidebar
  • companion - Compact/mobile view
  • chat - Chat only, no sessions list

Theme System

The theme is defined in src/theme.ts using a typed object with Tailwind classes:

const chatTheme: ChatTheme = {
  base: 'dark:text-white text-gray-500',
  console: 'flex w-full gap-4 h-full',
  // ... nested theme objects for each component
};

Components use the theme via useComponentTheme from reablocks:

const theme = useComponentTheme<ChatTheme>('chat', customTheme);

Message styling is keyed by role under messages.message:

messages: {
  message: {
    base: string;
    user: string;       // was: question
    assistant: string;  // was: response
    system: string;     // new — muted, centered informational style
    tool: string;       // new — compact style for tool activity
    author: {           // new — MessageAuthorBadge (avatar + name header)
      base: string;
      avatar: string;
      name: string;
    };
    cursor: string;
    overlay: string;
    expand: string;
    // ...files, sources, markdown, footer, scrollToBottom
  }
}

Rich Text Input Features

The library includes an advanced rich text input system built on Tiptap v3 with support for mentions and slash commands.

RichTextInput Component

Located in src/ChatInput/RichTextInput.tsx, this component provides:

  • Auto-expanding textarea with configurable min/max heights
  • Mentions support - Trigger with @ to mention users, files, or custom entities
  • Slash commands - Trigger with / for quick actions
  • Custom keyboard handling - Shift+Enter for multi-line, Enter to submit
  • Floating suggestions - Smart popup positioning using Floating UI
  • Keyboard navigation - Arrow keys, Enter/Tab to select, Escape to close

Exposed Methods via Ref:

interface RichTextInputRef {
  focus: () => void;
  getValue: () => string;
  setValue: (value: string) => void;
  insertText: (text: string) => void;
}

Usage Example:

<ChatInput
  mentions={{
    trigger: '@',
    items: [
      { id: '1', label: 'John Doe', description: 'Product Manager' },
      { id: '2', label: 'Jane Smith', description: 'Engineer' }
    ]
  }}
  commands={{
    trigger: '/',
    items: [
      { id: 'help', label: 'Help', description: 'Get help', type: 'action' },
      { id: 'search', label: 'Search', description: 'Search docs', type: 'insert' }
    ]
  }}
/>

MentionList Component

Located in src/ChatInput/MentionList.tsx, this floating popup component:

  • Displays suggestion items with keyboard navigation
  • Auto-scrolls to keep selected item visible
  • Supports custom rendering via renderItem and renderEmpty callbacks
  • Full ARIA accessibility attributes
  • Smart positioning to stay within viewport bounds

Suggestion Types

Core types defined in src/ChatInput/types.ts:

// Base suggestion item
interface SuggestionItem {
  id: string;
  label: string;
  description?: string;
  icon?: ReactNode;
  metadata?: Record<string, any>;
}

// For @mentions
interface MentionItem extends SuggestionItem {
  value?: string; // Override display value
}

// For /commands
interface SlashCommandItem extends SuggestionItem {
  shortcut?: string; // Keyboard shortcut hint
  type?: 'insert' | 'action';
}

// Configuration for suggestions
interface SuggestionConfig<T = SuggestionItem> {
  trigger: string;
  items?: T[];
  onSearch?: (query: string) => Promise<T[]>;
  onSelect?: (item: T) => void;
  maxResults?: number;
  renderItem?: (item: T) => ReactNode;
  renderEmpty?: () => ReactNode;
}

Theme Support

New theme sections in src/theme.ts:

input: {
  popup: {
    base: string;        // Popup container styles
    content: string;     // List content wrapper
    item: string;        // Individual item
    itemHighlighted: string; // Active/selected item
    itemIcon: string;    // Icon wrapper
    itemContent: string; // Text content wrapper
    itemLabel: string;   // Primary label
    itemDescription: string; // Secondary description
    itemShortcut: string; // Shortcut hint
    empty: string;       // Empty state
    loading: string;     // Loading state
  },
  tag: {
    base: string;        // Tag styles in editor
    mention: string;     // Mention-specific styles
    command: string;     // Command-specific styles
  },
  editor: {
    base: string;        // Editor wrapper
    container: string;   // Content container
    placeholder: string; // Placeholder text
  }
}

Dynamic Component Rendering (ComponentCatalog)

The library includes a ComponentCatalog system that allows LLMs to render custom React components via JSON specifications inside fenced code blocks. Located in src/ComponentCatalog/.

See src/ComponentCatalog/README.md for full usage documentation with examples.

How It Works

  1. Developer defines components with Zod schemas via componentCatalog()
  2. The catalog is passed to <Chat components={catalog}> which wires in a remark plugin and <pre> override
  3. When the LLM emits a ```component code block containing JSON, the system validates the spec against the Zod schema and renders the matching React component
  4. catalog.systemPrompt() generates LLM instructions describing available components

Key Files

File Purpose
componentCatalog.ts Main factory function — creates the catalog object
types.ts TypeScript interfaces (ComponentDefinition, ComponentSpec, etc.)
ComponentPre.tsx <pre> override that intercepts code blocks by language tag
ComponentRenderer.tsx Validates JSON and renders components with error boundary
validateSpec.ts Four-step validation pipeline (JSON parse, lookup, Zod, children)
generatePrompt.ts Generates LLM system prompt from definitions
ComponentError.tsx Default error display component
chartComponentDef.tsx Pre-built chart component definition using reaviz

Quick Example

import { componentCatalog } from 'reachat';
import { z } from 'zod';

const catalog = componentCatalog({
  WeatherCard: {
    description: 'Displays weather for a city',
    props: z.object({
      city: z.string(),
      temperature: z.number()
    }),
    component: ({ city, temperature }) => (
      <div>{city}: {temperature}°F</div>
    )
  }
});

<Chat sessions={sessions} components={catalog}>
  <SessionMessages />
  <ChatInput />
</Chat>

The LLM emits:

\`\`\`component
{ "type": "WeatherCard", "props": { "city": "SF", "temperature": 68 } }
\`\`\`

JSON Spec Format

  • Single: { "type": "Name", "props": { ... } }
  • Multiple: [{ "type": "A", "props": {} }, { "type": "B", "props": {} }]
  • Nested: { "type": "Parent", "props": {}, "children": [{ "type": "Child", "props": {} }] }

Error Handling

Four error types: invalid_json, unknown_component, invalid_props, render_error. Each component is wrapped in a React error boundary. Custom error UI via onError callback in options.

Dependencies

  • zod — regular dependency, used for runtime prop validation and system prompt generation
  • reaviz (optional peer dep) — required only when using createChartComponentDef()

Test Coverage

Tests are co-located in the ComponentCatalog/ directory:

  • componentCatalog.spec.ts — factory function tests
  • validateSpec.spec.ts — 18 validation pipeline test cases
  • generatePrompt.spec.ts — system prompt generation tests
  • chartComponentDef.spec.ts — chart definition validation tests

Code Conventions

Import Aliases

Use @/ for absolute imports from src/:

// Good
import { ChatContext } from '@/ChatContext';

// Avoid relative paths across directories
import { ChatContext } from '../../../ChatContext'; // Bad

The ESLint rule no-relative-import-paths enforces this (same folder imports are allowed).

Component Patterns

  1. Functional Components with TypeScript:
interface ComponentProps {
  /** JSDoc comment for prop */
  propName: string;
}

export const Component: FC<ComponentProps> = ({ propName }) => {
  // ...
};
  1. Forward Refs when exposing methods:
export interface ComponentRef {
  focus: () => void;
}

export const Component = forwardRef<ComponentRef, ComponentProps>((props, ref) => {
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus()
  }));
});
  1. Context consumption:
const { theme, isLoading, sendMessage } = useContext(ChatContext);

File Organization

Each component module follows this structure:

ComponentName/
├── index.ts           # Re-exports public APIs
├── ComponentName.tsx  # Main component
└── SubComponent.tsx   # Related sub-components

Styling Conventions

  1. Use Tailwind CSS classes via the theme system
  2. Use cn() from reablocks for conditional class merging:
<div className={cn(theme.base, { [theme.active]: isActive })} />
  1. Dark mode uses dark: prefix in Tailwind classes
  2. Theme tokens defined in src/index.css using @theme inline

Code Style

  • Semicolons: Required
  • Quotes: Single quotes
  • Trailing commas: None
  • Indentation: 2 spaces
  • Line width: 80 characters

Testing

Tests use Vitest with jsdom environment. Test files are co-located with source:

utils/
├── grouping.ts
└── grouping.spec.ts

Run tests:

npm test              # Watch mode
npm run test:coverage # With coverage report

Storybook

Stories are located in /stories/ directory and follow the pattern:

import { Meta } from '@storybook/react';

export default {
  title: 'Demos/ComponentName',
  component: ComponentName
} as Meta;

export const Default = () => <ComponentName />;
export const WithProps = () => <ComponentName prop="value" />;

Build Process

The build creates three outputs (ESM-only — no UMD/CJS):

  1. ESM (dist/index.js) - Modern ES modules
  2. CSS (dist/index.css) - Tailwind-compiled styles
  3. Types (dist/index.d.ts) - TypeScript declarations

Key Dependencies

  • reablocks: Base component library (Button, Textarea, etc.)
  • reakeys: Keyboard shortcuts
  • react-markdown: Markdown rendering
  • react-syntax-highlighter: Code highlighting
  • date-fns: Date utilities
  • lodash: Utility functions
  • @tiptap/react: Rich text editor framework (v3.x) with extensions for:
    • Document/paragraph/text structure
    • Hard breaks and placeholders
    • Mention support for @mentions
  • @floating-ui/react: Smart popup positioning for suggestion dropdowns
  • zod: Runtime prop validation for ComponentCatalog

Common Tasks

Adding a New Component

  1. Create directory: src/NewComponent/
  2. Create main component file: NewComponent.tsx
  3. Create index.ts with exports
  4. Add theme properties to ChatTheme in src/theme.ts
  5. Export from src/index.ts
  6. Create story in stories/NewComponent.stories.tsx

Modifying the Theme

  1. Update interface in src/theme.ts (ChatTheme)
  2. Add default values in chatTheme object
  3. Use via theme.newProperty in components

Adding Markdown Features

  1. Check existing plugins in src/Markdown/plugins/
  2. Add new remark/rehype plugins to the remarkPlugins prop
  3. Custom renderers go in the Markdown component

Configuring Mentions and Slash Commands

The ChatInput component accepts mentions and commands props for rich text functionality:

Static Items:

<ChatInput
  mentions={{
    trigger: '@',
    items: [
      { id: '1', label: 'User Name', description: 'Role', icon: <Icon /> }
    ]
  }}
/>

Dynamic Search:

<ChatInput
  commands={{
    trigger: '/',
    onSearch: async (query) => {
      const results = await searchAPI(query);
      return results.map(r => ({ id: r.id, label: r.name }));
    },
    maxResults: 10
  }}
/>

Custom Selection Handler:

<ChatInput
  mentions={{
    trigger: '@',
    items: mentionItems,
    onSelect: (item) => {
      console.log('Selected:', item);
      // Handle custom logic
    }
  }}
/>

Custom Rendering:

<ChatInput
  commands={{
    trigger: '/',
    items: commandItems,
    renderItem: (item) => (
      <div>
        <strong>{item.label}</strong>
        <span>{item.shortcut}</span>
      </div>
    ),
    renderEmpty: () => <div>No commands found</div>
  }}
/>

Important Notes

  • The library is designed for React 18+
  • All components support dark/light themes
  • CSS is injected via JS for library builds (vite-plugin-css-injected-by-js)
  • SVGs are imported as React components using vite-plugin-svgr
  • The package uses ES modules ("type": "module")
  • Rich text input uses Tiptap v3 with a document/paragraph/text node structure
  • Suggestion popups use Floating UI with flip/shift middleware for smart positioning
  • Accessibility: All interactive components include proper ARIA attributes

Git Workflow

  • Pre-commit hooks run via Husky
  • Prettier formats staged files automatically
  • Follow conventional commit messages