From ab027583824f62202be305d4646d768dd8201f1e Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:14:45 +0800 Subject: [PATCH] feat: add implementation plan and monorepo guide for DreamChat project --- doc/README.md | 44 +++ doc/api-spec.md | 572 +++++++++++++++++++++++++++++ doc/architecture.md | 490 +++++++++++++++++++++++++ doc/database-schema.md | 709 ++++++++++++++++++++++++++++++++++++ doc/deployment.md | 725 +++++++++++++++++++++++++++++++++++++ doc/frontend-guide.md | 635 ++++++++++++++++++++++++++++++++ doc/implementation-plan.md | 447 +++++++++++++++++++++++ doc/monorepo-guide.md | 664 +++++++++++++++++++++++++++++++++ 8 files changed, 4286 insertions(+) create mode 100644 doc/README.md create mode 100644 doc/api-spec.md create mode 100644 doc/architecture.md create mode 100644 doc/database-schema.md create mode 100644 doc/deployment.md create mode 100644 doc/frontend-guide.md create mode 100644 doc/implementation-plan.md create mode 100644 doc/monorepo-guide.md diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..a09ab68 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,44 @@ +# DreamChat Project Documentation + +This directory contains comprehensive planning and implementation documentation for the DreamChat project. + +## Documentation Structure + +| Document | Description | +|----------|-------------| +| [architecture.md](./architecture.md) | System architecture, tech stack, component diagrams | +| [monorepo-guide.md](./monorepo-guide.md) | pnpm workspace setup, shared packages | +| [database-schema.md](./database-schema.md) | PostgreSQL schema, vector store design, entities | +| [api-spec.md](./api-spec.md) | REST API & WebSocket specifications, OpenAPI | +| [implementation-plan.md](./implementation-plan.md) | Phased roadmap, milestones, deliverables | +| [frontend-guide.md](./frontend-guide.md) | Frontend architecture, component hierarchy | +| [deployment.md](./deployment.md) | Docker Compose, devcontainer, deployment guide | + +## Quick Reference + +### Tech Stack +- **Backend**: NestJS (TypeScript) +- **Frontend**: React + Vite (TypeScript) +- **Package Manager**: pnpm (monorepo workspaces) +- **Database**: PostgreSQL with pgvector +- **ORM**: Prisma +- **Vector Store**: PostgreSQL (pgvector) + LangChain + Local HuggingFace Embeddings +- **LLM**: OpenRouter (flexible adapter pattern) +- **Real-time**: WebSocket (shared types package) +- **Auth**: Keycloak + Password-based +- **Web Scraping**: Puppeteer/Playwright (headless browser) + +### Implementation Phases +1. **MVP**: Single character chat with memory +2. **Phase 2**: Story generation with branching tree view +3. **Phase 3**: Multi-character group chat + +### Key Design Patterns +- **Adapter Pattern**: LLM providers, data import sources +- **Repository Pattern**: Database access +- **Strategy Pattern**: Predefined web scrapers +- **Observer Pattern**: WebSocket events + +--- + +*Last updated: 2026-02-23* diff --git a/doc/api-spec.md b/doc/api-spec.md new file mode 100644 index 0000000..40e231f --- /dev/null +++ b/doc/api-spec.md @@ -0,0 +1,572 @@ +# DreamChat API Specification + +## Overview + +This document defines the REST API and WebSocket specifications for DreamChat. + +- **REST API**: For CRUD operations, file uploads, and synchronous requests +- **WebSocket**: For real-time chat streaming +- **OpenAPI**: Auto-generated from NestJS decorators for frontend client generation + +## Base URL + +``` +Development: http://localhost:3000/api +Production: https://api.dreamchat.example/api +``` + +## Authentication + +### JWT Token Flow + +```http +POST /api/auth/login +Content-Type: application/json + +{ + "username": "user@example.com", + "password": "securepassword" +} + +Response: +{ + "accessToken": "eyJhbGciOiJIUzI1NiIs...", + "refreshToken": "eyJhbGciOiJIUzI1NiIs...", + "expiresIn": 3600 +} +``` + +### Keycloak Integration + +```http +GET /api/auth/keycloak +Redirect to Keycloak login + +Callback: +GET /api/auth/keycloak/callback?code=... +Response: { accessToken, refreshToken, expiresIn } +``` + +### WebSocket Auth + +WebSocket connections authenticate via query parameter: +``` +ws://localhost:3000/chat?token=eyJhbGciOiJIUzI1NiIs... +``` + +## REST API Endpoints + +### Authentication Module + +| Method | Endpoint | Description | Auth | +|--------|----------|-------------|------| +| POST | `/auth/login` | Local login | Public | +| POST | `/auth/refresh` | Refresh token | Public | +| POST | `/auth/logout` | Logout | Bearer | +| GET | `/auth/me` | Get current user | Bearer | +| GET | `/auth/keycloak` | Keycloak login URL | Public | +| GET | `/auth/keycloak/callback` | Keycloak callback | Public | + +### Users Module + +| Method | Endpoint | Description | Auth | +|--------|----------|-------------|------| +| POST | `/users` | Register new user | Public | +| GET | `/users/:id` | Get user profile | Bearer | +| PATCH | `/users/:id` | Update user | Bearer (own only) | +| DELETE | `/users/:id` | Delete user | Bearer (own/admin) | + +### Characters Module + +```typescript +// DTOs +class CreateCharacterDto { + name: string; + personalityPrompt: string; + backstory?: string; + attributes?: Record; + avatarUrl?: string; + config?: Record; +} + +class CharacterResponseDto { + id: string; + name: string; + avatarUrl: string; + personalityPrompt: string; + backstory: string; + attributes: Record; + createdAt: Date; +} +``` + +| Method | Endpoint | Description | Auth | +|--------|----------|-------------|------| +| GET | `/characters` | List user's characters | Bearer | +| POST | `/characters` | Create character | Bearer | +| GET | `/characters/:id` | Get character details | Bearer | +| PATCH | `/characters/:id` | Update character | Bearer (owner) | +| DELETE | `/characters/:id` | Delete character | Bearer (owner) | + +**Example Requests:** + +```http +POST /api/characters +Authorization: Bearer {token} +Content-Type: application/json + +{ + "name": "Alice", + "personalityPrompt": "You are Alice, a curious and adventurous explorer...", + "backstory": "Alice grew up in a small village...", + "attributes": { + "traits": ["curious", "brave", "witty"], + "age": 25, + "species": "human", + "skills": ["navigation", "survival"] + } +} + +Response: 201 Created +{ + "id": "uuid", + "name": "Alice", + "personalityPrompt": "...", + "attributes": { ... }, + "createdAt": "2026-02-23T10:00:00Z" +} +``` + +### Conversations Module + +```typescript +class CreateConversationDto { + characterId: string; + title?: string; +} + +class ConversationResponseDto { + id: string; + title: string; + character: CharacterSummaryDto; + messageCount: number; + createdAt: Date; +} +``` + +| Method | Endpoint | Description | Auth | +|--------|----------|-------------|------| +| GET | `/conversations` | List conversations | Bearer | +| POST | `/conversations` | Create conversation | Bearer | +| GET | `/conversations/:id` | Get conversation | Bearer | +| PATCH | `/conversations/:id` | Update conversation | Bearer | +| DELETE | `/conversations/:id` | Delete conversation | Bearer | + +### Messages Module + +```typescript +class CreateMessageDto { + content: string; +} + +class MessageResponseDto { + id: string; + role: 'user' | 'assistant'; + content: string; + tokensUsed?: number; + model?: string; + createdAt: Date; +} +``` + +| Method | Endpoint | Description | Auth | +|--------|----------|-------------|------| +| GET | `/conversations/:id/messages` | Get messages (paginated) | Bearer | +| POST | `/conversations/:id/messages` | Send message (non-streaming) | Bearer | +| DELETE | `/messages/:id` | Delete message | Bearer | + +### Import Module + +```typescript +class FileImportDto { + // Multipart form data + file: File; +} + +class UrlImportDto { + url: string; +} + +class ImportResponseDto { + id: string; + status: 'pending' | 'processing' | 'completed' | 'failed'; + sourceName: string; + fileSize?: number; + content?: string; + errorMessage?: string; + createdAt: Date; +} +``` + +| Method | Endpoint | Description | Auth | +|--------|----------|-------------|------| +| POST | `/import/file` | Import file (txt, pdf, md) | Bearer | +| POST | `/import/url` | Import from URL | Bearer | +| GET | `/import/:id` | Get import status | Bearer | +| GET | `/import` | List imports | Bearer | +| DELETE | `/import/:id` | Delete import | Bearer | + +**File Upload Request:** + +```http +POST /api/import/file +Authorization: Bearer {token} +Content-Type: multipart/form-data + +------Boundary +Content-Disposition: form-data; name="file"; filename="story.txt" +Content-Type: text/plain + +(file content here) +------Boundary-- + +Response: 202 Accepted +{ + "id": "import-uuid", + "status": "processing", + "sourceName": "story.txt", + "createdAt": "2026-02-23T10:00:00Z" +} +``` + +**URL Import Request:** + +```http +POST /api/import/url +Authorization: Bearer {token} +Content-Type: application/json + +{ + "url": "https://archiveofourown.org/works/12345678" +} + +Response: 202 Accepted +{ + "id": "import-uuid", + "status": "processing", + "sourceName": "https://archiveofourown.org/works/12345678" +} + +// If no scraper available: +Response: 400 Bad Request +{ + "error": "UNSUPPORTED_URL", + "message": "No scraper available for this URL" +} +``` + +### Story Module (Phase 2) + +```typescript +class CreateStoryBranchDto { + parentId?: string; // null for root + userDirection: string; +} + +class StoryBranchResponseDto { + id: string; + conversationId: string; + parentId?: string; + title: string; + content: string; + userDirection: string; + depth: number; + children: StoryBranchResponseDto[]; + createdAt: Date; +} +``` + +| Method | Endpoint | Description | Auth | +|--------|----------|-------------|------| +| GET | `/conversations/:id/story` | Get story tree | Bearer | +| POST | `/conversations/:id/story/branches` | Create new branch | Bearer | +| GET | `/story-branches/:id` | Get branch details | Bearer | +| PATCH | `/story-branches/:id` | Update branch | Bearer | +| DELETE | `/story-branches/:id` | Delete branch | Bearer | + +**Get Story Tree:** + +```http +GET /api/conversations/conv-id/story +Authorization: Bearer {token} + +Response: +{ + "root": { + "id": "root-uuid", + "title": "The Beginning", + "content": "Once upon a time...", + "depth": 0, + "children": [ + { + "id": "branch-1", + "title": "The Left Path", + "content": "You chose the left path...", + "userDirection": "Go left into the dark forest", + "depth": 1, + "children": [] + }, + { + "id": "branch-2", + "title": "The Right Path", + "content": "You chose the right path...", + "userDirection": "Go right towards the castle", + "depth": 1, + "children": [] + } + ] + } +} +``` + +### Multi-Character Module (Phase 3) + +```typescript +class AddParticipantDto { + characterId: string; + autoRespond: boolean; +} + +class ParticipantResponseDto { + id: string; + character: CharacterSummaryDto; + isActive: boolean; + autoRespond: boolean; +} +``` + +| Method | Endpoint | Description | Auth | +|--------|----------|-------------|------| +| GET | `/conversations/:id/participants` | List participants | Bearer | +| POST | `/conversations/:id/participants` | Add participant | Bearer | +| DELETE | `/conversations/:id/participants/:charId` | Remove participant | Bearer | + +## WebSocket Specification + +### Connection + +```javascript +const ws = new WebSocket('ws://localhost:3000/chat?token=JWT_TOKEN'); + +ws.onopen = () => { + console.log('Connected'); +}; + +ws.onmessage = (event) => { + const message = JSON.parse(event.data); + handleMessage(message); +}; +``` + +### Message Protocol + +All WebSocket messages use JSON format: + +```typescript +interface WebSocketMessage { + type: string; + payload: any; + timestamp: string; + requestId?: string; // For correlating responses +} +``` + +### Client → Server Events + +#### 1. Join Conversation + +```typescript +// Join a conversation room +{ + "type": "JOIN_CONVERSATION", + "payload": { + "conversationId": "conv-uuid" + }, + "requestId": "req-123" +} + +// Response +{ + "type": "CONVERSATION_JOINED", + "payload": { + "conversationId": "conv-uuid", + "history": [ /* last N messages */ ] + }, + "requestId": "req-123" +} +``` + +#### 2. Send Message (Streaming) + +```typescript +{ + "type": "SEND_MESSAGE", + "payload": { + "conversationId": "conv-uuid", + "content": "Hello, how are you?", + "streaming": true // Enable streaming response + }, + "requestId": "msg-456" +} +``` + +#### 3. Stop Generation + +```typescript +{ + "type": "STOP_GENERATION", + "payload": { + "conversationId": "conv-uuid" + } +} +``` + +#### 4. Leave Conversation + +```typescript +{ + "type": "LEAVE_CONVERSATION", + "payload": { + "conversationId": "conv-uuid" + } +} +``` + +### Server → Client Events + +#### 1. Message Acknowledged + +```typescript +{ + "type": "MESSAGE_ACK", + "payload": { + "messageId": "msg-uuid", + "status": "received" + }, + "requestId": "msg-456" +} +``` + +#### 2. Stream Chunk + +```typescript +{ + "type": "STREAM_CHUNK", + "payload": { + "conversationId": "conv-uuid", + "chunk": " part of response", + "isComplete": false + }, + "requestId": "msg-456" +} +``` + +#### 3. Stream Complete + +```typescript +{ + "type": "STREAM_COMPLETE", + "payload": { + "conversationId": "conv-uuid", + "messageId": "assistant-msg-uuid", + "content": "Full response text", + "tokensUsed": 150, + "model": "openai/gpt-4o" + }, + "requestId": "msg-456" +} +``` + +#### 4. Error + +```typescript +{ + "type": "ERROR", + "payload": { + "code": "LLM_ERROR", + "message": "Failed to generate response", + "details": { ... } + }, + "requestId": "msg-456" +} +``` + +### Error Codes + +| Code | Description | HTTP Status | +|------|-------------|-------------| +| `UNAUTHORIZED` | Invalid or expired token | 401 | +| `FORBIDDEN` | Insufficient permissions | 403 | +| `NOT_FOUND` | Resource not found | 404 | +| `VALIDATION_ERROR` | Invalid input data | 400 | +| `LLM_ERROR` | LLM provider error | 502 | +| `RATE_LIMITED` | Too many requests | 429 | +| `FILE_TOO_LARGE` | File exceeds 50MB | 413 | +| `UNSUPPORTED_URL` | No scraper for URL | 400 | + +## OpenAPI Configuration + +### NestJS Setup + +```typescript +// main.ts +const config = new DocumentBuilder() + .setTitle('DreamChat API') + .setDescription('Character simulation and chat API') + .setVersion('1.0') + .addBearerAuth() + .addTag('auth', 'Authentication endpoints') + .addTag('users', 'User management') + .addTag('characters', 'Character CRUD') + .addTag('conversations', 'Chat sessions') + .addTag('messages', 'Chat messages') + .addTag('import', 'Data import') + .build(); + +const document = SwaggerModule.createDocument(app, config); +SwaggerModule.setup('api/docs', app, document); + +// Also serve raw JSON for client generation +writeFileSync('./openapi-spec.json', JSON.stringify(document, null, 2)); +``` + +### Frontend Client Generation + +```bash +# Generate TypeScript client +npx openapi-generator-cli generate \ + -i ./openapi-spec.json \ + -g typescript-fetch \ + -o ./apps/frontend/src/api/generated \ + --additional-properties=supportsES6=true,npmName=dreamchat-api + +# Or use Orval for React Query hooks +npx orval --config ./orval.config.js +``` + +## Rate Limiting + +| Endpoint | Limit | +|----------|-------| +| Auth endpoints | 10 req/min | +| General API | 100 req/min | +| File upload | 5 req/min | +| WebSocket messages | 60 msg/min | +| LLM streaming | 20 requests/min | + +```typescript +// Rate limit headers +X-RateLimit-Limit: 100 +X-RateLimit-Remaining: 95 +X-RateLimit-Reset: 1645603200 +``` diff --git a/doc/architecture.md b/doc/architecture.md new file mode 100644 index 0000000..4672954 --- /dev/null +++ b/doc/architecture.md @@ -0,0 +1,490 @@ +# DreamChat System Architecture + +## Overview + +DreamChat is a character simulation platform built with a modular, extensible architecture. The system follows clean architecture principles with clear separation of concerns. + +## High-Level Architecture + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Client Layer │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ +│ │ React │ │ Vite │ │ WebSocket │ │ OpenAPI Generator │ │ +│ │ (UI) │ │ (Build) │ │ Client │ │ (API Client) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ API Gateway Layer │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ NestJS Backend │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ Auth │ │ Guards │ │ Validation │ │ WebSocket │ │ │ +│ │ │ Module │ │ (JWT/Keycloak)│ │ Pipes │ │ Gateway │ │ │ +│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ┌──────────────────┼──────────────────┐ + ▼ ▼ ▼ +┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐ +│ Domain Modules │ │ Service Layer │ │ Infrastructure │ +│ ┌────────────────┐ │ │ ┌────────────────┐ │ │ ┌────────────────┐ │ +│ │ Character │ │ │ │ LLM Service │ │ │ │ LangChain │ │ +│ │ Module │ │ │ │ (OpenRouter) │ │ │ │ Integration │ │ +│ └────────────────┘ │ │ └────────────────┘ │ │ └────────────────┘ │ +│ ┌────────────────┐ │ │ ┌────────────────┐ │ │ ┌────────────────┐ │ +│ │ Chat Module │ │ │ │ Memory Service │ │ │ │ Vector Store │ │ +│ │ (MVP Focus) │ │ │ │ (Vector DB) │ │ │ │ (pgvector) │ │ +│ └────────────────┘ │ │ └────────────────┘ │ │ └────────────────┘ │ +│ ┌────────────────┐ │ │ ┌────────────────┐ │ │ ┌────────────────┐ │ +│ │ Story Module │ │ │ │ Import Service │ │ │ │ Puppeteer │ │ +│ │ (Phase 2) │ │ │ │ (Adapter) │ │ │ │ (Scraper) │ │ +│ └────────────────┘ │ │ └────────────────┘ │ │ └────────────────┘ │ +│ ┌────────────────┐ │ │ ┌────────────────┐ │ │ ┌────────────────┐ │ +│ │ Multi-Char │ │ │ │ File Processor │ │ │ │ PDF Parser │ │ +│ │ (Phase 3) │ │ │ │ Service │ │ │ │ MD Parser │ │ +│ └────────────────┘ │ │ └────────────────┘ │ │ └────────────────┘ │ +└──────────────────────┘ └──────────────────────┘ └──────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Data Layer │ +│ ┌─────────────┐ ┌─────────────────────┐ ┌─────────────────────────────┐ │ +│ │ PostgreSQL │ │ pgvector Extension │ │ Keycloak (External) │ │ +│ │ (Primary) │ │ (Vector Store) │ │ (Auth Provider) │ │ +│ └─────────────┘ └─────────────────────┘ └─────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +## Module Dependencies + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Application Module │ +├─────────────────────────────────────────────────────────────────┤ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │ +│ │ Auth │ │ User │ │ Config │ │ +│ │ Module │──│ Module │ │ (Global) │ │ +│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ + ┌─────────────────────┼─────────────────────┐ + ▼ ▼ ▼ +┌───────────────┐ ┌───────────────┐ ┌───────────────────┐ +│ Character │ │ Chat │ │ Import/Export │ +│ Module │ │ Module │ │ Module │ +│ │ │ (MVP Focus) │ │ │ +│ • Attributes │ │ │ │ • File Adapter │ +│ • Personality│ │ • Messages │ │ • Web Adapter │ +│ • Backstory │ │ • WebSocket │ │ • Preprocessing │ +└───────────────┘ └───────────────┘ └───────────────────┘ + │ + ┌───────────────┴───────────────┐ + ▼ ▼ + ┌───────────────────┐ ┌───────────────────┐ +│ Story Module │ │ Multi-Char Module│ +│ (Phase 2) │ │ (Phase 3) │ +│ │ │ │ +│ • Branching Tree │ │ • Group Chat │ +│ • Open-ended Gen │ │ • Char-to-Char │ +│ • Tree View API │ │ • Address Direct │ +└─────────────────────┘ └───────────────────┘ +``` + +## Component Details + +### Backend (NestJS) + +#### 1. Auth Module +```typescript +// Dual authentication strategy +- KeycloakStrategy (OAuth2/OIDC) +- LocalStrategy (Password-based) +- JWT Guard for stateless auth +- Roles: USER, ADMIN + +// Prisma User model +- id, email, username +- passwordHash, keycloakSub +- role, isActive +``` + +#### 2. Character Module +```typescript +- CharacterController (REST) +- CharacterService (Business Logic) +- CharacterRepository (Prisma) +- DTOs: CreateCharacterDto, UpdateCharacterDto, CharacterResponseDto + +Entities: +- Character + - id, name, avatar + - personalityPrompt: string + - attributes: JSON (complex attribute system) + - backstory: string + - createdBy: User + - createdAt, updatedAt +``` + +#### 3. Chat Module (MVP) +```typescript +- ChatGateway (WebSocket) +- ChatService +- MessageService +- ConversationRepository (Prisma) + +Prisma Models: +- Conversation + - id, title + - characterId (relation) + - userId (relation) + - messages: Message[] + - messageCount, totalTokens + - createdAt, updatedAt + +- Message + - id, role (MessageRole enum: user | assistant | system) + - content: String + - tokensUsed: Int? + - model: String? + - metadata: Json? + - conversationId (relation) + - createdAt: DateTime + +WebSocket Events: +- client:send_message → server:receive_message → llm:generate → server:stream_response → client:receive_chunk +``` + +#### 4. Memory Service (LangChain + pgvector + Local Embeddings) +```typescript +- EmbeddingService (Adapter Pattern) + - generateEmbeddings(texts: string[]): Promise + - getDimension(): number + + Implementations: + - LocalEmbeddingProvider: Loads HuggingFace model via @xenova/transformers + - HuggingFaceAPIProvider: Uses HuggingFace Inference API + +- VectorStoreService (uses Prisma with pgvector extension) + - addDocument(conversationId, content, metadata) + - similaritySearch(conversationId, query, k=5) + - Uses raw Prisma queries with pgvector operators + +- MemoryManager + - buildContext(conversationId, currentMessage): string + - summarizeOldMessages(conversationId): Promise + - retrieveRelevantMemories(conversationId, query): Document[] + +Prisma Model: +- VectorMemory + - id + - conversationId (relation) + - content: String + - embedding: Unsupported("vector") // pgvector type + - metadata: Json? + - createdAt: DateTime +``` + +#### 5. LLM Service (Adapter Pattern) +```typescript +interface LLMProvider { + generate(messages: Message[]): Promise; + stream(messages: Message[]): AsyncIterable; + getTokenCount(text: string): number; +} + +class OpenRouterProvider implements LLMProvider { ... } +class OpenAIProvider implements LLMProvider { ... } +class OllamaProvider implements LLMProvider { ... } + +// Configuration via environment +LLM_PROVIDER=openrouter +LLM_MODEL=openai/gpt-4o +LLM_API_KEY=... +``` + +#### 6. Import Module (Adapter Pattern) +```typescript +interface ImportAdapter { + canHandle(source: ImportSource): boolean; + import(source: ImportSource): Promise; +} + +// File Adapters +class TextFileAdapter implements ImportAdapter { ... } +class PdfFileAdapter implements ImportAdapter { ... } +class MarkdownFileAdapter implements ImportAdapter { ... } + +// Web Adapters (Predefined scrapers) +abstract class WebScraperAdapter implements ImportAdapter { + protected abstract canHandleUrl(url: string): boolean; + protected abstract extractContent(page: Page): Promise; +} + +class AO3Scraper extends WebScraperAdapter { ... } +class FanfictionNetScraper extends WebScraperAdapter { ... } +// Each scraper validates URL pattern before processing +// Uses Puppeteer for headless browser + +// Data Preprocessing Pipeline +class DataPreprocessor { + clean(text: string): string; + chunk(text: string, maxChunkSize: number): string[]; + extractEntities(text: string): Entity[]; +} +``` + +### Frontend (React + Vite) + +#### Component Hierarchy +``` +App +├── AuthProvider (Keycloak + Local) +├── Router +│ ├── /login +│ │ └── LoginPage +│ │ ├── KeycloakLoginButton +│ │ └── PasswordLoginForm +│ ├── /characters +│ │ └── CharacterListPage +│ │ ├── CharacterCard[] +│ │ └── CreateCharacterButton +│ ├── /characters/:id +│ │ └── CharacterDetailPage +│ │ ├── CharacterAttributesEditor +│ │ ├── PersonalityPromptEditor +│ │ └── ChatHistory (if any) +│ ├── /chat/:conversationId (MVP Focus) +│ │ └── ChatPage +│ │ ├── ChatHeader (character info) +│ │ ├── MessageList +│ │ │ └── MessageBubble[] +│ │ └── ChatInput +│ │ └── MessageComposer +│ ├── /stories (Phase 2) +│ │ └── StoryListPage +│ │ └── StoryTreeView +│ └── /import +│ └── ImportPage +│ ├── FileUpload (Drag & Drop) +│ │ └── FileDropzone +│ ├── UrlInput +│ │ └── ScraperSelector +│ └── ProcessingProgress +└── Layout + ├── Sidebar + └── Header +``` + +#### State Management +```typescript +// Using Zustand or React Query +- authStore: AuthState +- characterStore: Character[] +- chatStore: + - currentConversation: Conversation + - messages: Message[] + - isStreaming: boolean + - wsConnection: WebSocket +- importStore: ImportJob[] +``` + +#### API Client Generation +```bash +# Generated from OpenAPI spec +npx openapi-generator-cli generate \ + -i http://localhost:3000/api-json \ + -g typescript-fetch \ + -o src/api/generated +``` + +## Data Flow + +### Chat Flow (MVP) +``` +┌─────────┐ ┌──────────┐ ┌────────────┐ ┌─────────────┐ +│ User │────▶│ Frontend │────▶│ WebSocket │────▶│ Chat │ +│ │ │ │ │ Gateway │ │ Gateway │ +└─────────┘ └──────────┘ └────────────┘ └──────┬──────┘ + │ + ┌────────────────────────────────┘ + │ + ▼ + ┌────────────────────────────────────────────────────────┐ + │ ChatService │ + │ 1. Save user message to DB │ + │ 2. Call MemoryManager.buildContext() │ + │ 3. Retrieve relevant memories (vector search) │ + │ 4. Build system prompt + context + user message │ + │ 5. Call LLMService.generateStream() │ + └────────────────────────┬───────────────────────────────┘ + │ + ▼ + ┌────────────────────────────────────────────────────────┐ + │ LLMService │ + │ 1. Select provider (OpenRouter) │ + │ 2. Format messages for provider │ + │ 3. Stream response chunks │ + │ 4. Return async iterator │ + └────────────────────────┬───────────────────────────────┘ + │ + ▼ + ┌────────────────────────────────────────────────────────┐ + │ Stream Response │ + │ 1. Send chunks via WebSocket │ + │ 2. Accumulate full response │ + │ 3. Save assistant message to DB │ + │ 4. Store in vector memory │ + └────────────────────────────────────────────────────────┘ +``` + +### File Import Flow +``` +┌──────────┐ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ +│ User │────▶│ Frontend │────▶│ POST /api │────▶│ Import │ +│ Upload │ │ FileSelect │ │ /import/file│ │ Controller│ +└──────────┘ └─────────────┘ └──────────────┘ └──────┬──────┘ + │ + ▼ + ┌─────────────────────────────────────────────────────────────────┐ + │ ImportService │ + │ 1. Validate file (type, size < 50MB) │ + │ 2. Select adapter based on mime-type │ + │ 3. Parse file to raw text │ + │ 4. Run DataPreprocessor.clean() │ + │ 5. Chunk into segments │ + │ 6. Store in import_documents table │ + └─────────────────────────────────────────────────────────────────┘ +``` + +### Web Scraping Flow +``` +┌──────────┐ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ +│ User │────▶│ Frontend │────▶│ POST /api │────▶│ Import │ +│ Enter URL│ │ URL Input │ │ /import/url │ │ Controller│ +└──────────┘ └─────────────┘ └──────────────┘ └──────┬──────┘ + │ + ▼ + ┌─────────────────────────────────────────────────────────────────┐ + │ WebImportService │ + │ 1. Validate URL format │ + │ 2. Find matching scraper (or reject) │ + │ 3. Launch Puppeteer, navigate to URL │ + │ 4. Extract content using scraper selectors │ + │ 5. Run DataPreprocessor.clean() │ + │ 6. Chunk and store │ + └─────────────────────────────────────────────────────────────────┘ +``` + +## Directory Structure (pnpm Monorepo) + +``` +dreamchat/ +├── apps/ +│ ├── backend/ +│ │ ├── src/ +│ │ │ ├── app.module.ts +│ │ │ ├── main.ts +│ │ │ ├── config/ +│ │ │ ├── common/ +│ │ │ ├── modules/ +│ │ │ │ ├── auth/ +│ │ │ │ ├── users/ +│ │ │ │ ├── characters/ +│ │ │ │ ├── chat/ +│ │ │ │ ├── import/ +│ │ │ │ ├── story/ +│ │ │ │ └── multi-character/ +│ │ │ └── shared/ +│ │ │ ├── services/ +│ │ │ └── prisma/ +│ │ ├── test/ +│ │ ├── Dockerfile +│ │ └── package.json +│ │ +│ └── frontend/ +│ ├── src/ +│ │ ├── main.tsx +│ │ ├── App.tsx +│ │ ├── api/ +│ │ ├── components/ +│ │ ├── pages/ +│ │ ├── stores/ +│ │ ├── hooks/ +│ │ └── utils/ +│ ├── public/ +│ ├── Dockerfile +│ └── package.json +│ +├── packages/ +│ ├── shared/ # Shared types & WebSocket definitions +│ │ ├── src/ +│ │ │ ├── websocket/ +│ │ │ │ ├── events.ts # WebSocket event types +│ │ │ │ ├── messages.ts +│ │ │ │ └── index.ts +│ │ │ ├── api/ +│ │ │ │ ├── dto.ts # Shared DTOs +│ │ │ │ └── index.ts +│ │ │ └── index.ts +│ │ ├── package.json +│ │ └── tsconfig.json +│ │ +│ └── config/ # Shared configurations +│ ├── eslint/ +│ └── typescript/ +│ +├── prisma/ # Database schema (shared) +│ ├── schema.prisma +│ ├── migrations/ +│ └── seed.ts +│ +├── docker-compose.yml +├── pnpm-workspace.yaml +├── package.json # Root package.json +├── .npmrc +├── .devcontainer/ +└── doc/ +``` + +### Package Management + +```yaml +# pnpm-workspace.yaml +packages: + - 'apps/*' + - 'packages/*' +``` + +```bash +# Install all dependencies +pnpm install + +# Add dependency to specific app +pnpm --filter @dreamchat/backend add @nestjs/jwt + +# Add shared package to apps +pnpm --filter @dreamchat/backend add @dreamchat/shared@workspace:* +``` + +## Security Considerations + +1. **Authentication**: JWT tokens with refresh strategy +2. **Authorization**: Role-based access control (RBAC) +3. **File Upload**: + - Size limit: 50MB + - Mime-type validation + - Storage outside web root +4. **Web Scraping**: + - URL whitelist (predefined scrapers only) + - Rate limiting per domain + - Content sanitization +5. **WebSocket**: + - Auth token validation on connection + - Message rate limiting per user +6. **Database**: + - Prepared statements (Prisma) + - Connection pooling diff --git a/doc/database-schema.md b/doc/database-schema.md new file mode 100644 index 0000000..891b282 --- /dev/null +++ b/doc/database-schema.md @@ -0,0 +1,709 @@ +# DreamChat Database Schema + +## Overview + +PostgreSQL with pgvector extension for vector storage. All data is stored locally (offline-first). + +## Extensions Required + +```sql +-- Enable required extensions +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS "pgvector"; +``` + +## Entity Relationship Diagram + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ users │ │ characters │ │ conversations │ +├─────────────────┤ ├─────────────────┤ ├─────────────────┤ +│ id (PK) │◄──────│ user_id (FK) │ │ id (PK) │ +│ email │ │ id (PK) │◄──────│ character_id(FK)│ +│ username │ │ name │ │ user_id (FK) │ +│ password_hash │ │ avatar_url │ │ title │ +│ keycloak_sub │ │ personality │ │ created_at │ +│ role │ │ backstory │ │ updated_at │ +│ created_at │ │ attributes │ └────────┬────────┘ +│ updated_at │ │ created_at │ │ +└─────────────────┘ │ updated_at │ │ + └─────────────────┘ │ +┌─────────────────┐ │ +│import_documents │ │ +├─────────────────┤ │ +│ id (PK) │ │ +│ user_id (FK) │ │ +│ source_type │ ┌─────────────────┐ │ +│ source_name │ │ messages │◄───────────────┘ +│ content │ ├─────────────────┤ +│ metadata │ │ id (PK) │ +│ vector_id │ │ conversation_id │ +│ created_at │ │ role │ +└─────────────────┘ │ content │ + │ tokens_used │ + │ model │ +┌─────────────────┐ │ metadata │ +│vector_memories │ │ created_at │ +├─────────────────┤ └─────────────────┘ +│ id (PK) │ +│ conversation_id │ ┌─────────────────┐ +│ content │ │ story_branches │ (Phase 2) +│ embedding │ ├─────────────────┤ +│ metadata │ │ id (PK) │ +│ created_at │ │ conversation_id │ +└─────────────────┘ │ parent_id (FK) │ + │ content │ + │ direction │ + │ metadata │ + │ created_at │ + └─────────────────┘ +``` + +## Table Definitions + +### 1. users + +Stores user account information. Supports both Keycloak (OIDC) and local password authentication. + +```sql +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + email VARCHAR(255) UNIQUE NOT NULL, + username VARCHAR(100) UNIQUE NOT NULL, + password_hash VARCHAR(255), -- NULL if using Keycloak + keycloak_sub VARCHAR(255) UNIQUE, -- NULL if using password auth + role VARCHAR(20) DEFAULT 'USER' CHECK (role IN ('USER', 'ADMIN')), + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + + -- At least one auth method must be set + CONSTRAINT auth_method_check CHECK ( + (password_hash IS NOT NULL) OR (keycloak_sub IS NOT NULL) + ) +); + +CREATE INDEX idx_users_email ON users(email); +CREATE INDEX idx_users_keycloak_sub ON users(keycloak_sub); +``` + +### 2. characters + +Character definitions with complex attribute system (JSONB for flexibility). + +```sql +CREATE TABLE characters ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + avatar_url TEXT, + + -- Core personality prompt sent to LLM + personality_prompt TEXT NOT NULL, + + -- Backstory context for the character + backstory TEXT, + + -- Complex attribute system (structured JSON) + -- Example: {"traits": ["brave", "witty"], "age": 25, "species": "human"} + attributes JSONB DEFAULT '{}', + + -- Character configuration + config JSONB DEFAULT '{ + "model": "openai/gpt-4o", + "temperature": 0.7, + "max_tokens": 2048, + "memory_enabled": true + }', + + is_public BOOLEAN DEFAULT false, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_characters_user_id ON characters(user_id); +CREATE INDEX idx_characters_name ON characters(name); +CREATE INDEX idx_characters_attributes ON characters USING GIN(attributes); +``` + +### 3. conversations + +Chat sessions between user and character. + +```sql +CREATE TABLE conversations ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + character_id UUID NOT NULL REFERENCES characters(id) ON DELETE CASCADE, + title VARCHAR(255), -- Auto-generated or user-defined + + -- Context window management + message_count INTEGER DEFAULT 0, + total_tokens INTEGER DEFAULT 0, + + -- Conversation settings + settings JSONB DEFAULT '{}', + + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_conversations_user_id ON conversations(user_id); +CREATE INDEX idx_conversations_character_id ON conversations(character_id); +CREATE INDEX idx_conversations_created_at ON conversations(created_at); +``` + +### 4. messages + +Individual chat messages. + +```sql +CREATE TYPE message_role AS ENUM ('user', 'assistant', 'system'); + +CREATE TABLE messages ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, + role message_role NOT NULL, + content TEXT NOT NULL, + + -- LLM metadata + tokens_used INTEGER, + model VARCHAR(100), + + -- Additional metadata (temperature, latency, etc.) + metadata JSONB DEFAULT '{}', + + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_messages_conversation_id ON messages(conversation_id); +CREATE INDEX idx_messages_created_at ON messages(created_at); +CREATE INDEX idx_messages_conversation_created ON messages(conversation_id, created_at); +``` + +### 5. vector_memories + +Vector embeddings for conversation memory using pgvector. Stores chunked content for semantic search. + +```sql +-- Create vector extension first +CREATE EXTENSION IF NOT EXISTS vector; + +CREATE TABLE vector_memories ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, + + -- The text content + content TEXT NOT NULL, + + -- Vector embedding (configurable dimension based on model) + -- Common sizes: 384 (all-MiniLM-L6-v2), 768 (all-mpnet-base-v2), 1024 (BGE) + -- Must match the EMBEDDING_DIMENSION env var + embedding VECTOR({{EMBEDDING_DIMENSION}}), + + -- Metadata for filtering + metadata JSONB DEFAULT '{ + "chunk_index": 0, + "source": "conversation", + "timestamp": null + }', + + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- HNSW index for efficient similarity search +-- Note: Index is created after table creation based on actual dimension +-- CREATE INDEX idx_vector_memories_embedding ON vector_memories +-- USING hnsw (embedding vector_cosine_ops); + +CREATE INDEX idx_vector_memories_conversation ON vector_memories(conversation_id); +``` + +### 6. import_documents + +Raw imported documents from files or web scraping. + +```sql +CREATE TYPE import_source_type AS ENUM ('file', 'url'); +CREATE TYPE import_status AS ENUM ('pending', 'processing', 'completed', 'failed'); + +CREATE TABLE import_documents ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + + source_type import_source_type NOT NULL, + source_name VARCHAR(255) NOT NULL, -- filename or URL + + -- Mime type for files + mime_type VARCHAR(100), + + -- File size in bytes + file_size BIGINT, + + -- Raw content (preprocessed) + content TEXT, + + -- Processing status + status import_status DEFAULT 'pending', + error_message TEXT, + + -- Metadata (source info, extraction method, etc.) + metadata JSONB DEFAULT '{}', + + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_import_documents_user_id ON import_documents(user_id); +CREATE INDEX idx_import_documents_status ON import_documents(status); +``` + +### 7. story_branches (Phase 2) + +Tree structure for branching narratives. + +```sql +CREATE TABLE story_branches ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, + + -- Self-referential for tree structure + parent_id UUID REFERENCES story_branches(id) ON DELETE CASCADE, + + -- Branch content + title VARCHAR(255), + content TEXT NOT NULL, -- The generated story content + + -- User direction that led to this branch + user_direction TEXT, + + -- Branch metadata + generation_params JSONB DEFAULT '{}', + + -- Tree position + depth INTEGER DEFAULT 0, + branch_order INTEGER DEFAULT 0, + + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_story_branches_conversation ON story_branches(conversation_id); +CREATE INDEX idx_story_branches_parent ON story_branches(parent_id); +``` + +### 8. conversation_participants (Phase 3 - Multi-Character) + +Supports multiple characters in a single conversation. + +```sql +CREATE TABLE conversation_participants ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, + character_id UUID NOT NULL REFERENCES characters(id) ON DELETE CASCADE, + + -- Participant settings + is_active BOOLEAN DEFAULT true, + auto_respond BOOLEAN DEFAULT true, -- Auto-generate responses + + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + + UNIQUE(conversation_id, character_id) +); + +CREATE INDEX idx_participants_conversation ON conversation_participants(conversation_id); +``` + +## Prisma Schema (Reference) + +### Full Schema Definition + +```prisma +// schema.prisma + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// Enums +enum UserRole { + USER + ADMIN +} + +enum MessageRole { + user + assistant + system +} + +enum ImportSourceType { + file + url +} + +enum ImportStatus { + pending + processing + completed + failed +} + +// Models +model User { + id String @id @default(uuid()) + email String @unique + username String @unique + passwordHash String? + keycloakSub String? @unique + role UserRole @default(USER) + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + characters Character[] + conversations Conversation[] + importDocs ImportDocument[] + + @@index([email]) + @@index([keycloakSub]) +} + +model Character { + id String @id @default(uuid()) + name String + avatarUrl String? + personalityPrompt String + backstory String? + attributes Json @default("{}") + config Json @default("{}") + isPublic Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + conversations Conversation[] + + @@index([userId]) + @@index([name]) +} + +model Conversation { + id String @id @default(uuid()) + title String? + messageCount Int @default(0) + totalTokens Int @default(0) + settings Json @default("{}") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + characterId String + character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) + messages Message[] + vectorMemories VectorMemory[] + storyBranches StoryBranch[] + participants ConversationParticipant[] + + @@index([userId]) + @@index([characterId]) + @@index([createdAt]) +} + +model Message { + id String @id @default(uuid()) + role MessageRole + content String + tokensUsed Int? + model String? + metadata Json? + createdAt DateTime @default(now()) + + conversationId String + conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) + + @@index([conversationId]) + @@index([createdAt]) + @@index([conversationId, createdAt]) +} + +model VectorMemory { + id String @id @default(uuid()) + content String + embedding Unsupported("vector")? // pgvector extension + metadata Json? + createdAt DateTime @default(now()) + + conversationId String + conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) + + @@index([conversationId]) +} + +model ImportDocument { + id String @id @default(uuid()) + sourceType ImportSourceType + sourceName String + mimeType String? + fileSize BigInt? + content String? + status ImportStatus @default(pending) + errorMessage String? + metadata Json? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([status]) +} + +model StoryBranch { + id String @id @default(uuid()) + title String? + content String + userDirection String + generationParams Json? + depth Int @default(0) + branchOrder Int @default(0) + createdAt DateTime @default(now()) + + conversationId String + conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) + parentId String? + parent StoryBranch? @relation("BranchTree", fields: [parentId], references: [id], onDelete: Cascade) + children StoryBranch[] @relation("BranchTree") + + @@index([conversationId]) + @@index([parentId]) +} + +model ConversationParticipant { + id String @id @default(uuid()) + isActive Boolean @default(true) + autoRespond Boolean @default(true) + createdAt DateTime @default(now()) + + conversationId String + conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) + characterId String + + @@unique([conversationId, characterId]) + @@index([conversationId]) +} +``` + +### Prisma Client Usage Examples + +```typescript +// src/shared/prisma/prisma.service.ts +import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { + async onModuleInit() { + await this.$connect(); + } + + async onModuleDestroy() { + await this.$disconnect(); + } +} +``` + +```typescript +// Repository pattern with Prisma +@Injectable() +export class CharacterRepository { + constructor(private prisma: PrismaService) {} + + async findByUser(userId: string) { + return this.prisma.character.findMany({ + where: { userId }, + orderBy: { updatedAt: 'desc' }, + }); + } + + async create(data: CreateCharacterDto, userId: string) { + return this.prisma.character.create({ + data: { ...data, userId }, + }); + } +} +``` + +### Vector Memory Query with Prisma + +```typescript +// Similarity search using pgvector with Prisma +async similaritySearch( + conversationId: string, + queryEmbedding: number[], + k: number = 5 +) { + // Using raw query for pgvector-specific operations + const results = await this.prisma.$queryRaw` + SELECT + id, + content, + metadata, + embedding <=> ${queryEmbedding}::vector as distance + FROM "VectorMemory" + WHERE "conversationId" = ${conversationId} + ORDER BY embedding <=> ${queryEmbedding}::vector + LIMIT ${k} + `; + + return results; +} + +// Alternative: using cosine similarity +async similaritySearchCosine( + conversationId: string, + queryEmbedding: number[], + k: number = 5 +) { + const results = await this.prisma.$queryRaw` + SELECT + id, + content, + metadata, + 1 - (embedding <=> ${queryEmbedding}::vector) as similarity + FROM "VectorMemory" + WHERE "conversationId" = ${conversationId} + ORDER BY similarity DESC + LIMIT ${k} + `; + + return results; +} +``` + +### Embedding Configuration + +```typescript +// Configuration for embedding providers +interface EmbeddingConfig { + provider: 'local' | 'huggingface-api'; + model: string; + dimension: number; + // For local provider + localModelPath?: string; + // For HuggingFace API + apiKey?: string; + apiEndpoint?: string; +} + +// Example configurations: +// Local: { provider: 'local', model: 'Xenova/all-MiniLM-L6-v2', dimension: 384 } +// HF API: { provider: 'huggingface-api', model: 'sentence-transformers/all-mpnet-base-v2', dimension: 768 } +``` + +## Prisma Migration Strategy + +### Initial Migration + +```bash +# 1. Initialize Prisma +npx prisma init + +# 2. Define schema in prisma/schema.prisma + +# 3. Create first migration +npx prisma migrate dev --name init + +# 4. Generate Prisma Client +npx prisma generate +``` + +### Migration Workflow + +```bash +# After schema changes +npx prisma migrate dev --name descriptive_name + +# Production deployment +npx prisma migrate deploy + +# Generate client (in CI/CD) +npx prisma generate +``` + +### Migration File Example + +```sql +-- migrations/20240223120000_init/migration.sql + +-- Enable extensions +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS "vector"; + +-- CreateEnum +CREATE TYPE "UserRole" AS ENUM ('USER', 'ADMIN'); + +-- CreateEnum +CREATE TYPE "MessageRole" AS ENUM ('user', 'assistant', 'system'); + +-- CreateTable +CREATE TABLE "User" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "email" TEXT NOT NULL, + "username" TEXT NOT NULL, + -- ... etc +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- Vector index (HNSW) - created manually after migration +CREATE INDEX idx_vector_memories_embedding ON "VectorMemory" + USING hnsw (embedding vector_cosine_ops); +``` + +### Seeding + +```typescript +// prisma/seed.ts +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function main() { + // Seed default admin or test data + await prisma.user.create({ + data: { + email: 'admin@dreamchat.local', + username: 'admin', + role: 'ADMIN', + }, + }); +} + +main() + .catch(console.error) + .finally(() => prisma.$disconnect()); +``` + +```bash +# Run seed +npx prisma db seed +``` + +## Backup Strategy + +```bash +# pg_dump with custom format +docker exec dreamchat-postgres pg_dump -U postgres -Fc dreamchat > backup.dump + +# Restore +pg_restore -U postgres -d dreamchat backup.dump +``` diff --git a/doc/deployment.md b/doc/deployment.md new file mode 100644 index 0000000..1f14a30 --- /dev/null +++ b/doc/deployment.md @@ -0,0 +1,725 @@ +# DreamChat Deployment Guide + +## Overview + +This document covers the deployment configuration including Docker Compose setup, DevContainer configuration, and production deployment procedures. + +## DevContainer Setup + +### .devcontainer/devcontainer.json + +```json +{ + "name": "DreamChat Development", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspace", + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "20" + }, + "ghcr.io/devcontainers/features/docker-in-docker:2": {} + }, + "customizations": { + "vscode": { + "extensions": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "bradlc.vscode-tailwindcss", + "ms-vscode.vscode-typescript-next", + "nestjs.vscode-nestjs", + "prisma.prisma" + ], + "settings": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "typescript.preferences.importModuleSpecifier": "relative" + } + } + }, + "forwardPorts": [3000, 5173, 5432, 8080], + "portsAttributes": { + "3000": { + "label": "Backend API", + "onAutoForward": "notify" + }, + "5173": { + "label": "Frontend Dev Server", + "onAutoForward": "notify" + }, + "5432": { + "label": "PostgreSQL", + "onAutoForward": "silent" + }, + "8080": { + "label": "Keycloak", + "onAutoForward": "notify" + } + }, + "postCreateCommand": "bash .devcontainer/post-create.sh", + "remoteUser": "node", + "mounts": [ + "source=${localWorkspaceFolderBasename}-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume", + "source=${localWorkspaceFolderBasename}-pnpm-store,target=/home/node/.local/share/pnpm/store,type=volume" + ] +} +``` + +### .devcontainer/docker-compose.yml + +```yaml +version: '3.8' + +services: + app: + build: + context: .. + dockerfile: .devcontainer/Dockerfile + volumes: + - ..:/workspace:cached + - /var/run/docker.sock:/var/run/docker.sock + command: sleep infinity + environment: + - DATABASE_URL=postgresql://postgres:postgres@db:5432/dreamchat + - REDIS_URL=redis://redis:6379 + - KEYCLOAK_URL=http://keycloak:8080 + depends_on: + - db + - redis + - keycloak + networks: + - dreamchat-network + + db: + image: ankane/pgvector:latest + restart: unless-stopped + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: dreamchat + volumes: + - postgres-data:/var/lib/postgresql/data + ports: + - "5432:5432" + networks: + - dreamchat-network + + redis: + image: redis:7-alpine + restart: unless-stopped + ports: + - "6379:6379" + networks: + - dreamchat-network + + keycloak: + image: quay.io/keycloak/keycloak:23.0 + restart: unless-stopped + command: start-dev + environment: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://db:5432/keycloak + KC_DB_USERNAME: postgres + KC_DB_PASSWORD: postgres + ports: + - "8080:8080" + depends_on: + - db + networks: + - dreamchat-network + +volumes: + postgres-data: + +networks: + dreamchat-network: + driver: bridge +``` + +### .devcontainer/Dockerfile + +```dockerfile +FROM mcr.microsoft.com/devcontainers/typescript-node:20 + +# Install additional tools +RUN apt-get update && apt-get install -y \ + postgresql-client \ + redis-tools \ + && rm -rf /var/lib/apt/lists/* + +# Install pnpm +RUN npm install -g pnpm@8 + +# Set working directory +WORKDIR /workspace + +# Install global packages +RUN npm install -g @nestjs/cli@latest + +# Create non-root user +USER node +``` + +### .devcontainer/post-create.sh + +```bash +#!/bin/bash +set -e + +echo "🚀 Setting up DreamChat monorepo development environment..." + +# Install pnpm globally +npm install -g pnpm@8 + +# Install all dependencies (uses pnpm workspaces) +echo "📦 Installing dependencies..." +cd /workspace +pnpm install + +# Build shared packages first +echo "📦 Building shared packages..." +pnpm --filter @dreamchat/shared build + +# Generate Prisma client +pnpm db:generate + +# Copy environment files if they don't exist +if [ ! -f /workspace/apps/backend/.env ]; then + echo "⚙️ Creating backend .env file..." + cat > /workspace/apps/backend/.env << EOF +NODE_ENV=development +PORT=3000 +DATABASE_URL=postgresql://postgres:postgres@db:5432/dreamchat +JWT_SECRET=dev-jwt-secret-change-in-production +JWT_EXPIRES_IN=1h +JWT_REFRESH_EXPIRES_IN=7d +LLM_PROVIDER=openrouter +LLM_API_KEY=your-openrouter-api-key +LLM_MODEL=openai/gpt-4o +KEYCLOAK_URL=http://localhost:8080 +KEYCLOAK_REALM=dreamchat +KEYCLOAK_CLIENT_ID=dreamchat-backend +EOF +fi + +if [ ! -f /workspace/apps/frontend/.env ]; then + echo "⚙️ Creating frontend .env file..." + cat > /workspace/apps/frontend/.env << EOF +VITE_API_URL=http://localhost:3000/api +VITE_WS_URL=ws://localhost:3000 +VITE_KEYCLOAK_URL=http://localhost:8080 +VITE_KEYCLOAK_REALM=dreamchat +VITE_KEYCLOAK_CLIENT_ID=dreamchat-frontend +EOF +fi + +echo "✅ Development environment setup complete!" +echo "" +echo "Next steps:" +echo "1. Start all apps: pnpm dev" +echo "2. Or start individually:" +echo " - Backend: pnpm --filter @dreamchat/backend dev" +echo " - Frontend: pnpm --filter @dreamchat/frontend dev" +echo "3. Access Keycloak admin at http://localhost:8080 (admin/admin)" +``` + +## Docker Compose Production + +### docker-compose.yml (Root) + +```yaml +version: '3.8' + +services: + # Backend API + backend: + build: + context: . + dockerfile: apps/backend/Dockerfile + restart: unless-stopped + environment: + - NODE_ENV=production + - PORT=3000 + - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/dreamchat + - JWT_SECRET=${JWT_SECRET} + - JWT_EXPIRES_IN=${JWT_EXPIRES_IN:-1h} + - JWT_REFRESH_EXPIRES_IN=${JWT_REFRESH_EXPIRES_IN:-7d} + - LLM_PROVIDER=${LLM_PROVIDER} + - LLM_API_KEY=${LLM_API_KEY} + - LLM_MODEL=${LLM_MODEL} + - EMBEDDING_PROVIDER=${EMBEDDING_PROVIDER:-local} + - EMBEDDING_MODEL=${EMBEDDING_MODEL:-Xenova/all-MiniLM-L6-v2} + - EMBEDDING_DIMENSION=${EMBEDDING_DIMENSION:-384} + - EMBEDDING_DEVICE=${EMBEDDING_DEVICE:-cpu} + - HUGGINGFACE_API_KEY=${HUGGINGFACE_API_KEY} + - KEYCLOAK_URL=${KEYCLOAK_URL} + - KEYCLOAK_REALM=${KEYCLOAK_REALM} + - KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID} + - KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET} + ports: + - "3000:3000" + depends_on: + db: + condition: service_healthy + volumes: + - backend-logs:/app/logs + - model-cache:/app/models + networks: + - dreamchat-network + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + + # Frontend + frontend: + build: + context: . + dockerfile: apps/frontend/Dockerfile + restart: unless-stopped + ports: + - "80:80" + - "443:443" + environment: + - VITE_API_URL=/api + - VITE_WS_URL=/ws + depends_on: + - backend + networks: + - dreamchat-network + + # Database + db: + image: ankane/pgvector:latest + restart: unless-stopped + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: dreamchat + volumes: + - postgres-data:/var/lib/postgresql/data + - ./init-scripts:/docker-entrypoint-initdb.d + networks: + - dreamchat-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + + # Redis (optional, for session storage and caching) + redis: + image: redis:7-alpine + restart: unless-stopped + volumes: + - redis-data:/data + networks: + - dreamchat-network + + # Nginx Reverse Proxy (optional) + nginx: + image: nginx:alpine + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/ssl:/etc/nginx/ssl:ro + - model-cache:/model-cache:ro + depends_on: + - backend + - frontend + networks: + - dreamchat-network + +volumes: + postgres-data: + redis-data: + backend-logs: + model-cache: # Persist downloaded embedding models + +networks: + dreamchat-network: + driver: bridge +``` + +### Embedding Model Cache (Optional) + +To avoid re-downloading models on every restart, models are cached in a Docker volume: + +```yaml +# Add to docker-compose volumes +volumes: + model-cache: +``` + +Models are downloaded on first use and cached at `/app/models` in the backend container. + +Common models and their sizes: +- `Xenova/all-MiniLM-L6-v2`: ~80MB (384 dimensions) +- `Xenova/all-mpnet-base-v2`: ~420MB (768 dimensions) +- `BAAI/bge-small-en`: ~130MB (384 dimensions) + +### .env.example + +```bash +# Database +POSTGRES_PASSWORD=your_secure_password_here + +# JWT +JWT_SECRET=your_jwt_secret_key_min_32_chars +JWT_EXPIRES_IN=1h +JWT_REFRESH_EXPIRES_IN=7d + +# LLM Configuration +LLM_PROVIDER=openrouter +LLM_API_KEY=sk-or-v1-... +LLM_MODEL=openai/gpt-4o + +# Embedding Configuration (Local HuggingFace by default) +EMBEDDING_PROVIDER=local +EMBEDDING_MODEL=Xenova/all-MiniLM-L6-v2 +EMBEDDING_DIMENSION=384 +EMBEDDING_DEVICE=cpu + +# HuggingFace API (optional - if not using local embeddings) +# HUGGINGFACE_API_KEY=hf_... + +# Keycloak (optional - for external auth) +KEYCLOAK_URL=http://keycloak:8080 +KEYCLOAK_REALM=dreamchat +KEYCLOAK_CLIENT_ID=dreamchat-backend +KEYCLOAK_CLIENT_SECRET=your_keycloak_secret +``` + +## Backend Dockerfile + +```dockerfile +# apps/backend/Dockerfile +FROM node:20-alpine AS base +RUN npm install -g pnpm@8 + +FROM base AS dependencies +WORKDIR /app + +# Copy workspace configuration +COPY pnpm-workspace.yaml package.json ./ +COPY apps/backend/package.json ./apps/backend/ +COPY packages/shared/package.json ./packages/shared/ + +# Install dependencies +RUN pnpm install --frozen-lockfile + +FROM base AS build +WORKDIR /app +COPY --from=dependencies /app/node_modules ./node_modules +COPY --from=dependencies /app/apps/backend/node_modules ./apps/backend/node_modules +COPY --from=dependencies /app/packages/shared/node_modules ./packages/shared/node_modules + +# Copy source code +COPY packages/shared ./packages/shared +COPY apps/backend ./apps/backend + +# Build shared packages first +RUN pnpm --filter @dreamchat/shared build + +# Build backend +RUN pnpm --filter @dreamchat/backend build + +FROM base AS production +WORKDIR /app + +# Copy only production dependencies +COPY --from=dependencies /app/node_modules ./node_modules +COPY --from=dependencies /app/apps/backend/node_modules ./apps/backend/node_modules +COPY --from=build /app/apps/backend/dist ./dist +COPY --from=build /app/packages/shared/dist ./node_modules/@dreamchat/shared/dist + +# Create logs directory +RUN mkdir -p /app/logs + +# Non-root user +RUN addgroup -g 1001 -S nodejs +RUN adduser -S nodejs -u 1001 +USER nodejs + +EXPOSE 3000 + +CMD ["node", "dist/main.js"] +``` + +## Frontend Dockerfile + +```dockerfile +# apps/frontend/Dockerfile +FROM node:20-alpine AS base +RUN npm install -g pnpm@8 + +FROM base AS dependencies +WORKDIR /app + +# Copy workspace configuration +COPY pnpm-workspace.yaml package.json ./ +COPY apps/frontend/package.json ./apps/frontend/ +COPY packages/shared/package.json ./packages/shared/ + +# Install dependencies +RUN pnpm install --frozen-lockfile + +FROM base AS build +WORKDIR /app +COPY --from=dependencies /app/node_modules ./node_modules +COPY --from=dependencies /app/apps/frontend/node_modules ./apps/frontend/node_modules +COPY --from=dependencies /app/packages/shared/node_modules ./packages/shared/node_modules + +# Copy source code +COPY packages/shared ./packages/shared +COPY apps/frontend ./apps/frontend + +# Build shared packages first +RUN pnpm --filter @dreamchat/shared build + +# Build frontend +RUN pnpm --filter @dreamchat/frontend build + +# Production with Nginx +FROM nginx:alpine + +# Copy built assets +COPY --from=build /app/apps/frontend/dist /usr/share/nginx/html + +# Copy nginx config +COPY apps/frontend/nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] +``` + +### frontend/nginx.conf + +```nginx +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml; + + # API proxy + location /api { + proxy_pass http://backend:3000/api; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + + # WebSocket proxy + location /ws { + proxy_pass http://backend:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Static files + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "public, max-age=31536000, immutable"; + } + + # Health check + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } +} +``` + +## Keycloak Configuration + +### Initial Setup + +1. Access Keycloak admin console: `http://localhost:8080/admin` +2. Login with admin credentials +3. Create new realm: `dreamchat` +4. Create client: `dreamchat-backend` + - Client authentication: ON + - Authorization: ON + - Valid redirect URIs: `http://localhost:3000/*` + - Web origins: `http://localhost:3000` + +5. Create client: `dreamchat-frontend` + - Client authentication: OFF + - Valid redirect URIs: `http://localhost:5173/*` + - Web origins: `http://localhost:5173` + +### realm-export.json (Optional) + +```json +{ + "realm": "dreamchat", + "enabled": true, + "clients": [ + { + "clientId": "dreamchat-backend", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": ["http://localhost:3000/*"], + "webOrigins": ["http://localhost:3000"], + "protocol": "openid-connect" + }, + { + "clientId": "dreamchat-frontend", + "enabled": true, + "publicClient": true, + "redirectUris": ["http://localhost:5173/*"], + "webOrigins": ["http://localhost:5173"], + "protocol": "openid-connect" + } + ] +} +``` + +## Deployment Procedures + +### Local Development + +```bash +# Using DevContainer +1. Open project in VS Code +2. Click "Reopen in Container" +3. Wait for setup to complete +4. Start services: + - Terminal 1: pnpm --filter @dreamchat/backend dev + - Terminal 2: pnpm --filter @dreamchat/frontend dev + - Or run all: pnpm dev +``` + +### Production Deployment + +```bash +# 1. Clone repository +git clone https://github.com/yourusername/dreamchat.git +cd dreamchat + +# 2. Create environment file +cp .env.example .env +# Edit .env with production values + +# 3. Build and start +docker-compose up -d --build + +# 4. Run database migrations +docker-compose exec backend pnpm db:migrate + +# 5. Check health +curl http://localhost:3000/health + +# 6. View logs +docker-compose logs -f backend +``` + +### Backup and Restore + +```bash +# Backup database +docker-compose exec db pg_dump -U postgres -Fc dreamchat > backup_$(date +%Y%m%d).dump + +# Restore database +docker-compose exec -T db pg_restore -U postgres -d dreamchat < backup_20240223.dump + +# Backup volumes +docker run --rm -v dreamchat_postgres-data:/data -v $(pwd):/backup alpine tar czf /backup/postgres-backup.tar.gz -C /data . +``` + +### Updates + +```bash +# Pull latest changes +git pull origin main + +# Rebuild and restart +docker-compose down +docker-compose up -d --build + +# Run migrations +docker-compose exec backend npx prisma migrate deploy +``` + +## Monitoring and Logging + +### Health Checks + +```bash +# Backend health +curl http://localhost:3000/health + +# Database health +docker-compose exec db pg_isready -U postgres + +# Full stack +docker-compose ps +``` + +### Log Management + +```bash +# View all logs +docker-compose logs + +# View specific service +docker-compose logs -f backend + +# View last 100 lines +docker-compose logs --tail=100 backend +``` + +## Security Considerations + +1. **Change default passwords** in production +2. **Use HTTPS** with valid SSL certificates +3. **Enable firewall** rules for required ports only +4. **Regular updates** of base images and dependencies +5. **Secrets management** - use Docker secrets or external vault +6. **Network isolation** - separate networks for different services + +## Troubleshooting + +### Common Issues + +1. **Database connection failed** + - Check DATABASE_URL environment variable + - Ensure db service is healthy: `docker-compose ps` + +2. **WebSocket not connecting** + - Verify VITE_WS_URL matches your domain + - Check Nginx proxy configuration + +3. **Keycloak authentication issues** + - Verify client configuration in Keycloak admin + - Check redirect URIs match exactly + +4. **File upload fails** + - Check file size limits in Nginx config + - Verify disk space in backend container diff --git a/doc/frontend-guide.md b/doc/frontend-guide.md new file mode 100644 index 0000000..c1ea8a7 --- /dev/null +++ b/doc/frontend-guide.md @@ -0,0 +1,635 @@ +# DreamChat Frontend Guide + +## Overview + +This document outlines the frontend architecture, component structure, and development guidelines for the DreamChat React application. + +## Tech Stack + +| Category | Technology | Purpose | +|----------|------------|---------| +| Framework | React 18+ | UI library | +| Build Tool | Vite | Fast development and building | +| Language | TypeScript | Type safety | +| Styling | Tailwind CSS | Utility-first CSS | +| State Management | Zustand | Global state | +| Server State | TanStack Query | API data caching | +| Routing | React Router v6 | Navigation | +| WebSocket | Socket.io-client | Real-time communication | +| Shared Types | `@dreamchat/shared` | Monorepo shared package | +| API Client | OpenAPI Generator | Auto-generated API client | +| Forms | React Hook Form + Zod | Form handling and validation | +| UI Components | Radix UI + shadcn/ui | Accessible primitives | +| Icons | Lucide React | Icon library | +| Testing | Vitest + React Testing Library | Unit/component tests | + +## Project Structure + +``` +apps/frontend/ +├── src/ +│ ├── api/ +│ │ ├── generated/ # Auto-generated from OpenAPI +│ │ │ ├── models/ # DTO types +│ │ │ └── apis/ # API client classes +│ │ └── client.ts # Axios/fetch configuration +│ │ +│ ├── websocket/ +│ │ ├── socket.ts # Socket.io client setup +│ │ └── hooks.ts # WebSocket hooks (uses @dreamchat/shared) +│ │ +│ ├── components/ +│ │ ├── ui/ # Reusable UI components +│ │ │ ├── button.tsx +│ │ │ ├── input.tsx +│ │ │ ├── card.tsx +│ │ │ ├── dialog.tsx +│ │ │ └── ... +│ │ ├── layout/ +│ │ │ ├── sidebar.tsx +│ │ │ ├── header.tsx +│ │ │ └── main-layout.tsx +│ │ ├── character/ +│ │ │ ├── character-card.tsx +│ │ │ ├── character-form.tsx +│ │ │ ├── attribute-editor.tsx +│ │ │ └── personality-editor.tsx +│ │ ├── chat/ +│ │ │ ├── chat-container.tsx +│ │ │ ├── message-list.tsx +│ │ │ ├── message-bubble.tsx +│ │ │ ├── chat-input.tsx +│ │ │ └── typing-indicator.tsx +│ │ └── import/ +│ │ ├── file-dropzone.tsx +│ │ ├── url-input.tsx +│ │ └── import-progress.tsx +│ │ +│ ├── pages/ +│ │ ├── login-page.tsx +│ │ ├── register-page.tsx +│ │ ├── character-list-page.tsx +│ │ ├── character-detail-page.tsx +│ │ ├── chat-page.tsx +│ │ ├── story-page.tsx +│ │ └── import-page.tsx +│ │ +│ ├── hooks/ +│ │ ├── use-auth.ts +│ │ ├── use-chat.ts +│ │ ├── use-characters.ts +│ │ └── use-import.ts +│ │ +│ ├── stores/ +│ │ ├── auth-store.ts +│ │ ├── chat-store.ts +│ │ └── ui-store.ts +│ │ +│ ├── lib/ +│ │ ├── utils.ts # Utility functions +│ │ └── constants.ts # App constants +│ │ +│ ├── types/ +│ │ └── index.ts # App-specific types +│ │ +│ ├── styles/ +│ │ └── globals.css +│ │ +│ ├── main.tsx +│ └── App.tsx +│ +├── public/ +│ └── assets/ +│ +├── index.html +├── vite.config.ts +├── tailwind.config.ts +├── tsconfig.json +└── package.json +``` + +## Component Architecture + +### UI Components (shadcn/ui pattern) + +Base components built on Radix UI primitives: + +```typescript +// components/ui/button.tsx +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: "bg-destructive text-destructive-foreground", + outline: "border border-input bg-background hover:bg-accent", + secondary: "bg-secondary text-secondary-foreground", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "underline-offset-4 hover:underline text-primary", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps {} + +const Button = React.forwardRef( + ({ className, variant, size, ...props }, ref) => { + return ( +