457 lines
17 KiB
Markdown
457 lines
17 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 tail # View live logs from deployed worker
|
||
```
|
||
|
||
**Critical Deployment Checklist (must be followed exactly):**
|
||
|
||
1. **Never use npm.** Always run commands with pnpm (e.g., `pnpm install`, `pnpm dev`, `pnpm build`).
|
||
|
||
2. **Run the standard Next.js production build:**
|
||
```bash
|
||
pnpm build
|
||
```
|
||
Do not skip this step—it’s the fastest way to catch type or compile issues before packaging for Cloudflare.
|
||
|
||
3. **Fix lint errors before deploying:**
|
||
```bash
|
||
pnpm lint
|
||
```
|
||
Resolve every reported error (warnings are acceptable) before proceeding so Cloudflare builds don’t fail mid-process.
|
||
|
||
4. **Before every deploy, run the Cloudflare build step:**
|
||
```bash
|
||
npx @opennextjs/cloudflare build
|
||
```
|
||
If this fails, fix the error before proceeding—Wrangler deploys without this step will push stale assets.
|
||
|
||
5. **Deploy only after a successful OpenNext build:**
|
||
```bash
|
||
npx wrangler deploy
|
||
```
|
||
**Do NOT use `--env` flags.** The deployment uses the default configuration from `wrangler.jsonc` with route `agents.nicholai.work`.
|
||
|
||
**Troubleshooting:**
|
||
- "No updated asset files to upload": you skipped the Cloudflare build; rerun step 2
|
||
- ESLint config errors during build: they're informational—build still succeeds, but address them separately
|
||
- Viewport metadata warning: move viewport values from metadata to generateViewport per Next.js docs
|
||
|
||
**No deploy is compliant unless each step above is completed and verified.**
|
||
|
||
## Architecture
|
||
|
||
### Agent Configuration
|
||
- **Standard agents** defined via environment variables: `AGENT_N_URL`, `AGENT_N_NAME`, `AGENT_N_DESCRIPTION` (where N = 1, 2, 3...)
|
||
- **Custom agents** use special environment variables:
|
||
- `CUSTOM_AGENT_WEBHOOK` - n8n webhook for custom agent message processing
|
||
- `CUSTOM_AGENT_REGISTRATION_WEBHOOK` - n8n webhook for storing agent prompts
|
||
- `/api/agents` dynamically discovers standard 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
|
||
- Custom agents (created via Morgan) have `agentId` format: `custom-{uuid}`
|
||
|
||
### 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:
|
||
- Standard agents (`agent-N`): Uses `AGENT_N_URL` from environment variables
|
||
- Custom agents (`custom-*`): Uses `CUSTOM_AGENT_WEBHOOK` from environment variables
|
||
4. Message forwarded to n8n webhook with all metadata (including systemPrompt for custom agents)
|
||
5. Response parsed from n8n (supports streaming chunks, tool calls, and regular messages)
|
||
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`
|
||
|
||
### Agent Forge Feature
|
||
- **Morgan (Agent Architect)** - Typically `agent-2`, creates custom agents users can pin or use immediately
|
||
- System prompt: `.fortura-core/web-agents/agent-architect-web.md`
|
||
- **Custom Agent Creation Flow:**
|
||
1. User asks Morgan to create an agent
|
||
2. Morgan outputs `messageType: "tool_call"` with `create_agent_package` payload
|
||
3. Client displays `AgentForgeCard` with animated reveal
|
||
4. User actions: **Use now** (registers + switches), **Pin for later** (saves to localStorage), **Share** (copies to clipboard)
|
||
- **Storage:** Custom agents stored in localStorage `pinned-agents` array with structure:
|
||
```typescript
|
||
{
|
||
agentId: "custom-{uuid}",
|
||
displayName: string,
|
||
summary: string,
|
||
tags: string[],
|
||
systemPrompt: string,
|
||
recommendedIcon: string,
|
||
whenToUse: string,
|
||
pinnedAt: string,
|
||
note?: string
|
||
}
|
||
```
|
||
- Custom agents use `CUSTOM_AGENT_WEBHOOK` instead of numbered agent URLs
|
||
- Registration webhook (`CUSTOM_AGENT_REGISTRATION_WEBHOOK`) stores prompts when user pins/uses agents
|
||
|
||
## Code Structure
|
||
|
||
### API Routes
|
||
- `src/app/api/agents/route.ts` - Discovers and returns configured agents from environment variables
|
||
- `src/app/api/agents/create/route.ts` - Registers custom agents with n8n via registration webhook
|
||
- `src/app/api/chat/route.ts` - Proxies chat messages to agent webhooks, handles streaming responses and diff tool calls
|
||
- `src/app/api/flags/route.ts` - Returns feature flags from environment variables
|
||
|
||
### 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/agent-forge-card.tsx` - Displays custom agent creation UI with animated reveal
|
||
- `src/components/pinned-agents-drawer.tsx` - Sliding drawer for managing pinned custom agents
|
||
- `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 check agent type and extract appropriate webhook URL:
|
||
```typescript
|
||
// For standard agents
|
||
const match = agentId.match(/agent-(\d+)/)
|
||
if (match) {
|
||
const agentIndex = match[1]
|
||
const webhookUrl = process.env[`AGENT_${agentIndex}_URL`]
|
||
}
|
||
|
||
// For custom agents
|
||
if (agentId.startsWith('custom-')) {
|
||
const webhookUrl = process.env.CUSTOM_AGENT_WEBHOOK
|
||
}
|
||
```
|
||
|
||
### LocalStorage Keys
|
||
- Agent selection: `selected-agent` (full object), `selected-agent-id` (string)
|
||
- Per-agent session: `chat-session-{agentId}`
|
||
- Per-agent messages: `chat-messages-{agentId}`
|
||
- Custom agents: `pinned-agents` (array of PinnedAgent objects)
|
||
|
||
### 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"`)
|
||
3. Code node output (`[{ output: { messageType, content, toolCall? } }]`)
|
||
4. Regular JSON with various fields (`response`, `message`, `output`, `text`)
|
||
5. Plain text fallback
|
||
|
||
### n8n Response Format Requirements
|
||
**CRITICAL:** For proper tool call handling (especially Morgan), n8n workflows must output in this exact format:
|
||
|
||
```json
|
||
[{
|
||
"output": {
|
||
"messageType": "regular_message" | "tool_call",
|
||
"content": "Message text (always present)",
|
||
"toolCall": { // Only when messageType is "tool_call"
|
||
"type": "tool_call",
|
||
"name": "create_agent_package" | "show_diff",
|
||
"payload": {
|
||
// Tool-specific data structure
|
||
}
|
||
}
|
||
}
|
||
}]
|
||
```
|
||
|
||
**Implementation in n8n:**
|
||
- Use a **Code node** (NOT structured output parser) after the LLM node
|
||
- The Code node unwraps nested `output` fields and ensures clean JSON:
|
||
```javascript
|
||
const llmOutput = $input.item.json.output || $input.item.json;
|
||
let actual = llmOutput;
|
||
while (actual.output && typeof actual.output === 'object') {
|
||
actual = actual.output; // Unwrap nested output
|
||
}
|
||
return { json: { output: actual } };
|
||
```
|
||
|
||
**Morgan-specific requirements:**
|
||
- System prompt must instruct LLM to output valid JSON matching the schema above
|
||
- For `create_agent_package`, payload must include: `agentId`, `displayName`, `summary`, `tags`, `systemPrompt`, `hints.recommendedIcon`, `hints.whenToUse`
|
||
- The `systemPrompt` should be wrapped in Web Agent Bundle format
|
||
|
||
### 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`
|
||
- Route: `agents.nicholai.work` (configured at top level, not in env section)
|
||
- Environment variables: Configured in top-level `vars` section
|
||
- Compatibility flags: `nodejs_compat`, `global_fetch_strictly_public`
|
||
- **Important:** Deployment uses default configuration - do not use `--env` flags
|
||
|
||
### 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
|
||
|
||
### Top Button Layout
|
||
- "Start new chat" button positioned on left side of manuscript panel
|
||
- "View pinned agents" bookmark button positioned on right side
|
||
- Uses flex layout with `ml-auto` to push bookmark to the right
|
||
|
||
### Message Display
|
||
- User messages: positioned on right with glass bubble styling, text left-aligned within bubble (natural flow)
|
||
- Message content limited to 75% max-width with word-wrapping for long text
|
||
- Code blocks in messages have horizontal scrolling with proper overflow handling
|
||
- Assistant messages: markdown-rendered with copy button and syntax highlighting
|
||
- Error messages: red text, optional hint field displayed below
|
||
- Loading state: animated shimmer bar
|
||
- Scrollbars: gray color (neutral), hidden by default, shown on hover
|
||
|
||
### Pinned Agents Drawer
|
||
**Mobile Design:**
|
||
- Full-width bottom sheet sliding up from bottom
|
||
- Drag handle indicator visible at top
|
||
- Semi-transparent dark background
|
||
- Responsive layout for touch interaction
|
||
|
||
**Desktop Design:**
|
||
- Side drawer that slides out from behind the main manuscript card to the right
|
||
- Positioned relative to the manuscript panel (not viewport-locked)
|
||
- Partially hidden behind main card, with left edge not visible
|
||
- Shorter height, centered vertically on screen
|
||
- Reduced opacity (40% less) for subtle appearance
|
||
|
||
**Card Functionality:**
|
||
- Minimalist cards showing: agent name, handle, and start chat button
|
||
- Hover reveals: full description and action button
|
||
- Smooth height animation on hover using CSS Grid transitions
|
||
- Drag-and-drop reordering with visual feedback
|
||
- Dragged cards render above others (z-index layering)
|
||
- Delete button only visible on hover
|
||
|
||
## 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
|
||
- `VOICE_INPUT_ENABLED` (default: false) - Future feature for voice input (not yet implemented)
|
||
|
||
### 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
|
||
|
||
## Debugging and Troubleshooting
|
||
|
||
### Checking Agent Configuration
|
||
```bash
|
||
# View live logs from deployed worker
|
||
npx wrangler tail
|
||
|
||
# Test agent webhook directly
|
||
curl -X POST $AGENT_1_URL \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"message":"test","sessionId":"test-session"}'
|
||
```
|
||
|
||
### Checking n8n Response Format
|
||
Look for these log patterns in `npx wrangler tail`:
|
||
- `[v0] Webhook response body` - Shows raw n8n response
|
||
- `[v0] Parsed webhook data` - Shows parsed response object
|
||
- `[v0] parsedOutput messageType` - Shows detected message type (regular_message or tool_call)
|
||
|
||
### Checking LocalStorage
|
||
```javascript
|
||
// Browser console
|
||
localStorage.getItem('pinned-agents') // Custom agents array
|
||
localStorage.getItem('chat-messages-agent-2') // Morgan's chat history
|
||
localStorage.getItem('selected-agent') // Currently selected agent
|
||
```
|
||
|
||
### Common Issues
|
||
|
||
**Custom agents not working:**
|
||
- Verify `CUSTOM_AGENT_WEBHOOK` is set in `wrangler.jsonc` vars
|
||
- Check browser console for agentId format (`custom-{uuid}`)
|
||
- Ensure custom agent has systemPrompt in localStorage
|
||
|
||
**Morgan not creating agents:**
|
||
- Verify Morgan's n8n workflow has Code node for output unwrapping
|
||
- Check response format matches schema (messageType, content, toolCall)
|
||
- System prompt must instruct to output valid JSON
|
||
|
||
**Deployment shows "No updated asset files":**
|
||
- You skipped `npx @opennextjs/cloudflare build` step
|
||
- Run the OpenNext build before deploying again
|