chore: add pnpm workspace configuration for apps and packages

This commit is contained in:
GW_MC
2026-02-23 21:04:19 +08:00
parent ab02758382
commit 932f384f0d
31 changed files with 9081 additions and 203 deletions

20
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
FROM mcr.microsoft.com/devcontainers/typescript-node:24
# Install additional tools
RUN apt-get update && apt-get install -y \
postgresql-client \
redis-tools \
&& rm -rf /var/lib/apt/lists/*
# Set up pnpm environment
ENV PNPM_HOME=/home/node/.local/share/pnpm
ENV PATH=$PNPM_HOME:$PATH
USER node
# Set working directory
WORKDIR /workspace
# Install global packages
RUN pnpm install -g @nestjs/cli@latest

View File

@@ -0,0 +1,52 @@
{
"name": "DreamChat Development",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "24"
},
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"moby": false
}
},
"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],
"portsAttributes": {
"3000": {
"label": "Backend API",
"onAutoForward": "notify"
},
"5173": {
"label": "Frontend Dev Server",
"onAutoForward": "notify"
},
"5432": {
"label": "PostgreSQL",
"onAutoForward": "silent"
}
},
"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"
]
}

View File

@@ -0,0 +1,47 @@
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 is external - configure KEYCLOAK_URL in apps/backend/.env
depends_on:
- db
- redis
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
volumes:
postgres-data:
networks:
dreamchat-network:
driver: bridge

View File

@@ -0,0 +1,76 @@
#!/bin/bash
set -e
echo "🚀 Setting up DreamChat monorepo development environment..."
# 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 || echo "Shared package build skipped (may not exist yet)"
# Generate Prisma client
echo "🔧 Generating Prisma client..."
cd /workspace/apps/backend
pnpm db:generate || echo "Prisma generate skipped (may not be set up yet)"
cd -
# Copy environment files if they don't exist
if [ ! -f /workspace/apps/backend/.env ]; then
echo "⚙️ Creating backend .env file..."
mkdir -p /workspace/apps/backend
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 (external) - configure if using external Keycloak
KEYCLOAK_ENABLED=false
# KEYCLOAK_URL=http://your-keycloak-server:8080
# KEYCLOAK_REALM=dreamchat
# KEYCLOAK_CLIENT_ID=dreamchat-backend
# KEYCLOAK_CLIENT_SECRET=your_keycloak_secret
# Keycloak Authorization (optional)
# KEYCLOAK_REQUIRED_GROUP=dreamchat-users
# KEYCLOAK_REQUIRED_ROLE=dreamchat-access
# KEYCLOAK_REQUIRED_CLIENT_ROLE=user
# KEYCLOAK_REQUIRED_ATTRIBUTE=approved:true
# Keycloak Auto-Create Users
KEYCLOAK_AUTO_CREATE_USER=true
KEYCLOAK_DEFAULT_USER_ROLE=USER
EOF
fi
if [ ! -f /workspace/apps/frontend/.env ]; then
echo "⚙️ Creating frontend .env file..."
mkdir -p /workspace/apps/frontend
cat > /workspace/apps/frontend/.env << EOF
VITE_API_URL=http://localhost:3000/api
VITE_WS_URL=ws://localhost:3000
# Keycloak (external) - configure if using external Keycloak
# VITE_KEYCLOAK_URL=http://your-keycloak-server: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 ""
echo "Note: Keycloak is external. Configure KEYCLOAK_URL in apps/backend/.env if needed."

2
.dockerignore Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
.pnpm-store/

56
.env.example Normal file
View File

@@ -0,0 +1,56 @@
# 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 (External) Configuration
# Enable Keycloak authentication
KEYCLOAK_ENABLED=false
KEYCLOAK_URL=http://your-keycloak-server:8080
KEYCLOAK_REALM=dreamchat
KEYCLOAK_CLIENT_ID=dreamchat-backend
KEYCLOAK_CLIENT_SECRET=your_keycloak_secret
# Keycloak Authorization Settings
# Require specific group/role/attribute for access
# Set at least one of these to enforce authorization checks
# Required Keycloak group (e.g., "dreamchat-users")
KEYCLOAK_REQUIRED_GROUP=
# Required Keycloak realm role (e.g., "dreamchat-access")
KEYCLOAK_REQUIRED_ROLE=
# Required Keycloak client role (e.g., "user")
KEYCLOAK_REQUIRED_CLIENT_ROLE=
# Required Keycloak user attribute (format: "attribute_name:attribute_value")
# Examples:
# KEYCLOAK_REQUIRED_ATTRIBUTE=department:engineering
# KEYCLOAK_REQUIRED_ATTRIBUTE=approved:true
KEYCLOAK_REQUIRED_ATTRIBUTE=
# Auto-create users on first Keycloak login
# If true, users will be automatically created in the database
# If false, only existing users can log in via Keycloak
KEYCLOAK_AUTO_CREATE_USER=true
# Default role for auto-created Keycloak users
KEYCLOAK_DEFAULT_USER_ROLE=USER

51
.gitignore vendored Normal file
View File

@@ -0,0 +1,51 @@
# Dependencies
node_modules/
.pnp
.pnp.js
.pnpm-store/
# Build outputs
dist/
build/
*.tsbuildinfo
# Environment files
.env
.env.local
.env.*.local
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Testing
coverage/
.nyc_output/
# Prisma
prisma/migrations/*/migration_lock.toml
# Misc
.cache/
temp/
tmp/
*.tgz
# DevContainer
.devcontainer/.pnpm-store/

3
.npmrc Normal file
View File

@@ -0,0 +1,3 @@
shamefully-hoist=true
auto-install-peers=true
strict-peer-dependencies=false

193
README.md
View File

@@ -1,24 +1,191 @@
# DreamChat
## Overview
A character simulation and interactive storytelling platform with AI-powered conversations.
This project is designed to simulate characters, enable interactive conversations with them, and generate stories based on user-provided templates and backgrounds. The platform focuses on creating immersive, personalized fan-like experiences where users can interact with their favorite simulated personas.
## Features
## Core Features
- **Character Simulation**: Create custom characters with personalities, attributes, and backstories
- **Interactive Dialogue**: Real-time chat with AI characters using WebSocket streaming
- **Story Generation**: Branching narratives with tree-view visualization
- **Vector Memory**: Context-aware conversations using local embeddings
- **Data Import**: Import character data from files (TXT, PDF, MD) or web sources
- **Multi-Character Chat**: Group conversations with multiple characters (Phase 3)
- Character Simulation: Users can define and customize characters with attributes, personalities, and backstories.
- Interactive Dialogue: The main focus is enabling users to talk directly to their simulated characters, using chat data provided by the user.
- Story Generation: The system generates narratives based on user-defined templates, backgrounds, and character interactions.
- User Data Import: Users can import their own chat histories, blogs, or other text data to enrich character simulation and personalize interactions.
## Tech Stack
## Key Requirements
- **Backend**: NestJS + TypeScript + Prisma + PostgreSQL (pgvector)
- **Frontend**: React + Vite + TypeScript + Tailwind CSS
- **Package Manager**: pnpm workspaces (monorepo)
- **AI/LLM**: OpenRouter with flexible provider support
- **Embeddings**: Local HuggingFace models (@xenova/transformers)
- **Auth**: Password-based (with optional external Keycloak SSO supporting group/role/attribute authorization)
- **DevOps**: Docker + DevContainer (external reverse proxy expected)
User-Centric Design: Prioritize ease of use, intuitive character creation, and natural conversational flow.
## Quick Start
## Target Audience
### Using DevContainer (Recommended)
- Storytellers and roleplayers seeking immersive character-driven narratives.
1. Open project in VS Code
2. Install [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
3. Press `F1` → "Dev Containers: Reopen in Container"
4. Wait for setup to complete
5. Start development:
```bash
pnpm dev
```
## Goals
### Manual Setup
Deliver a platform where users can define, interact, and narrate with their characters.
Prerequisites:
- Node.js 20+
- pnpm 8+
- PostgreSQL 15+ with pgvector extension
- Redis (optional)
1. Install dependencies:
```bash
pnpm install
```
2. Build shared packages:
```bash
pnpm --filter @dreamchat/shared build
```
3. Setup database:
```bash
pnpm db:generate
pnpm db:migrate
pnpm db:seed
```
4. Create environment files:
```bash
cp .env.example apps/backend/.env
cp .env.example apps/frontend/.env
# Edit both files with your configuration
```
5. Start development:
```bash
pnpm dev
```
## Project Structure
```
dreamchat/
├── apps/
│ ├── backend/ # NestJS API
│ └── frontend/ # React + Vite SPA
├── packages/
│ └── shared/ # Shared types & WebSocket definitions
├── prisma/
│ ├── schema.prisma # Main schema (imports from models/)
│ ├── seed.ts # Seed data
│ └── models/ # Individual model files
│ ├── user.prisma
│ ├── character.prisma
│ └── ...
├── .devcontainer/ # DevContainer configuration
├── docker-compose.yml # Production Docker Compose
└── doc/ # Project documentation
```
## Documentation
See the `doc/` folder for comprehensive documentation:
- [architecture.md](doc/architecture.md) - System architecture and design
- [monorepo-guide.md](doc/monorepo-guide.md) - pnpm workspace setup
- [database-schema.md](doc/database-schema.md) - Database schema (Prisma)
- [api-spec.md](doc/api-spec.md) - REST API & WebSocket specifications
- [implementation-plan.md](doc/implementation-plan.md) - Phased roadmap
- [frontend-guide.md](doc/frontend-guide.md) - Frontend architecture
- [deployment.md](doc/deployment.md) - Deployment guide
## Development Commands
```bash
# Install dependencies
pnpm install
# Start all apps in development mode
pnpm dev
# Build all packages
pnpm build
# Run tests
pnpm test
# Database commands
pnpm db:generate # Generate Prisma client
pnpm db:migrate # Run migrations
pnpm db:studio # Open Prisma Studio
pnpm db:seed # Seed database
# Lint
pnpm lint
# Clean
pnpm clean
```
## Environment Variables
### Backend (`apps/backend/.env`)
```bash
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://postgres:postgres@db:5432/dreamchat
JWT_SECRET=your-secret-key
LLM_PROVIDER=openrouter
LLM_API_KEY=your-api-key
LLM_MODEL=openai/gpt-4o
EMBEDDING_PROVIDER=local
EMBEDDING_MODEL=Xenova/all-MiniLM-L6-v2
# Keycloak (optional)
KEYCLOAK_ENABLED=false
KEYCLOAK_URL=http://your-keycloak-server:8080
KEYCLOAK_REALM=dreamchat
KEYCLOAK_CLIENT_ID=dreamchat-backend
KEYCLOAK_CLIENT_SECRET=your-secret
# Keycloak Authorization (optional)
KEYCLOAK_REQUIRED_GROUP=dreamchat-users
KEYCLOAK_REQUIRED_ROLE=dreamchat-access
KEYCLOAK_AUTO_CREATE_USER=true
```
### Frontend (`apps/frontend/.env`)
```bash
VITE_API_URL=http://localhost:3000/api
VITE_WS_URL=ws://localhost:3000
```
## Deployment
See [deployment.md](doc/deployment.md) for production deployment instructions.
Quick deployment with Docker:
```bash
# Copy and edit environment file
cp .env.example .env
# Build and start services
docker-compose up -d --build
# Run migrations
docker-compose exec backend pnpm db:migrate
```
**Note:** An external reverse proxy (nginx, Traefik, etc.) is expected for SSL termination and routing. See [deployment.md](doc/deployment.md) for configuration examples.
## License
MIT

57
apps/backend/Dockerfile Normal file
View File

@@ -0,0 +1,57 @@
# apps/backend/Dockerfile
FROM node:24-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
COPY prisma ./prisma
# Build shared packages first
RUN pnpm --filter @dreamchat/shared build
# Generate Prisma client
RUN pnpm db:generate
# 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
COPY --from=build /app/node_modules/.pnpm/@prisma+client* ./node_modules/.pnpm/
COPY --from=build /app/node_modules/@prisma ./node_modules/@prisma
# 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"]

50
apps/backend/package.json Normal file
View File

@@ -0,0 +1,50 @@
{
"name": "@dreamchat/backend",
"version": "1.0.0",
"scripts": {
"build": "nest build",
"dev": "nest start --watch",
"start": "node dist/main",
"test": "jest",
"test:watch": "jest --watch",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
"db:migrate": "prisma migrate deploy",
"db:generate": "prisma generate",
"db:seed": "prisma db seed",
"clean": "rm -r dist"
},
"dependencies": {
"@dreamchat/shared": "workspace:*",
"@nestjs/common": "^11.1.14",
"@nestjs/core": "^11.1.14",
"@nestjs/platform-express": "^11.1.14",
"@nestjs/platform-socket.io": "^11.1.14",
"@nestjs/websockets": "^11.1.14",
"@prisma/client": "^7.4.1",
"@xenova/transformers": "^2.15.0",
"bcrypt": "^6.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"dotenv": "^17.3.1",
"jsonwebtoken": "^9.0.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"puppeteer": "^24.37.5",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.0",
"socket.io": "^4.7.0"
},
"devDependencies": {
"@nestjs/cli": "^11.0.16",
"@nestjs/testing": "^11.1.14",
"@types/bcrypt": "^6.0.0",
"@types/jsonwebtoken": "^9.0.0",
"@types/node": "^24.10.13",
"@types/passport-jwt": "^4.0.0",
"@types/passport-local": "^1.0.0",
"jest": "^30.2.0",
"prisma": "^7.4.1",
"typescript": "^5.3.0"
}
}

View File

@@ -0,0 +1,13 @@
import { defineConfig, env } from 'prisma/config';
import 'dotenv/config';
export default defineConfig({
schema: 'prisma/',
migrations: {
path: 'prisma/migrations',
seed: 'tsx prisma/seed.ts',
},
datasource: {
url: env('DATABASE_URL'),
},
});

View File

@@ -0,0 +1,14 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
previewFeatures = ["strictUndefinedChecks"]
}
datasource db {
provider = "postgresql"
}

View File

@@ -0,0 +1,55 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('🌱 Seeding database...');
// Create default admin user
const admin = await prisma.user.upsert({
where: { email: 'admin@dreamchat.local' },
update: {},
create: {
email: 'admin@dreamchat.local',
username: 'admin',
role: 'ADMIN',
passwordHash: '$2b$10$YourHashedPasswordHere', // Replace with actual hash
},
});
console.log(`✅ Created admin user: ${admin.email}`);
// Create a sample character
const character = await prisma.character.upsert({
where: {
id: '00000000-0000-0000-0000-000000000001'
},
update: {},
create: {
id: '00000000-0000-0000-0000-000000000001',
name: 'Alice',
personalityPrompt: 'You are Alice, a curious and adventurous explorer who loves discovering new things. You are friendly, witty, and always eager to help.',
backstory: 'Alice grew up in a small village at the edge of a vast forest. From a young age, she was fascinated by the unknown and would often venture into the woods to explore.',
attributes: {
traits: ['curious', 'brave', 'witty', 'friendly'],
age: 25,
species: 'human',
skills: ['navigation', 'survival', 'cartography'],
},
userId: admin.id,
},
});
console.log(`✅ Created sample character: ${character.name}`);
console.log('✅ Seeding complete!');
}
main()
.catch((e) => {
console.error('❌ Seeding failed:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}

55
apps/frontend/Dockerfile Normal file
View File

@@ -0,0 +1,55 @@
# apps/frontend/Dockerfile
FROM node:24-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 stage - using serve for static files
# External reverse proxy (nginx/traefik/etc.) expected
FROM node:24-alpine AS production
WORKDIR /app
# Install serve
RUN npm install -g serve
# Copy built assets
COPY --from=build /app/apps/frontend/dist ./dist
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
USER nodejs
EXPOSE 3000
# Serve static files
# Note: External reverse proxy should handle:
# - SSL/TLS termination
# - Path routing (/api -> backend, / -> frontend)
# - WebSocket proxying
CMD ["serve", "-s", "dist", "-l", "3000"]

View File

@@ -0,0 +1,33 @@
{
"name": "@dreamchat/frontend",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest",
"lint": "eslint . --ext ts,tsx",
"clean": "rm -rf dist"
},
"dependencies": {
"@dreamchat/shared": "workspace:*",
"@tanstack/react-query": "^5.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.0",
"socket.io-client": "^4.7.0",
"zustand": "^4.5.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.0",
"postcss": "^8.4.0",
"tailwindcss": "^3.4.0",
"typescript": "^5.3.0",
"vite": "^5.0.0",
"vitest": "^1.0.0"
}
}

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,30 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 5173,
proxy: {
// Development proxy - external reverse proxy used in production
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
},
'/ws': {
target: 'ws://localhost:3000',
ws: true,
},
},
},
build: {
outDir: 'dist',
sourcemap: true,
},
});

View File

@@ -101,7 +101,7 @@ DreamChat is a character simulation platform built with a modular, extensible ar
#### 1. Auth Module
```typescript
// Dual authentication strategy
- KeycloakStrategy (OAuth2/OIDC)
- KeycloakStrategy (OAuth2/OIDC with group/role/attribute authorization)
- LocalStrategy (Password-based)
- JWT Guard for stateless auth
- Roles: USER, ADMIN
@@ -110,6 +110,13 @@ DreamChat is a character simulation platform built with a modular, extensible ar
- id, email, username
- passwordHash, keycloakSub
- role, isActive
// Keycloak Authorization
- Validates group membership (KEYCLOAK_REQUIRED_GROUP)
- Checks realm roles (KEYCLOAK_REQUIRED_ROLE)
- Checks client roles (KEYCLOAK_REQUIRED_CLIENT_ROLE)
- Validates user attributes (KEYCLOAK_REQUIRED_ATTRIBUTE)
- Auto-creates users on first login (if KEYCLOAK_AUTO_CREATE_USER=true)
```
#### 2. Character Module
@@ -119,14 +126,23 @@ DreamChat is a character simulation platform built with a modular, extensible ar
- CharacterRepository (Prisma)
- DTOs: CreateCharacterDto, UpdateCharacterDto, CharacterResponseDto
Entities:
Prisma Models:
- Character
- id, name, avatar
- id, name, avatarUrl
- personalityPrompt: string
- attributes: JSON (complex attribute system)
- backstory: string
- knowledgeSources: CharacterKnowledge[]
- vectorMemories: VectorMemory[]
- createdBy: User
- createdAt, updatedAt
- CharacterKnowledge
- id, name, sourceType (file/url/manual)
- sourceName, mimeType, fileSize
- rawContent: string
- status (pending/processing/completed/failed)
- processingInfo: JSON
- vectorMemories: VectorMemory[]
- characterId: Character
```
#### 3. Chat Module (MVP)
@@ -169,21 +185,32 @@ WebSocket Events:
- HuggingFaceAPIProvider: Uses HuggingFace Inference API
- VectorStoreService (uses Prisma with pgvector extension)
- addDocument(conversationId, content, metadata)
- similaritySearch(conversationId, query, k=5)
- addDocument(targetId, content, metadata, memoryType)
- similaritySearch(targetId, query, k=5, memoryType)
- Supports both conversation and character memory
- Uses raw Prisma queries with pgvector operators
- MemoryManager
- buildContext(conversationId, currentMessage): string
- buildConversationContext(conversationId, currentMessage): string
- buildCharacterContext(characterId, query): string
- summarizeOldMessages(conversationId): Promise<void>
- retrieveRelevantMemories(conversationId, query): Document[]
- retrieveRelevantMemories(targetId, query, memoryType): Document[]
- CharacterKnowledgeService
- importKnowledge(characterId, file/url)
- chunkAndEmbed(knowledgeId, content)
- processKnowledgeSource(knowledgeId)
- searchCharacterKnowledge(characterId, query)
Prisma Model:
- VectorMemory
- id
- conversationId (relation)
- content: String
- embedding: Unsupported("vector") // pgvector type
- embedding: Unsupported("vector")
- memoryType: enum ('conversation' | 'character')
- conversationId?: Conversation
- characterId?: Character
- knowledgeId?: CharacterKnowledge
- metadata: Json?
- createdAt: DateTime
```
@@ -235,6 +262,23 @@ class DataPreprocessor {
chunk(text: string, maxChunkSize: number): string[];
extractEntities(text: string): Entity[];
}
// Character Knowledge Import Service
class CharacterKnowledgeService {
// Import file/URL as character knowledge
importKnowledge(characterId: string, file: File): Promise<CharacterKnowledge>;
importFromUrl(characterId: string, url: string): Promise<CharacterKnowledge>;
// Process and embed knowledge
processKnowledge(knowledgeId: string): Promise<void>;
chunkAndEmbed(knowledgeId: string, content: string): Promise<void>;
// Search character knowledge
searchKnowledge(characterId: string, query: string): Promise<VectorMemory[]>;
// Get knowledge context for LLM
buildKnowledgeContext(characterId: string, query: string): string;
}
```
### Frontend (React + Vite)
@@ -438,9 +482,17 @@ dreamchat/
│ └── typescript/
├── prisma/ # Database schema (shared)
│ ├── schema.prisma
│ ├── schema.prisma # Main schema with imports
│ ├── migrations/
── seed.ts
── seed.ts
│ └── models/ # Individual model files
│ ├── user.prisma
│ ├── character.prisma
│ ├── conversation.prisma
│ ├── message.prisma
│ ├── vectorMemory.prisma
│ ├── importDocument.prisma
│ └── storyBranch.prisma
├── docker-compose.yml
├── pnpm-workspace.yaml

View File

@@ -23,35 +23,41 @@ CREATE EXTENSION IF NOT EXISTS "pgvector";
│ 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 │
│ role │ │ attributes │ │ updated_at │
│ created_at │ │ created_at │ └────────┬────────┘
│ updated_at │ │ updated_at │ │
└─────────────────┘ └────────┬────────┘
┌────────┴────────┐
│character_knowledge│
├─────────────────┤ │
│ 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)◄───────────────┤
│ character_id │
│ name
│ source_type │
│ raw_content │
│ status │ │
└────────┬────────┘
───────────────── ┌────────┴────────┐ ┌────────┴────────┐
│import_documents │ vector_memories│ messages
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
│ id (PK) │ │ id (PK) │ │ id (PK)
user_id (FK) │ │ content │ │ conversation_id
│ source_type │ │ embedding │ │ role │
│ source_name │ │ memory_type │ │ content │
│ content │ │ conversation_id │ │ tokens_used │
│ status │ │ character_id │ │ model │
└─────────────────┘ │ knowledge_id │ │ metadata │
│ created_at │ │ created_at │
└─────────────────┘ └─────────────────┘
┌────────┴────────┐
│ story_branches │ (Phase 2)
├─────────────────┤
│ id (PK) │
│ conversation_id │ ┌─────────────────┐
│ content story_branches │ (Phase 2)
│ embedding │ ├─────────────────┤
│ metadata │ │ id (PK) │
│ created_at │ │ conversation_id │
└─────────────────┘ │ parent_id (FK) │
│ conversation_id │
│ parent_id (FK) │
│ content │
│ direction │
│ metadata │
@@ -89,7 +95,7 @@ CREATE INDEX idx_users_keycloak_sub ON users(keycloak_sub);
### 2. characters
Character definitions with complex attribute system (JSONB for flexibility).
Character definitions with complex attribute system. Character knowledge is stored separately in `character_knowledge` with embeddings.
```sql
CREATE TABLE characters (
@@ -101,9 +107,6 @@ CREATE TABLE characters (
-- 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 '{}',
@@ -181,19 +184,53 @@ 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
### 5. character_knowledge
Vector embeddings for conversation memory using pgvector. Stores chunked content for semantic search.
Multiple knowledge sources for characters. Each source is chunked and stored with embeddings in `vector_memories`.
```sql
-- Create vector extension first
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TYPE import_source_type AS ENUM ('file', 'url', 'manual');
CREATE TYPE import_status AS ENUM ('pending', 'processing', 'completed', 'failed');
CREATE TABLE character_knowledge (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
character_id UUID NOT NULL REFERENCES characters(id) ON DELETE CASCADE,
-- Knowledge source info
name VARCHAR(255) NOT NULL, -- Display name
source_type import_source_type NOT NULL,
source_name VARCHAR(255) NOT NULL, -- Original filename or URL
mime_type VARCHAR(100),
file_size BIGINT,
-- Raw content (before chunking)
raw_content TEXT,
-- Processing status
status import_status DEFAULT 'pending',
processing_info JSONB, -- chunks count, errors, etc.
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_character_knowledge_character ON character_knowledge(character_id);
CREATE INDEX idx_character_knowledge_status ON character_knowledge(status);
```
### 6. vector_memories
Unified vector embeddings storage for:
- **Character knowledge** - Background info, imported documents (linked to `character_knowledge`)
- **Conversation history** - Chat context (linked to `conversations`)
```sql
CREATE TYPE memory_type AS ENUM ('conversation', 'character');
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
-- The text chunk
content TEXT NOT NULL,
-- Vector embedding (configurable dimension based on model)
@@ -201,14 +238,21 @@ CREATE TABLE vector_memories (
-- Must match the EMBEDDING_DIMENSION env var
embedding VECTOR({{EMBEDDING_DIMENSION}}),
-- Metadata for filtering
metadata JSONB DEFAULT '{
"chunk_index": 0,
"source": "conversation",
"timestamp": null
}',
-- Memory type determines the context
memory_type memory_type DEFAULT 'conversation',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
-- Metadata (chunk_index, source_info, etc.)
metadata JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- Polymorphic relations (at least one must be set)
-- For conversation context
conversation_id UUID REFERENCES conversations(id) ON DELETE CASCADE,
-- For character knowledge
character_id UUID REFERENCES characters(id) ON DELETE CASCADE,
knowledge_id UUID REFERENCES character_knowledge(id) ON DELETE CASCADE
);
-- HNSW index for efficient similarity search
@@ -216,22 +260,22 @@ CREATE TABLE vector_memories (
-- 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);
CREATE INDEX idx_vector_memories_conversation ON vector_memories(conversation_id) WHERE conversation_id IS NOT NULL;
CREATE INDEX idx_vector_memories_character ON vector_memories(character_id) WHERE character_id IS NOT NULL;
CREATE INDEX idx_vector_memories_knowledge ON vector_memories(knowledge_id) WHERE knowledge_id IS NOT NULL;
CREATE INDEX idx_vector_memories_type ON vector_memories(memory_type);
```
### 6. import_documents
### 7. import_documents
Raw imported documents from files or web scraping.
General-purpose imported documents (not linked to characters). For character knowledge, use `character_knowledge`.
```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_type import_source_type NOT NULL, -- file, url, manual
source_name VARCHAR(255) NOT NULL, -- filename or URL
-- Mime type for files
@@ -258,7 +302,7 @@ 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)
### 8. story_branches (Phase 2)
Tree structure for branching narratives.
@@ -291,7 +335,7 @@ 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)
### 9. conversation_participants (Phase 3 - Multi-Character)
Supports multiple characters in a single conversation.
@@ -313,12 +357,73 @@ CREATE TABLE conversation_participants (
CREATE INDEX idx_participants_conversation ON conversation_participants(conversation_id);
```
### Enums
```sql
-- User roles
CREATE TYPE user_role AS ENUM ('USER', 'ADMIN');
-- Message roles
CREATE TYPE message_role AS ENUM ('user', 'assistant', 'system');
-- Import/knowledge source types
CREATE TYPE import_source_type AS ENUM ('file', 'url', 'manual');
-- Processing status
CREATE TYPE import_status AS ENUM ('pending', 'processing', 'completed', 'failed');
-- Vector memory types
CREATE TYPE memory_type AS ENUM ('conversation', 'character');
```
## Prisma Schema (Reference)
Prisma schema uses the [multi-file schema](https://www.prisma.io/docs/orm/prisma-schema/overview/location) feature. Models are organized in `prisma/models/` folder and imported into `schema.prisma`.
### Schema Structure
```
prisma/
├── schema.prisma # Main schema file with imports
├── seed.ts # Database seeding
└── models/
├── user.prisma # User model + UserRole enum
├── character.prisma # Character + CharacterKnowledge models
├── conversation.prisma # Conversation + ConversationParticipant
├── message.prisma # Message model + MessageRole enum
├── vectorMemory.prisma # VectorMemory + MemoryType enum
├── importDocument.prisma # ImportDocument model
└── storyBranch.prisma # StoryBranch model
```
### Main Schema (schema.prisma)
```prisma
// schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// Import all models from the models folder
import { User } from "./models/user"
import { Character, CharacterKnowledge } from "./models/character"
import { Conversation, ConversationParticipant } from "./models/conversation"
import { Message } from "./models/message"
import { VectorMemory } from "./models/vectorMemory"
import { ImportDocument } from "./models/importDocument"
import { StoryBranch } from "./models/storyBranch"
```
### Full Schema Definition
```prisma
// schema.prisma
// models/user.prisma
generator client {
provider = "prisma-client-js"
@@ -344,6 +449,7 @@ enum MessageRole {
enum ImportSourceType {
file
url
manual
}
enum ImportStatus {
@@ -353,6 +459,11 @@ enum ImportStatus {
failed
}
enum MemoryType {
conversation
character
}
// Models
model User {
id String @id @default(uuid())
@@ -378,7 +489,6 @@ model Character {
name String
avatarUrl String?
personalityPrompt String
backstory String?
attributes Json @default("{}")
config Json @default("{}")
isPublic Boolean @default(false)
@@ -388,11 +498,34 @@ model Character {
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
conversations Conversation[]
knowledgeSources CharacterKnowledge[]
vectorMemories VectorMemory[]
@@index([userId])
@@index([name])
}
model CharacterKnowledge {
id String @id @default(uuid())
name String
sourceType ImportSourceType
sourceName String
mimeType String?
fileSize BigInt?
rawContent String?
status ImportStatus @default(pending)
processingInfo Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
characterId String
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
vectorMemories VectorMemory[]
@@index([characterId])
@@index([status])
}
model Conversation {
id String @id @default(uuid())
title String?
@@ -436,14 +569,24 @@ model Message {
model VectorMemory {
id String @id @default(uuid())
content String
embedding Unsupported("vector")? // pgvector extension
embedding Unsupported("vector")?
memoryType MemoryType @default(conversation)
metadata Json?
createdAt DateTime @default(now())
conversationId String
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
conversationId String?
conversation Conversation? @relation(fields: [conversationId], references: [id], onDelete: Cascade)
characterId String?
character Character? @relation(fields: [characterId], references: [id], onDelete: Cascade)
knowledgeId String?
knowledge CharacterKnowledge? @relation(fields: [knowledgeId], references: [id], onDelete: Cascade)
@@index([conversationId])
@@index([characterId])
@@index([knowledgeId])
@@index([memoryType])
}
model ImportDocument {
@@ -546,11 +689,16 @@ export class CharacterRepository {
```typescript
// Similarity search using pgvector with Prisma
async similaritySearch(
conversationId: string,
targetId: string,
queryEmbedding: number[],
memoryType: MemoryType,
k: number = 5
) {
// Using raw query for pgvector-specific operations
// Build the where clause based on memory type
const whereClause = memoryType === 'conversation'
? { conversationId: targetId, memoryType }
: { characterId: targetId, memoryType };
const results = await this.prisma.$queryRaw`
SELECT
id,
@@ -558,7 +706,7 @@ async similaritySearch(
metadata,
embedding <=> ${queryEmbedding}::vector as distance
FROM "VectorMemory"
WHERE "conversationId" = ${conversationId}
WHERE ${whereClause}
ORDER BY embedding <=> ${queryEmbedding}::vector
LIMIT ${k}
`;
@@ -566,20 +714,23 @@ async similaritySearch(
return results;
}
// Alternative: using cosine similarity
async similaritySearchCosine(
conversationId: string,
// Search character knowledge
async searchCharacterKnowledge(
characterId: 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}
vm.id,
vm.content,
vm.metadata,
ck.name as source_name,
1 - (vm.embedding <=> ${queryEmbedding}::vector) as similarity
FROM "VectorMemory" vm
JOIN "CharacterKnowledge" ck ON vm."knowledgeId" = ck.id
WHERE vm."characterId" = ${characterId}
AND vm."memoryType" = 'character'
ORDER BY similarity DESC
LIMIT ${k}
`;

View File

@@ -112,24 +112,6 @@ services:
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:
@@ -138,6 +120,8 @@ networks:
driver: bridge
```
**Note:** Keycloak is configured as an external service. Set `KEYCLOAK_URL` in your environment to point to your external Keycloak instance.
### .devcontainer/Dockerfile
```dockerfile
@@ -274,15 +258,15 @@ services:
timeout: 10s
retries: 3
# Frontend
# Frontend (static file server)
# Note: External reverse proxy expected for SSL and routing
frontend:
build:
context: .
dockerfile: apps/frontend/Dockerfile
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "3001:3000"
environment:
- VITE_API_URL=/api
- VITE_WS_URL=/ws
@@ -319,23 +303,6 @@ services:
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:
@@ -400,7 +367,7 @@ KEYCLOAK_CLIENT_SECRET=your_keycloak_secret
```dockerfile
# apps/backend/Dockerfile
FROM node:20-alpine AS base
FROM node:24-alpine AS base
RUN npm install -g pnpm@8
FROM base AS dependencies
@@ -456,7 +423,7 @@ CMD ["node", "dist/main.js"]
```dockerfile
# apps/frontend/Dockerfile
FROM node:20-alpine AS base
FROM node:24-alpine AS base
RUN npm install -g pnpm@8
FROM base AS dependencies
@@ -486,46 +453,59 @@ RUN pnpm --filter @dreamchat/shared build
# Build frontend
RUN pnpm --filter @dreamchat/frontend build
# Production with Nginx
FROM nginx:alpine
# Production stage - using serve for static files
# External reverse proxy (nginx/traefik/etc.) expected
FROM node:24-alpine AS production
WORKDIR /app
# Install serve
RUN npm install -g serve
# Copy built assets
COPY --from=build /app/apps/frontend/dist /usr/share/nginx/html
COPY --from=build /app/apps/frontend/dist ./dist
# Copy nginx config
COPY apps/frontend/nginx.conf /etc/nginx/conf.d/default.conf
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
USER nodejs
EXPOSE 80
EXPOSE 3000
CMD ["nginx", "-g", "daemon off;"]
# Serve static files
# Note: External reverse proxy should handle:
# - SSL/TLS termination
# - Path routing (/api -> backend, / -> frontend)
# - WebSocket proxying
CMD ["serve", "-s", "dist", "-l", "3000"]
```
### frontend/nginx.conf
### External Reverse Proxy Configuration
The frontend container serves static files on port 3000. An external reverse proxy is expected to handle:
- **SSL/TLS termination**
- **Path routing**:
- `/api/*` → Backend (port 3000)
- `/ws` → Backend WebSocket (port 3000)
- `/*` → Frontend (port 3001)
- **Static file caching**
Example nginx configuration:
```nginx
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
listen 443 ssl;
server_name dreamchat.example.com;
# 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;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# 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
@@ -534,45 +514,110 @@ server {
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
# Frontend 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;
proxy_pass http://frontend:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
}
```
## Keycloak Configuration
## Keycloak Configuration (External)
### Initial Setup
Keycloak is configured as an external service with support for group/role/attribute-based authorization and auto-user creation.
1. Access Keycloak admin console: `http://localhost:8080/admin`
2. Login with admin credentials
3. Create new realm: `dreamchat`
4. Create client: `dreamchat-backend`
### Prerequisites
1. Have a running Keycloak instance (self-hosted or managed)
2. Configure the following environment variables in `.env`:
```bash
# Basic Keycloak settings
KEYCLOAK_ENABLED=true
KEYCLOAK_URL=http://your-keycloak-server:8080
KEYCLOAK_REALM=dreamchat
KEYCLOAK_CLIENT_ID=dreamchat-backend
KEYCLOAK_CLIENT_SECRET=your_keycloak_secret
# Authorization settings (optional but recommended)
KEYCLOAK_REQUIRED_GROUP=dreamchat-users
KEYCLOAK_REQUIRED_ROLE=dreamchat-access
KEYCLOAK_REQUIRED_CLIENT_ROLE=user
KEYCLOAK_REQUIRED_ATTRIBUTE=approved:true
# Auto-create users
KEYCLOAK_AUTO_CREATE_USER=true
KEYCLOAK_DEFAULT_USER_ROLE=USER
```
### Keycloak Realm Setup
1. Access your Keycloak admin console
2. Create new realm: `dreamchat`
3. 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`
4. Create client: `dreamchat-frontend`
- Client authentication: OFF
- Valid redirect URIs: `http://localhost:5173/*`
- Web origins: `http://localhost:5173`
### Authorization Configuration
You can restrict access based on:
**1. Group Membership**
```bash
KEYCLOAK_REQUIRED_GROUP=dreamchat-users
```
Users must be members of this Keycloak group to access the application.
**2. Realm Role**
```bash
KEYCLOAK_REQUIRED_ROLE=dreamchat-access
```
Users must have this realm-level role.
**3. Client Role**
```bash
KEYCLOAK_REQUIRED_CLIENT_ROLE=user
```
Users must have this role for the `dreamchat-backend` client.
**4. User Attribute**
```bash
KEYCLOAK_REQUIRED_ATTRIBUTE=department:engineering
# or
KEYCLOAK_REQUIRED_ATTRIBUTE=approved:true
```
Users must have this attribute with the specified value.
### User Auto-Creation
When `KEYCLOAK_AUTO_CREATE_USER=true`:
- Users are automatically created in the database on first Keycloak login
- Username is derived from Keycloak preferred_username
- Email is taken from Keycloak email claim
- Role is set to `KEYCLOAK_DEFAULT_USER_ROLE` (default: USER)
- The `keycloakSub` field links the local user to Keycloak
When `KEYCLOAK_AUTO_CREATE_USER=false`:
- Only existing local users can log in via Keycloak
- The `keycloakSub` must match between Keycloak and local user
### Example Keycloak Group/Role Setup
1. Create a group: `dreamchat-users`
2. Create a realm role: `dreamchat-access`
3. Assign the group and/or role to users who should have access
4. Configure `KEYCLOAK_REQUIRED_GROUP` and/or `KEYCLOAK_REQUIRED_ROLE`
### realm-export.json (Optional)
```json

View File

@@ -29,7 +29,7 @@ This document outlines the phased implementation approach for DreamChat.
- [ ] Initialize NestJS app in `apps/backend`
- [ ] Configure Prisma ORM with PostgreSQL
- [ ] Install and configure pgvector extension
- [ ] Define Prisma schema for all entities
- [ ] Define Prisma schema using multi-file structure (`prisma/models/`)
- [ ] Configure Jest for unit testing
- [ ] Set up Swagger/OpenAPI
- [ ] Implement basic logging (Winston/Pino)
@@ -204,10 +204,15 @@ HUGGINGFACE_API_KEY=hf_...
#### Backend Tasks
- [ ] Import adapter interface
- [ ] Text file adapter
- [ ] Text file adapter (txt, md)
- [ ] File upload endpoint
- [ ] Data preprocessor (cleaning)
- [ ] Basic text chunking
- [ ] CharacterKnowledge Prisma model
- [ ] CharacterKnowledgeService
- [ ] Import file/URL as knowledge
- [ ] Chunk and embed content
- [ ] Store in VectorMemory with type='character'
#### Frontend Tasks
- [ ] Import page
@@ -274,7 +279,8 @@ HUGGINGFACE_API_KEY=hf_...
- [ ] Predefined scraper: AO3
- [ ] Predefined scraper: FanFiction.net
- [ ] URL validation and scraper selection
- [ ] URL import endpoint
- [ ] URL import endpoint (stores as CharacterKnowledge)
- [ ] PDF and Markdown support for character knowledge
#### Frontend Tasks
- [ ] URL import component

View File

@@ -616,7 +616,7 @@ services:
```dockerfile
# apps/backend/Dockerfile
FROM node:20-alpine AS base
FROM node:24-alpine AS base
RUN npm install -g pnpm
FROM base AS dependencies

107
docker-compose.yml Normal file
View File

@@ -0,0 +1,107 @@
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 Configuration
- KEYCLOAK_ENABLED=${KEYCLOAK_ENABLED:-false}
- KEYCLOAK_URL=${KEYCLOAK_URL:-}
- KEYCLOAK_REALM=${KEYCLOAK_REALM:-}
- KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-}
- KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-}
# Keycloak Authorization
- KEYCLOAK_REQUIRED_GROUP=${KEYCLOAK_REQUIRED_GROUP:-}
- KEYCLOAK_REQUIRED_ROLE=${KEYCLOAK_REQUIRED_ROLE:-}
- KEYCLOAK_REQUIRED_CLIENT_ROLE=${KEYCLOAK_REQUIRED_CLIENT_ROLE:-}
- KEYCLOAK_REQUIRED_ATTRIBUTE=${KEYCLOAK_REQUIRED_ATTRIBUTE:-}
# Keycloak Auto-Create
- KEYCLOAK_AUTO_CREATE_USER=${KEYCLOAK_AUTO_CREATE_USER:-true}
- KEYCLOAK_DEFAULT_USER_ROLE=${KEYCLOAK_DEFAULT_USER_ROLE:-USER}
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 (static files served via 'serve')
# Note: External reverse proxy expected for SSL and path routing
frontend:
build:
context: .
dockerfile: apps/frontend/Dockerfile
restart: unless-stopped
ports:
- "3001:3000"
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
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
volumes:
postgres-data:
redis-data:
backend-logs:
model-cache:
networks:
dreamchat-network:
driver: bridge

17
package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "dreamchat",
"version": "1.0.0",
"private": true,
"packageManager": "pnpm@8.15.0",
"scripts": {
"build": "pnpm -r build",
"dev": "pnpm -r --parallel dev",
"test": "pnpm -r test",
"lint": "pnpm -r lint",
"clean": "pnpm -r clean && rm -rf node_modules"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.3.0"
}
}

View File

@@ -0,0 +1,29 @@
{
"name": "@dreamchat/shared",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./websocket": {
"import": "./dist/websocket/index.js",
"types": "./dist/websocket/index.d.ts"
},
"./api": {
"import": "./dist/api/index.js",
"types": "./dist/api/index.d.ts"
}
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"clean": "rm -rf dist"
},
"devDependencies": {
"typescript": "^5.3.0"
}
}

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"declaration": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

7615
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

3
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,3 @@
packages:
- 'apps/*'
- 'packages/*'