Correspondents/CLAUDE.md

268 lines
9.8 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Multi-agent chat interface deployed to Cloudflare Workers. Users select from configured AI agents and chat with them through n8n webhooks. The interface features a glass-morphism design with mobile support, markdown rendering with code highlighting, and a custom diff visualization tool.
## Key Commands
### Development
```bash
pnpm dev # Start Next.js dev server at localhost:3000
pnpm build # Build Next.js application
pnpm lint # Run ESLint checks
pnpm test # Run Vitest test suites
pnpm test:ui # Run tests with Vitest UI
pnpm test:coverage # Generate test coverage report
```
### Deployment to Cloudflare
```bash
npx @opennextjs/cloudflare build # Convert Next.js build to Cloudflare Workers format
npx wrangler deploy # Deploy to Cloudflare (requires OpenNext build first)
npx wrangler deploy --env="" # Deploy to default environment explicitly
npx wrangler tail # View live logs from deployed worker
```
**Critical**: Always run `npx @opennextjs/cloudflare build` before deploying. The standard `next build` alone is insufficient for Cloudflare deployment.
## Architecture
### Agent Configuration
- Agents are defined via environment variables: `AGENT_N_URL`, `AGENT_N_NAME`, `AGENT_N_DESCRIPTION` (where N = 1, 2, 3...)
- `/api/agents` dynamically discovers agents by iterating through numbered environment variables
- Each agent has a webhook URL pointing to an n8n workflow
- Agent selection is persisted to localStorage using agent-specific keys
### Message Flow
1. User submits message through `ChatInterface` component
2. POST `/api/chat` receives message with agentId, sessionId, timestamp, and optional base64 images
3. Route extracts webhook URL from environment variables based on agentId format (`agent-N`)
4. Message forwarded to n8n webhook with all metadata
5. Response parsed from n8n (supports streaming chunks and diff tool calls)
6. Messages stored in localStorage per agent (`chat-messages-{agentId}`)
### Session Management
- Each agent has its own session: `chat-session-{agentId}` in localStorage
- Session ID format: `session-{agentId}-{timestamp}-{random}`
- New session created when user clicks "Start a fresh conversation"
- Messages persist across page reloads per agent
### Diff Tool Integration
- Custom markdown code fence: ` ```diff-tool ` with JSON payload
- n8n can send `type: "tool_call", name: "show_diff"` which is converted server-side
- `MarkdownRenderer` parses diff-tool blocks and renders via `DiffTool``DiffDisplay` components
- Diff props: `oldCode`, `newCode`, `title`, `language`
## Code Structure
### API Routes
- `src/app/api/agents/route.ts` - Discovers and returns configured agents from environment variables
- `src/app/api/chat/route.ts` - Proxies chat messages to agent webhooks, handles streaming responses and diff tool calls
### Core Components
- `src/app/page.tsx` - Main page with agent selection/persistence and chat interface mounting
- `src/components/chat-interface.tsx` - Full chat UI: message history, input composer, agent dropdown, image attachments
- `src/components/markdown-renderer.tsx` - Renders markdown with syntax highlighting and custom diff-tool blocks
- `src/components/diff-tool.tsx` - Wrapper for diff display functionality
- `src/components/diff-display.tsx` - Side-by-side diff visualization
### Styling
- Tailwind CSS 4.x with custom glass-morphism design system
- Custom colors defined in CSS variables (charcoal, burnt, terracotta, etc.)
- Mobile-specific classes: `mobile-shell`, `mobile-feed`, `mobile-composer`
- Framer Motion for animations and layout transitions
### Type Definitions
- `src/lib/types.ts` - All TypeScript interfaces for Agent, Message, API requests/responses
## Important Patterns
### Environment Variable Access
Always extract agent index from agentId before accessing env vars:
```typescript
const match = agentId.match(/agent-(\d+)/)
const agentIndex = match[1]
const webhookUrl = process.env[`AGENT_${agentIndex}_URL`]
```
### LocalStorage Keys
- Agent selection: `selected-agent` (full object), `selected-agent-id` (string)
- Per-agent session: `chat-session-{agentId}`
- Per-agent messages: `chat-messages-{agentId}`
### Response Parsing in /api/chat
The route handles multiple n8n response formats:
1. Streaming chunks (newline-delimited JSON with `type: "item"`)
2. Tool calls (`type: "tool_call", name: "show_diff"`)
3. Regular JSON with various fields (`response`, `message`, `output`, `text`)
4. Plain text fallback
### Image Handling
- Images converted to base64 via FileReader in browser
- Passed as `images: string[]` array to `/api/chat`
- Forwarded to n8n webhook for processing
- Preview thumbnails shown in composer with remove buttons
## Deployment Configuration
### wrangler.jsonc
- Worker entry: `.open-next/worker.js` (generated by OpenNext)
- Assets directory: `.open-next/assets`
- Production route: `agents.nicholai.work`
- Compatibility flags: `nodejs_compat`, `global_fetch_strictly_public`
### Build Output
- Standard Next.js build: `.next/`
- Cloudflare-adapted build: `.open-next/` (generated by @opennextjs/cloudflare)
- Only `.open-next/` is deployed to Cloudflare
## UI/UX Details
### Empty State
- Hero greeting animation with per-character stagger
- Agent selection buttons shown prominently
- Sample prompt cards (4 suggestions) users can click to populate input
### Composer Behavior
- Auto-expanding textarea (max 224px height)
- Agent dropdown required before sending first message
- Agent selection highlighted if not yet chosen
- Enter to send, Shift+Enter for new line
- Paperclip icon for image attachments
### Message Display
- User messages: right-aligned with glass bubble styling
- Assistant messages: markdown-rendered with copy button and syntax highlighting
- Error messages: red text, optional hint field displayed below
- Loading state: animated shimmer bar
## Feature Flag System
### Architecture
- Flags defined in `src/lib/flags.ts` with TypeScript interfaces
- Environment variable based: `IMAGE_UPLOADS_ENABLED`, `DIFF_TOOL_ENABLED`
- Runtime overrides supported via `registerRuntimeFlags()` (useful for testing)
- Global caching in `globalThis.__MULTI_AGENT_CHAT_FLAGS__`
- Client-side access via `useFlags()` hook that fetches from `/api/flags`
### Available Flags
- `IMAGE_UPLOADS_ENABLED` (default: true) - Controls image attachment UI and API validation
- `DIFF_TOOL_ENABLED` (default: true) - Controls diff visualization rendering
### Flag Checking Patterns
**Server-side (API routes):**
```typescript
import { getFlags } from '@/lib/flags'
const flags = getFlags()
if (!flags.IMAGE_UPLOADS_ENABLED) {
return NextResponse.json({ error: 'Feature disabled' }, { status: 403 })
}
```
**Client-side (Components):**
```typescript
import { useFlags } from '@/lib/use-flags'
const { flags, isLoading, error } = useFlags()
if (flags.IMAGE_UPLOADS_ENABLED) {
// Render feature UI
}
```
**Testing:**
```typescript
import { registerRuntimeFlags, resetFlagsCache } from '@/lib/flags'
beforeEach(() => {
resetFlagsCache()
registerRuntimeFlags({ IMAGE_UPLOADS_ENABLED: false })
})
```
### Configuration
- Development: Set in `.env.local`
- Production: Set in `wrangler.jsonc` under `env.production.vars`
- Parse format: "true"/"false", "1"/"0", "yes"/"no" (case-insensitive)
## Testing
### Test Framework
- **Vitest 4.0** - Fast unit test framework with Jest-compatible API
- **@testing-library/react** - React component testing utilities
- **jsdom** - Browser environment simulation
### Test Structure
Tests organized in `__tests__/` by domain:
- `lib/` - Utility and library tests (flags, types, helpers)
- `api/` - API route tests with mocked fetch
- `components/` - React component rendering tests
- `flags/` - Feature flag integration tests
### Running Tests
```bash
pnpm test # Run all tests in watch mode
pnpm test:ui # Open Vitest UI dashboard
pnpm test:coverage # Generate coverage report
```
### Test Patterns
**API Route Testing:**
```typescript
import { POST } from '@/app/api/chat/route'
import { NextRequest } from 'next/server'
const request = new NextRequest('http://localhost/api/chat', {
method: 'POST',
body: JSON.stringify({ message: 'test', agentId: 'agent-1' })
})
const response = await POST(request)
```
**Component Testing:**
```typescript
import { render, screen } from '@testing-library/react'
import { MarkdownRenderer } from '@/components/markdown-renderer'
render(<MarkdownRenderer content="# Hello" />)
expect(screen.getByText('Hello')).toBeInTheDocument()
```
**Flag Behavior Testing:**
```typescript
// Test feature when flag is disabled
process.env.IMAGE_UPLOADS_ENABLED = 'false'
resetFlagsCache()
const response = await POST(requestWithImages)
expect(response.status).toBe(403)
```
### Test Configuration
- Config: `vitest.config.ts`
- Setup: `vitest.setup.ts` (mocks window.matchMedia, IntersectionObserver, etc.)
- Path aliases: `@/` resolves to `./src/`
- Environment: jsdom with globals enabled
- Coverage: v8 provider, excludes node_modules, .next, __tests__
### Important Testing Notes
- Always call `resetFlagsCache()` in beforeEach when testing flags
- Mock `global.fetch` for API tests that call webhooks
- Use `vi.mock()` for module mocking, `vi.spyOn()` for function spying
- Clean up mocks with `vi.clearAllMocks()` in beforeEach
- Test both success and error paths for all features
## Development Notes
- Next.js 15.5.4 with App Router (all components are Client Components via "use client")
- React 19.1.0 with concurrent features
- Deployed to Cloudflare Workers, not Vercel
- No database - all state in localStorage and n8n workflows
- Mobile-first responsive design with specific mobile breakpoint styling
- Vitest for testing with Testing Library for React components