diff --git a/apps/backend/prisma/models/character.prisma b/apps/backend/prisma/models/character.prisma new file mode 100644 index 0000000..68adf30 --- /dev/null +++ b/apps/backend/prisma/models/character.prisma @@ -0,0 +1,56 @@ +// Character model and character knowledge + +enum ImportSourceType { + file + url + manual +} + +enum ImportStatus { + pending + processing + completed + failed +} + +model Character { + id String @id @default(uuid()) + name String + avatarUrl String? + personalityPrompt 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[] + 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]) +} diff --git a/apps/backend/prisma/models/conversation.prisma b/apps/backend/prisma/models/conversation.prisma new file mode 100644 index 0000000..1d63197 --- /dev/null +++ b/apps/backend/prisma/models/conversation.prisma @@ -0,0 +1,38 @@ +// Conversation and participant models + +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 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]) +} diff --git a/apps/backend/prisma/models/importDocument.prisma b/apps/backend/prisma/models/importDocument.prisma new file mode 100644 index 0000000..71f4670 --- /dev/null +++ b/apps/backend/prisma/models/importDocument.prisma @@ -0,0 +1,21 @@ +// General import documents (not linked to characters) + +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]) +} diff --git a/apps/backend/prisma/models/message.prisma b/apps/backend/prisma/models/message.prisma new file mode 100644 index 0000000..d6295c1 --- /dev/null +++ b/apps/backend/prisma/models/message.prisma @@ -0,0 +1,24 @@ +// Message model + +enum MessageRole { + user + assistant + system +} + +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]) +} diff --git a/apps/backend/prisma/models/storyBranch.prisma b/apps/backend/prisma/models/storyBranch.prisma new file mode 100644 index 0000000..fd152e5 --- /dev/null +++ b/apps/backend/prisma/models/storyBranch.prisma @@ -0,0 +1,21 @@ +// Story branching for narrative generation (Phase 2) + +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]) +} diff --git a/apps/backend/prisma/models/user.prisma b/apps/backend/prisma/models/user.prisma new file mode 100644 index 0000000..38ea73d --- /dev/null +++ b/apps/backend/prisma/models/user.prisma @@ -0,0 +1,25 @@ +// User model and related enums + +enum UserRole { + USER + ADMIN +} + +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]) +} diff --git a/apps/backend/prisma/models/vectorMemory.prisma b/apps/backend/prisma/models/vectorMemory.prisma new file mode 100644 index 0000000..094ad1e --- /dev/null +++ b/apps/backend/prisma/models/vectorMemory.prisma @@ -0,0 +1,29 @@ +// Vector memory for embeddings (conversation and character knowledge) + +enum MemoryType { + conversation + character +} + +model VectorMemory { + id String @id @default(uuid()) + content String + embedding Unsupported("vector")? + memoryType MemoryType @default(conversation) + metadata Json? + createdAt DateTime @default(now()) + + 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]) +} diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts new file mode 100644 index 0000000..ee5f2c9 --- /dev/null +++ b/apps/backend/src/app.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; + +@Module({ + imports: [], + controllers: [], + providers: [], +}) +export class AppModule {} diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts new file mode 100644 index 0000000..8bdaff5 --- /dev/null +++ b/apps/backend/src/main.ts @@ -0,0 +1,20 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + app.enableCors({ + origin: ['http://localhost:5173'], + credentials: true, + }); + + app.setGlobalPrefix('api'); + + const port = process.env.PORT || 3000; + await app.listen(port); + + console.log(`🚀 Backend running on: http://localhost:${port}/api`); +} + +bootstrap(); diff --git a/apps/frontend/index.html b/apps/frontend/index.html new file mode 100644 index 0000000..8356551 --- /dev/null +++ b/apps/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + DreamChat + + +
+ + + diff --git a/apps/frontend/postcss.config.js b/apps/frontend/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/apps/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/frontend/public/vite.svg b/apps/frontend/public/vite.svg new file mode 100644 index 0000000..e69de29 diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx new file mode 100644 index 0000000..f07479c --- /dev/null +++ b/apps/frontend/src/App.tsx @@ -0,0 +1,14 @@ +function App() { + return ( +
+
+

DreamChat

+

+ Character simulation and interactive storytelling platform +

+
+
+ ); +} + +export default App; diff --git a/apps/frontend/src/main.tsx b/apps/frontend/src/main.tsx new file mode 100644 index 0000000..1f860e3 --- /dev/null +++ b/apps/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './styles/globals.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/apps/frontend/src/styles/globals.css b/apps/frontend/src/styles/globals.css new file mode 100644 index 0000000..10c2d37 --- /dev/null +++ b/apps/frontend/src/styles/globals.css @@ -0,0 +1,59 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/apps/frontend/tailwind.config.ts b/apps/frontend/tailwind.config.ts new file mode 100644 index 0000000..5f3b447 --- /dev/null +++ b/apps/frontend/tailwind.config.ts @@ -0,0 +1,38 @@ +import type { Config } from 'tailwindcss'; + +export default { + darkMode: ['class'], + content: ['./src/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + }, + }, + }, + plugins: [], +} satisfies Config; diff --git a/packages/shared/src/api/dto.ts b/packages/shared/src/api/dto.ts new file mode 100644 index 0000000..c6b2cdb --- /dev/null +++ b/packages/shared/src/api/dto.ts @@ -0,0 +1,86 @@ +/** + * Shared API DTOs + * Used by backend for validation and frontend for type safety + */ + +// Character DTOs +export interface CreateCharacterDto { + name: string; + personalityPrompt: string; + backstory?: string; + attributes?: Record; + avatarUrl?: string; + config?: Record; +} + +export interface UpdateCharacterDto extends Partial {} + +export interface CharacterResponseDto { + id: string; + name: string; + avatarUrl?: string; + personalityPrompt: string; + backstory?: string; + attributes: Record; + config: Record; + isPublic: boolean; + createdAt: string; + updatedAt: string; +} + +// Conversation DTOs +export interface CreateConversationDto { + characterId: string; + title?: string; +} + +export interface ConversationResponseDto { + id: string; + title?: string; + characterId: string; + messageCount: number; + totalTokens: number; + createdAt: string; + updatedAt: string; +} + +// Message DTOs +export interface CreateMessageDto { + content: string; +} + +export interface MessageResponseDto { + id: string; + role: 'user' | 'assistant' | 'system'; + content: string; + tokensUsed?: number; + model?: string; + metadata?: Record; + createdAt: string; +} + +// Auth DTOs +export interface LoginDto { + username: string; + password: string; +} + +export interface RegisterDto { + email: string; + username: string; + password: string; +} + +export interface AuthResponseDto { + accessToken: string; + refreshToken: string; + expiresIn: number; + user: UserResponseDto; +} + +export interface UserResponseDto { + id: string; + email: string; + username: string; + role: 'USER' | 'ADMIN'; +} diff --git a/packages/shared/src/api/index.ts b/packages/shared/src/api/index.ts new file mode 100644 index 0000000..ab3e767 --- /dev/null +++ b/packages/shared/src/api/index.ts @@ -0,0 +1 @@ +export * from './dto.js'; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000..aaddc73 --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,2 @@ +export * from './websocket/index.js'; +export * from './api/index.js'; diff --git a/packages/shared/src/websocket/events.ts b/packages/shared/src/websocket/events.ts new file mode 100644 index 0000000..8d7f794 --- /dev/null +++ b/packages/shared/src/websocket/events.ts @@ -0,0 +1,84 @@ +/** + * WebSocket Event Types + * Shared between frontend and backend for type-safe communication + */ + +export enum WebSocketEventType { + // Client -> Server + JOIN_CONVERSATION = 'JOIN_CONVERSATION', + LEAVE_CONVERSATION = 'LEAVE_CONVERSATION', + SEND_MESSAGE = 'SEND_MESSAGE', + STOP_GENERATION = 'STOP_GENERATION', + + // Server -> Client + CONVERSATION_JOINED = 'CONVERSATION_JOINED', + MESSAGE_ACK = 'MESSAGE_ACK', + STREAM_CHUNK = 'STREAM_CHUNK', + STREAM_COMPLETE = 'STREAM_COMPLETE', + ERROR = 'ERROR', +} + +// Base message interface +export interface WebSocketMessage { + type: WebSocketEventType; + payload: T; + timestamp: string; + requestId?: string; +} + +// Client -> Server payloads +export interface JoinConversationPayload { + conversationId: string; +} + +export interface LeaveConversationPayload { + conversationId: string; +} + +export interface SendMessagePayload { + conversationId: string; + content: string; + streaming?: boolean; +} + +export interface StopGenerationPayload { + conversationId: string; +} + +// Server -> Client payloads +export interface ConversationJoinedPayload { + conversationId: string; + history: MessageHistoryItem[]; +} + +export interface MessageHistoryItem { + id: string; + role: 'user' | 'assistant' | 'system'; + content: string; + createdAt: string; +} + +export interface MessageAckPayload { + messageId: string; + status: 'received' | 'processing' | 'error'; +} + +export interface StreamChunkPayload { + conversationId: string; + chunk: string; + isComplete: boolean; +} + +export interface StreamCompletePayload { + conversationId: string; + messageId: string; + content: string; + tokensUsed?: number; + model?: string; +} + +export interface ErrorPayload { + code: string; + message: string; + details?: Record; +} diff --git a/packages/shared/src/websocket/index.ts b/packages/shared/src/websocket/index.ts new file mode 100644 index 0000000..4cc3078 --- /dev/null +++ b/packages/shared/src/websocket/index.ts @@ -0,0 +1 @@ +export * from './events.js';