diff --git a/LOVABLE_CLONE_CODE_TEMPLATES.md b/LOVABLE_CLONE_CODE_TEMPLATES.md new file mode 100644 index 00000000..0347795a --- /dev/null +++ b/LOVABLE_CLONE_CODE_TEMPLATES.md @@ -0,0 +1,1559 @@ +# 🎯 Lovable Clone - Complete Code Templates + +> Production-ready code templates để build Lovable Clone nhanh chΓ³ng + +--- + +## πŸ“ Project Structure + +``` +lovable-clone/ +β”œβ”€β”€ apps/ +β”‚ β”œβ”€β”€ web/ # Next.js Frontend +β”‚ └── api/ # Backend API +β”œβ”€β”€ packages/ +β”‚ β”œβ”€β”€ agent/ # AI Agent Logic +β”‚ β”œβ”€β”€ database/ # Database Schema +β”‚ β”œβ”€β”€ ui/ # Shared UI Components +β”‚ └── config/ # Shared Configs +└── templates/ # Project Templates +``` + +--- + +# πŸ€– I. AI AGENT LAYER + +## 1. Agent System Core + +**File: `packages/agent/src/index.ts`** + +```typescript +import { ChatOpenAI } from '@langchain/openai'; +import { ChatAnthropic } from '@langchain/anthropic'; +import { HumanMessage, SystemMessage, AIMessage } from '@langchain/core/messages'; + +export interface AgentConfig { + provider: 'openai' | 'anthropic'; + model: string; + temperature?: number; + maxTokens?: number; +} + +export interface AgentContext { + projectId: string; + fileTree: FileTree; + designSystem: DesignSystem; + conversationHistory: Message[]; + userPreferences?: UserPreferences; +} + +export class LovableAgent { + private llm: ChatOpenAI | ChatAnthropic; + private systemPrompt: string; + private tools: AgentTool[]; + + constructor(config: AgentConfig, systemPrompt: string) { + if (config.provider === 'openai') { + this.llm = new ChatOpenAI({ + modelName: config.model, + temperature: config.temperature ?? 0.7, + maxTokens: config.maxTokens ?? 4000, + openAIApiKey: process.env.OPENAI_API_KEY + }); + } else { + this.llm = new ChatAnthropic({ + modelName: config.model, + temperature: config.temperature ?? 0.7, + maxTokens: config.maxTokens ?? 4000, + anthropicApiKey: process.env.ANTHROPIC_API_KEY + }); + } + + this.systemPrompt = systemPrompt; + this.tools = []; + } + + registerTool(tool: AgentTool): void { + this.tools.push(tool); + } + + async chat( + message: string, + context: AgentContext, + options?: { + stream?: boolean; + onToken?: (token: string) => void; + } + ): Promise { + // Build messages + const messages = [ + new SystemMessage(this.buildSystemPrompt(context)), + ...this.buildConversationHistory(context.conversationHistory), + new HumanMessage(message) + ]; + + if (options?.stream) { + return await this.streamResponse(messages, context, options.onToken); + } else { + return await this.generateResponse(messages, context); + } + } + + private async generateResponse( + messages: any[], + context: AgentContext + ): Promise { + const response = await this.llm.invoke(messages); + + // Check if response contains tool calls + const toolCalls = this.parseToolCalls(response.content as string); + + if (toolCalls.length > 0) { + // Execute tools + const toolResults = await this.executeTools(toolCalls, context); + + return { + content: response.content as string, + toolCalls, + toolResults, + finishReason: 'tool_calls' + }; + } + + return { + content: response.content as string, + finishReason: 'stop' + }; + } + + private async streamResponse( + messages: any[], + context: AgentContext, + onToken?: (token: string) => void + ): Promise { + let fullContent = ''; + + const stream = await this.llm.stream(messages); + + for await (const chunk of stream) { + const token = chunk.content as string; + fullContent += token; + + if (onToken) { + onToken(token); + } + } + + // Check for tool calls in completed response + const toolCalls = this.parseToolCalls(fullContent); + + if (toolCalls.length > 0) { + const toolResults = await this.executeTools(toolCalls, context); + + return { + content: fullContent, + toolCalls, + toolResults, + finishReason: 'tool_calls' + }; + } + + return { + content: fullContent, + finishReason: 'stop' + }; + } + + private buildSystemPrompt(context: AgentContext): string { + return `${this.systemPrompt} + +## Current Context + +Project ID: ${context.projectId} + +File Tree: +\`\`\`json +${JSON.stringify(context.fileTree, null, 2)} +\`\`\` + +Design System: +\`\`\`json +${JSON.stringify(context.designSystem, null, 2)} +\`\`\` + +Available Tools: +${this.tools.map(t => `- ${t.name}: ${t.description}`).join('\n')} +`; + } + + private buildConversationHistory(history: Message[]): any[] { + return history.map(msg => { + if (msg.role === 'user') { + return new HumanMessage(msg.content); + } else if (msg.role === 'assistant') { + return new AIMessage(msg.content); + } else { + return new SystemMessage(msg.content); + } + }); + } + + private parseToolCalls(content: string): ToolCall[] { + // Parse XML-like tool call syntax + // Example: ... + const toolCallRegex = /]*)>([\s\S]*?)<\/tool_call>/g; + const toolCalls: ToolCall[] = []; + + let match; + while ((match = toolCallRegex.exec(content)) !== null) { + const [, name, paramsStr, callContent] = match; + + // Parse parameters + const params: Record = {}; + const paramRegex = /(\w+)="([^"]*)"/g; + let paramMatch; + while ((paramMatch = paramRegex.exec(paramsStr)) !== null) { + params[paramMatch[1]] = paramMatch[2]; + } + + // If there's content, add it as 'content' param + if (callContent.trim()) { + params.content = callContent.trim(); + } + + toolCalls.push({ name, parameters: params }); + } + + return toolCalls; + } + + private async executeTools( + toolCalls: ToolCall[], + context: AgentContext + ): Promise { + const results: ToolResult[] = []; + + for (const call of toolCalls) { + const tool = this.tools.find(t => t.name === call.name); + + if (!tool) { + results.push({ + toolName: call.name, + success: false, + error: `Tool '${call.name}' not found` + }); + continue; + } + + try { + const result = await tool.execute(call.parameters, context); + results.push({ + toolName: call.name, + success: true, + result + }); + } catch (error) { + results.push({ + toolName: call.name, + success: false, + error: (error as Error).message + }); + } + } + + return results; + } +} + +// Types +export interface AgentResponse { + content: string; + toolCalls?: ToolCall[]; + toolResults?: ToolResult[]; + finishReason: 'stop' | 'tool_calls' | 'length'; +} + +export interface ToolCall { + name: string; + parameters: Record; +} + +export interface ToolResult { + toolName: string; + success: boolean; + result?: any; + error?: string; +} + +export interface AgentTool { + name: string; + description: string; + parameters: Record; + execute: (params: Record, context: AgentContext) => Promise; +} +``` + +--- + +## 2. Agent Tools Implementation + +**File: `packages/agent/src/tools/index.ts`** + +```typescript +import { AgentTool, AgentContext } from '../index'; +import * as fs from 'fs/promises'; +import * as path from 'path'; + +// Tool: Write File +export const writeFileTool: AgentTool = { + name: 'lov-write', + description: 'Write or create a file in the project', + parameters: { + file_path: 'string', + content: 'string' + }, + execute: async (params, context) => { + const { file_path, content } = params; + const fullPath = path.join( + process.env.PROJECTS_DIR!, + context.projectId, + file_path + ); + + // Ensure directory exists + await fs.mkdir(path.dirname(fullPath), { recursive: true }); + + // Write file + await fs.writeFile(fullPath, content, 'utf-8'); + + return { + success: true, + filePath: file_path, + size: Buffer.byteLength(content) + }; + } +}; + +// Tool: Read File +export const readFileTool: AgentTool = { + name: 'lov-view', + description: 'Read contents of a file', + parameters: { + file_path: 'string', + lines: 'string (optional, e.g., "1-100")' + }, + execute: async (params, context) => { + const { file_path, lines } = params; + const fullPath = path.join( + process.env.PROJECTS_DIR!, + context.projectId, + file_path + ); + + const content = await fs.readFile(fullPath, 'utf-8'); + + if (lines) { + const [start, end] = lines.split('-').map(Number); + const allLines = content.split('\n'); + const selectedLines = allLines.slice(start - 1, end); + + return { + filePath: file_path, + content: selectedLines.join('\n'), + lineRange: { start, end }, + totalLines: allLines.length + }; + } + + return { + filePath: file_path, + content, + totalLines: content.split('\n').length + }; + } +}; + +// Tool: Line Replace +export const lineReplaceTool: AgentTool = { + name: 'lov-line-replace', + description: 'Replace specific lines in a file', + parameters: { + file_path: 'string', + search: 'string', + replace: 'string', + first_replaced_line: 'number', + last_replaced_line: 'number' + }, + execute: async (params, context) => { + const { + file_path, + search, + replace, + first_replaced_line, + last_replaced_line + } = params; + + const fullPath = path.join( + process.env.PROJECTS_DIR!, + context.projectId, + file_path + ); + + // Read file + const content = await fs.readFile(fullPath, 'utf-8'); + const lines = content.split('\n'); + + // Validate line range + const targetContent = lines + .slice(first_replaced_line - 1, last_replaced_line) + .join('\n'); + + // Handle ellipsis in search + const searchPattern = search.replace(/\.\.\./g, '[\\s\\S]*?'); + const regex = new RegExp(searchPattern); + + if (!regex.test(targetContent)) { + throw new Error( + `Search content not found at lines ${first_replaced_line}-${last_replaced_line}` + ); + } + + // Replace + const replaced = targetContent.replace(regex, replace); + const newLines = [ + ...lines.slice(0, first_replaced_line - 1), + ...replaced.split('\n'), + ...lines.slice(last_replaced_line) + ]; + + // Write back + await fs.writeFile(fullPath, newLines.join('\n'), 'utf-8'); + + return { + success: true, + filePath: file_path, + linesReplaced: last_replaced_line - first_replaced_line + 1 + }; + } +}; + +// Tool: Search Files +export const searchFilesTool: AgentTool = { + name: 'lov-search-files', + description: 'Search for code patterns in project files', + parameters: { + query: 'string (regex pattern)', + include_pattern: 'string (glob pattern)', + exclude_pattern: 'string (optional)', + case_sensitive: 'boolean (optional)' + }, + execute: async (params, context) => { + const { query, include_pattern, exclude_pattern, case_sensitive } = params; + + // Use ripgrep or similar + const { execSync } = require('child_process'); + const projectPath = path.join(process.env.PROJECTS_DIR!, context.projectId); + + let cmd = `rg "${query}" "${projectPath}"`; + if (include_pattern) { + cmd += ` --glob "${include_pattern}"`; + } + if (exclude_pattern) { + cmd += ` --glob "!${exclude_pattern}"`; + } + if (!case_sensitive) { + cmd += ' -i'; + } + + try { + const output = execSync(cmd, { encoding: 'utf-8' }); + const results = output + .split('\n') + .filter(Boolean) + .map(line => { + const [file, ...rest] = line.split(':'); + return { + file: file.replace(projectPath + '/', ''), + match: rest.join(':') + }; + }); + + return { matches: results, count: results.length }; + } catch (error) { + return { matches: [], count: 0 }; + } + } +}; + +// Tool: Delete File +export const deleteFileTool: AgentTool = { + name: 'lov-delete', + description: 'Delete a file from the project', + parameters: { + file_path: 'string' + }, + execute: async (params, context) => { + const { file_path } = params; + const fullPath = path.join( + process.env.PROJECTS_DIR!, + context.projectId, + file_path + ); + + await fs.unlink(fullPath); + + return { + success: true, + filePath: file_path + }; + } +}; + +// Tool: Rename File +export const renameFileTool: AgentTool = { + name: 'lov-rename', + description: 'Rename a file', + parameters: { + original_file_path: 'string', + new_file_path: 'string' + }, + execute: async (params, context) => { + const { original_file_path, new_file_path } = params; + + const oldPath = path.join( + process.env.PROJECTS_DIR!, + context.projectId, + original_file_path + ); + const newPath = path.join( + process.env.PROJECTS_DIR!, + context.projectId, + new_file_path + ); + + // Ensure new directory exists + await fs.mkdir(path.dirname(newPath), { recursive: true }); + + await fs.rename(oldPath, newPath); + + return { + success: true, + oldPath: original_file_path, + newPath: new_file_path + }; + } +}; + +// Export all tools +export const allTools: AgentTool[] = [ + writeFileTool, + readFileTool, + lineReplaceTool, + searchFilesTool, + deleteFileTool, + renameFileTool +]; +``` + +--- + +## 3. Code Generator + +**File: `packages/agent/src/generators/react-component.ts`** + +```typescript +import { LovableAgent, AgentContext } from '../index'; + +export interface ComponentSpec { + name: string; + type: 'page' | 'component' | 'layout' | 'hook'; + description: string; + props?: PropDefinition[]; + features?: string[]; + designSystem?: DesignSystem; +} + +export interface PropDefinition { + name: string; + type: string; + required: boolean; + description?: string; +} + +export class ReactComponentGenerator { + private agent: LovableAgent; + + constructor(agent: LovableAgent) { + this.agent = agent; + } + + async generate( + spec: ComponentSpec, + context: AgentContext + ): Promise { + const prompt = this.buildPrompt(spec); + + const response = await this.agent.chat(prompt, context); + + // Parse response to extract code + const parsed = this.parseResponse(response.content); + + return { + name: spec.name, + code: parsed.code, + filePath: parsed.filePath, + imports: parsed.imports, + exports: parsed.exports, + tests: parsed.tests + }; + } + + private buildPrompt(spec: ComponentSpec): string { + return `Generate a React TypeScript ${spec.type} component. + +**Specification:** +- Name: ${spec.name} +- Type: ${spec.type} +- Description: ${spec.description} + +${spec.props?.length ? `**Props:** +${spec.props.map(p => `- ${p.name}: ${p.type}${p.required ? ' (required)' : ''} - ${p.description || ''}`).join('\n')}` : ''} + +${spec.features?.length ? `**Features:** +${spec.features.map(f => `- ${f}`).join('\n')}` : ''} + +**Requirements:** +1. Use TypeScript with strict types +2. Follow React best practices (hooks, composition) +3. Use the design system tokens (NO hardcoded colors) +4. Implement responsive design +5. Add ARIA attributes for accessibility +6. Use semantic HTML +7. Add JSDoc comments for props +8. Export component as default + +${spec.designSystem ? `**Design System:** +\`\`\`json +${JSON.stringify(spec.designSystem, null, 2)} +\`\`\` + +Use design tokens like: +- Colors: \`bg-primary\`, \`text-foreground\`, etc. +- Spacing: Design system spacing scale +- Typography: Design system font sizes +` : ''} + +Generate the complete component code.`; + } + + private parseResponse(response: string): ParsedComponent { + // Extract code blocks + const codeBlockRegex = /```(?:typescript|tsx|jsx)?\n([\s\S]*?)```/g; + const matches = [...response.matchAll(codeBlockRegex)]; + + if (matches.length === 0) { + throw new Error('No code block found in response'); + } + + const code = matches[0][1]; + + // Extract file path + const filePathMatch = response.match(/File:\s*`?([^`\n]+)`?/i); + const filePath = filePathMatch + ? filePathMatch[1] + : 'src/components/Generated.tsx'; + + // Extract imports + const imports = this.extractImports(code); + + // Extract exports + const exports = this.extractExports(code); + + // Check for test code + const testCode = matches.find(m => + m[0].includes('test') || m[0].includes('spec') + ); + + return { + code, + filePath, + imports, + exports, + tests: testCode ? testCode[1] : undefined + }; + } + + private extractImports(code: string): string[] { + const importRegex = /^import\s+.+\s+from\s+['"].+['"];?$/gm; + return code.match(importRegex) || []; + } + + private extractExports(code: string): string[] { + const exportRegex = /^export\s+(?:default\s+)?(?:function|const|class|interface|type)\s+(\w+)/gm; + const exports: string[] = []; + + let match; + while ((match = exportRegex.exec(code)) !== null) { + exports.push(match[1]); + } + + return exports; + } +} + +interface ParsedComponent { + code: string; + filePath: string; + imports: string[]; + exports: string[]; + tests?: string; +} + +export interface GeneratedComponent { + name: string; + code: string; + filePath: string; + imports: string[]; + exports: string[]; + tests?: string; +} +``` + +--- + +## 4. Error Detection & Fixer + +**File: `packages/agent/src/fixer/error-detector.ts`** + +```typescript +import * as ts from 'typescript'; +import { ESLint } from 'eslint'; + +export interface CodeError { + type: 'typescript' | 'eslint' | 'build' | 'runtime'; + file: string; + line: number; + column?: number; + message: string; + code?: string; + severity: 'error' | 'warning'; + suggestion?: string; +} + +export class ErrorDetector { + private eslint: ESLint; + + constructor() { + this.eslint = new ESLint({ + useEslintrc: false, + baseConfig: { + extends: ['next/core-web-vitals', 'plugin:@typescript-eslint/recommended'], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { jsx: true } + } + } + }); + } + + async detectAllErrors(projectPath: string): Promise { + const errors: CodeError[] = []; + + // TypeScript errors + const tsErrors = await this.detectTypeScriptErrors(projectPath); + errors.push(...tsErrors); + + // ESLint errors + const eslintErrors = await this.detectESLintErrors(projectPath); + errors.push(...eslintErrors); + + return errors; + } + + async detectTypeScriptErrors(projectPath: string): Promise { + const configPath = ts.findConfigFile( + projectPath, + ts.sys.fileExists, + 'tsconfig.json' + ); + + if (!configPath) { + return []; + } + + const { config } = ts.readConfigFile(configPath, ts.sys.readFile); + const { options, fileNames, errors: configErrors } = ts.parseJsonConfigFileContent( + config, + ts.sys, + projectPath + ); + + // Create program + const program = ts.createProgram(fileNames, options); + + // Get diagnostics + const diagnostics = [ + ...program.getSemanticDiagnostics(), + ...program.getSyntacticDiagnostics() + ]; + + return diagnostics.map(diagnostic => { + const message = ts.flattenDiagnosticMessageText( + diagnostic.messageText, + '\n' + ); + + let file = 'unknown'; + let line = 0; + let column = 0; + + if (diagnostic.file && diagnostic.start) { + file = diagnostic.file.fileName; + const { line: l, character } = diagnostic.file.getLineAndCharacterOfPosition( + diagnostic.start + ); + line = l + 1; + column = character + 1; + } + + return { + type: 'typescript', + file, + line, + column, + message, + code: diagnostic.code?.toString(), + severity: diagnostic.category === ts.DiagnosticCategory.Error + ? 'error' + : 'warning' + }; + }); + } + + async detectESLintErrors(projectPath: string): Promise { + const results = await this.eslint.lintFiles(`${projectPath}/**/*.{ts,tsx,js,jsx}`); + + const errors: CodeError[] = []; + + for (const result of results) { + for (const message of result.messages) { + errors.push({ + type: 'eslint', + file: result.filePath, + line: message.line, + column: message.column, + message: message.message, + code: message.ruleId || undefined, + severity: message.severity === 2 ? 'error' : 'warning', + suggestion: message.suggestions?.[0]?.desc + }); + } + } + + return errors; + } + + async detectBuildErrors(buildOutput: string): Promise { + // Parse build output for errors + const errors: CodeError[] = []; + + // Next.js error pattern + const nextErrorRegex = /Error: (.+)\n\s+at (.+):(\d+):(\d+)/g; + let match; + + while ((match = nextErrorRegex.exec(buildOutput)) !== null) { + errors.push({ + type: 'build', + file: match[2], + line: parseInt(match[3]), + column: parseInt(match[4]), + message: match[1], + severity: 'error' + }); + } + + return errors; + } +} +``` + +**File: `packages/agent/src/fixer/code-fixer.ts`** + +```typescript +import { LovableAgent, AgentContext } from '../index'; +import { CodeError, ErrorDetector } from './error-detector'; + +export class CodeFixer { + private agent: LovableAgent; + private detector: ErrorDetector; + + constructor(agent: LovableAgent) { + this.agent = agent; + this.detector = new ErrorDetector(); + } + + async fixErrors( + errors: CodeError[], + context: AgentContext + ): Promise { + const results: FixResult[] = []; + + // Group errors by file + const errorsByFile = this.groupErrorsByFile(errors); + + for (const [file, fileErrors] of Object.entries(errorsByFile)) { + const result = await this.fixFileErrors(file, fileErrors, context); + results.push(result); + } + + return results; + } + + private async fixFileErrors( + filePath: string, + errors: CodeError[], + context: AgentContext + ): Promise { + // Read file content + const fileContent = await this.readFile(filePath, context); + + // Build fix prompt + const prompt = this.buildFixPrompt(filePath, fileContent, errors); + + // Generate fix + const response = await this.agent.chat(prompt, context); + + // Extract fixed code + const fixedCode = this.extractCode(response.content); + + return { + file: filePath, + originalErrors: errors, + fixedCode, + success: true + }; + } + + private buildFixPrompt( + filePath: string, + fileContent: string, + errors: CodeError[] + ): string { + return `Fix the following errors in \`${filePath}\`: + +**Current Code:** +\`\`\`typescript +${fileContent} +\`\`\` + +**Errors to Fix:** +${errors.map((e, i) => ` +${i + 1}. Line ${e.line}${e.column ? `:${e.column}` : ''} - ${e.type.toUpperCase()} + ${e.message} + ${e.code ? `Code: ${e.code}` : ''} +`).join('\n')} + +**Instructions:** +1. Fix all errors listed above +2. Maintain existing functionality +3. Keep the same code style +4. Do NOT remove existing features +5. Add comments for complex fixes +6. Ensure TypeScript types are correct + +Provide the complete fixed code.`; + } + + private groupErrorsByFile(errors: CodeError[]): Record { + const grouped: Record = {}; + + for (const error of errors) { + if (!grouped[error.file]) { + grouped[error.file] = []; + } + grouped[error.file].push(error); + } + + return grouped; + } + + private async readFile( + filePath: string, + context: AgentContext + ): Promise { + const fs = require('fs/promises'); + const path = require('path'); + + const fullPath = path.join( + process.env.PROJECTS_DIR!, + context.projectId, + filePath + ); + + return await fs.readFile(fullPath, 'utf-8'); + } + + private extractCode(response: string): string { + const codeBlockRegex = /```(?:typescript|tsx|jsx|javascript)?\n([\s\S]*?)```/; + const match = response.match(codeBlockRegex); + + if (!match) { + // If no code block, assume entire response is code + return response; + } + + return match[1]; + } +} + +export interface FixResult { + file: string; + originalErrors: CodeError[]; + fixedCode: string; + success: boolean; + remainingErrors?: CodeError[]; +} +``` + +--- + +# 🌐 II. BACKEND API LAYER + +## 1. Main API Server + +**File: `apps/api/src/index.ts`** + +```typescript +import express from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import morgan from 'morgan'; +import { config } from 'dotenv'; +import { createServer } from 'http'; +import { Server } from 'socket.io'; + +// Routes +import { chatRouter } from './routes/chat'; +import { codegenRouter } from './routes/codegen'; +import { projectRouter } from './routes/project'; +import { deployRouter } from './routes/deploy'; +import { authRouter } from './routes/auth'; + +// Middleware +import { authMiddleware } from './middleware/auth'; +import { rateLimitMiddleware } from './middleware/rate-limit'; +import { errorHandler } from './middleware/error-handler'; + +config(); + +const app = express(); +const httpServer = createServer(app); +const io = new Server(httpServer, { + cors: { + origin: process.env.FRONTEND_URL || 'http://localhost:3000', + credentials: true + } +}); + +// Middleware +app.use(helmet()); +app.use(cors({ + origin: process.env.FRONTEND_URL || 'http://localhost:3000', + credentials: true +})); +app.use(morgan('combined')); +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true })); + +// Rate limiting +app.use(rateLimitMiddleware); + +// Health check +app.get('/health', (req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +// Public routes +app.use('/api/auth', authRouter); + +// Protected routes +app.use('/api/chat', authMiddleware, chatRouter); +app.use('/api/codegen', authMiddleware, codegenRouter); +app.use('/api/projects', authMiddleware, projectRouter); +app.use('/api/deploy', authMiddleware, deployRouter); + +// WebSocket handling +io.use((socket, next) => { + // Auth middleware for WebSocket + const token = socket.handshake.auth.token; + // Verify token... + next(); +}); + +io.on('connection', (socket) => { + console.log('Client connected:', socket.id); + + socket.on('join-project', (projectId) => { + socket.join(`project:${projectId}`); + console.log(`Socket ${socket.id} joined project ${projectId}`); + }); + + socket.on('code-change', async (data) => { + // Broadcast to other clients in the same project + socket.to(`project:${data.projectId}`).emit('code-updated', data); + }); + + socket.on('disconnect', () => { + console.log('Client disconnected:', socket.id); + }); +}); + +// Error handling +app.use(errorHandler); + +// Start server +const PORT = process.env.PORT || 3001; +httpServer.listen(PORT, () => { + console.log(`πŸš€ API server running on port ${PORT}`); + console.log(`πŸ“‘ WebSocket server ready`); +}); + +export { io }; +``` + +--- + +## 2. Chat API Route + +**File: `apps/api/src/routes/chat.ts`** + +```typescript +import { Router } from 'express'; +import { LovableAgent } from '@lovable/agent'; +import { allTools } from '@lovable/agent/tools'; +import { db } from '../lib/db'; +import { io } from '../index'; +import fs from 'fs/promises'; +import path from 'path'; + +const router = Router(); + +// Load system prompt +const SYSTEM_PROMPT = await fs.readFile( + path.join(__dirname, '../prompts/lovable-system.txt'), + 'utf-8' +); + +// Initialize agent +const agent = new LovableAgent( + { + provider: process.env.AI_PROVIDER as 'openai' | 'anthropic', + model: process.env.AI_MODEL || 'gpt-4-turbo-preview', + temperature: 0.7, + maxTokens: 4000 + }, + SYSTEM_PROMPT +); + +// Register tools +allTools.forEach(tool => agent.registerTool(tool)); + +router.post('/message', async (req, res) => { + try { + const { message, projectId, conversationId } = req.body; + const userId = req.user!.id; + + // Get project context + const project = await db.project.findFirst({ + where: { id: projectId, userId } + }); + + if (!project) { + return res.status(404).json({ error: 'Project not found' }); + } + + // Get conversation history + const conversation = await db.conversation.findUnique({ + where: { id: conversationId }, + include: { messages: { orderBy: { createdAt: 'asc' } } } + }); + + if (!conversation) { + return res.status(404).json({ error: 'Conversation not found' }); + } + + // Build context + const context = { + projectId, + fileTree: project.fileTree, + designSystem: project.designSystem, + conversationHistory: conversation.messages, + userPreferences: req.user!.preferences + }; + + // Save user message + await db.message.create({ + data: { + conversationId, + role: 'user', + content: message + } + }); + + // Generate response + const response = await agent.chat(message, context); + + // Save assistant message + const assistantMessage = await db.message.create({ + data: { + conversationId, + role: 'assistant', + content: response.content, + toolCalls: response.toolCalls || [] + } + }); + + // Emit to WebSocket + io.to(`project:${projectId}`).emit('new-message', { + message: assistantMessage, + toolResults: response.toolResults + }); + + // Track usage + await db.usage.create({ + data: { + userId, + tokens: estimateTokens(message + response.content), + type: 'chat' + } + }); + + res.json({ + message: assistantMessage, + toolResults: response.toolResults + }); + } catch (error) { + console.error('Chat error:', error); + res.status(500).json({ error: 'Failed to process message' }); + } +}); + +// Streaming endpoint +router.post('/stream', async (req, res) => { + try { + const { message, projectId, conversationId } = req.body; + const userId = req.user!.id; + + // Set headers for SSE + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + + // Get context (same as above) + const project = await db.project.findFirst({ + where: { id: projectId, userId } + }); + + const conversation = await db.conversation.findUnique({ + where: { id: conversationId }, + include: { messages: true } + }); + + const context = { + projectId, + fileTree: project!.fileTree, + designSystem: project!.designSystem, + conversationHistory: conversation!.messages, + userPreferences: req.user!.preferences + }; + + // Save user message + await db.message.create({ + data: { conversationId, role: 'user', content: message } + }); + + let fullResponse = ''; + + // Stream response + await agent.chat(message, context, { + stream: true, + onToken: (token) => { + fullResponse += token; + res.write(`data: ${JSON.stringify({ token })}\n\n`); + } + }); + + // Save complete response + await db.message.create({ + data: { + conversationId, + role: 'assistant', + content: fullResponse + } + }); + + res.write('data: [DONE]\n\n'); + res.end(); + } catch (error) { + console.error('Stream error:', error); + res.write(`data: ${JSON.stringify({ error: 'Stream failed' })}\n\n`); + res.end(); + } +}); + +function estimateTokens(text: string): number { + // Rough estimate: ~4 characters per token + return Math.ceil(text.length / 4); +} + +export { router as chatRouter }; +``` + +--- + +## 3. Code Generation Route + +**File: `apps/api/src/routes/codegen.ts`** + +```typescript +import { Router } from 'express'; +import { ReactComponentGenerator } from '@lovable/agent/generators'; +import { LovableAgent } from '@lovable/agent'; +import { db } from '../lib/db'; +import { io } from '../index'; + +const router = Router(); + +router.post('/component', async (req, res) => { + try { + const { spec, projectId } = req.body; + const userId = req.user!.id; + + // Get project + const project = await db.project.findFirst({ + where: { id: projectId, userId } + }); + + if (!project) { + return res.status(404).json({ error: 'Project not found' }); + } + + // Create agent + const agent = new LovableAgent( + { + provider: process.env.AI_PROVIDER as any, + model: process.env.AI_MODEL! + }, + SYSTEM_PROMPT + ); + + // Generate component + const generator = new ReactComponentGenerator(agent); + + const context = { + projectId, + fileTree: project.fileTree, + designSystem: project.designSystem, + conversationHistory: [] + }; + + const generated = await generator.generate(spec, context); + + // Write file + const fs = require('fs/promises'); + const path = require('path'); + const fullPath = path.join( + process.env.PROJECTS_DIR!, + projectId, + generated.filePath + ); + + await fs.mkdir(path.dirname(fullPath), { recursive: true }); + await fs.writeFile(fullPath, generated.code, 'utf-8'); + + // Update file tree + // ... update logic + + // Emit to WebSocket + io.to(`project:${projectId}`).emit('file-created', { + path: generated.filePath, + content: generated.code + }); + + res.json({ component: generated }); + } catch (error) { + console.error('Codegen error:', error); + res.status(500).json({ error: 'Failed to generate component' }); + } +}); + +router.post('/fix-errors', async (req, res) => { + try { + const { errors, projectId } = req.body; + const userId = req.user!.id; + + // Implement error fixing logic using CodeFixer + // ... + + res.json({ success: true, fixes: [] }); + } catch (error) { + res.status(500).json({ error: 'Failed to fix errors' }); + } +}); + +export { router as codegenRouter }; +``` + +--- + +## 4. Project Management Route + +**File: `apps/api/src/routes/project.ts`** + +```typescript +import { Router } from 'express'; +import { db } from '../lib/db'; +import { initializeProjectTemplate } from '../lib/templates'; +import archiver from 'archiver'; +import { createReadStream } from 'fs'; +import { join } from 'path'; + +const router = Router(); + +// Create project +router.post('/', async (req, res) => { + try { + const { name, description, framework } = req.body; + const userId = req.user!.id; + + // Create project in database + const project = await db.project.create({ + data: { + name, + description, + userId, + framework: framework || 'next', + fileTree: {}, + designSystem: {}, + dependencies: {} + } + }); + + // Initialize project template + await initializeProjectTemplate(project.id, framework); + + res.json({ project }); + } catch (error) { + console.error('Create project error:', error); + res.status(500).json({ error: 'Failed to create project' }); + } +}); + +// Get project +router.get('/:id', async (req, res) => { + try { + const { id } = req.params; + const userId = req.user!.id; + + const project = await db.project.findFirst({ + where: { id, userId }, + include: { + conversation: { + include: { messages: { orderBy: { createdAt: 'asc' } } } + } + } + }); + + if (!project) { + return res.status(404).json({ error: 'Project not found' }); + } + + res.json({ project }); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch project' }); + } +}); + +// List projects +router.get('/', async (req, res) => { + try { + const userId = req.user!.id; + + const projects = await db.project.findMany({ + where: { userId }, + orderBy: { updatedAt: 'desc' } + }); + + res.json({ projects }); + } catch (error) { + res.status(500).json({ error: 'Failed to list projects' }); + } +}); + +// Export project as ZIP +router.get('/:id/export', async (req, res) => { + try { + const { id } = req.params; + const userId = req.user!.id; + + const project = await db.project.findFirst({ + where: { id, userId } + }); + + if (!project) { + return res.status(404).json({ error: 'Project not found' }); + } + + const projectPath = join(process.env.PROJECTS_DIR!, id); + + // Create ZIP + res.setHeader('Content-Type', 'application/zip'); + res.setHeader( + 'Content-Disposition', + `attachment; filename="${project.name}.zip"` + ); + + const archive = archiver('zip', { zlib: { level: 9 } }); + + archive.on('error', (err) => { + throw err; + }); + + archive.pipe(res); + archive.directory(projectPath, false); + await archive.finalize(); + } catch (error) { + console.error('Export error:', error); + res.status(500).json({ error: 'Failed to export project' }); + } +}); + +// Delete project +router.delete('/:id', async (req, res) => { + try { + const { id } = req.params; + const userId = req.user!.id; + + await db.project.delete({ + where: { id, userId } + }); + + // Delete project files + const fs = require('fs/promises'); + const projectPath = join(process.env.PROJECTS_DIR!, id); + await fs.rm(projectPath, { recursive: true, force: true }); + + res.json({ success: true }); + } catch (error) { + res.status(500).json({ error: 'Failed to delete project' }); + } +}); + +export { router as projectRouter }; +``` + +--- + +_TiαΊΏp tα»₯c vα»›i phαΊ§n Frontend Components trong file tiαΊΏp theo..._ diff --git a/LOVABLE_CLONE_CONFIG_TEMPLATES.md b/LOVABLE_CLONE_CONFIG_TEMPLATES.md new file mode 100644 index 00000000..deff36c1 --- /dev/null +++ b/LOVABLE_CLONE_CONFIG_TEMPLATES.md @@ -0,0 +1,1263 @@ +# βš™οΈ Lovable Clone - Configuration & Infrastructure Templates + +> WebContainer, Stores, Database, and Config templates + +--- + +# 🐳 I. WEBCONTAINER INTEGRATION + +## 1. WebContainer Manager + +**File: `apps/web/lib/webcontainer.ts`** + +```typescript +import { WebContainer, FileSystemTree } from '@webcontainer/api'; + +let webcontainerInstance: WebContainer | null = null; + +export class WebContainerManager { + private static container: WebContainer | null = null; + private static bootPromise: Promise | null = null; + + static async getInstance(): Promise { + if (this.container) { + return this.container; + } + + if (this.bootPromise) { + return this.bootPromise; + } + + this.bootPromise = WebContainer.boot(); + this.container = await this.bootPromise; + this.bootPromise = null; + + return this.container; + } + + static async createProject( + projectId: string, + files: FileSystemTree + ): Promise<{ + container: WebContainer; + url: string; + }> { + const container = await this.getInstance(); + + // Mount files + await container.mount(files); + + // Install dependencies + const installProcess = await container.spawn('npm', ['install']); + + installProcess.output.pipeTo( + new WritableStream({ + write(data) { + console.log('[npm install]', data); + } + }) + ); + + const installExitCode = await installProcess.exit; + + if (installExitCode !== 0) { + throw new Error('Failed to install dependencies'); + } + + // Start dev server + const devProcess = await container.spawn('npm', ['run', 'dev']); + + devProcess.output.pipeTo( + new WritableStream({ + write(data) { + console.log('[dev server]', data); + } + }) + ); + + // Wait for server to be ready + const url = await new Promise((resolve) => { + container.on('server-ready', (port, url) => { + resolve(url); + }); + }); + + return { container, url }; + } + + static async writeFile( + path: string, + content: string + ): Promise { + const container = await this.getInstance(); + await container.fs.writeFile(path, content); + } + + static async readFile(path: string): Promise { + const container = await this.getInstance(); + const file = await container.fs.readFile(path, 'utf-8'); + return file; + } + + static async deleteFile(path: string): Promise { + const container = await this.getInstance(); + await container.fs.rm(path); + } + + static async renameFile( + oldPath: string, + newPath: string + ): Promise { + const container = await this.getInstance(); + const content = await container.fs.readFile(oldPath, 'utf-8'); + await container.fs.writeFile(newPath, content); + await container.fs.rm(oldPath); + } + + static async getFileTree(path: string = '/'): Promise { + const container = await this.getInstance(); + const entries = await container.fs.readdir(path, { + withFileTypes: true + }); + + const tree: FileSystemTree = {}; + + for (const entry of entries) { + const fullPath = `${path}/${entry.name}`; + + if (entry.isDirectory()) { + tree[entry.name] = { + directory: await this.getFileTree(fullPath) + }; + } else { + const content = await container.fs.readFile(fullPath, 'utf-8'); + tree[entry.name] = { + file: { + contents: content + } + }; + } + } + + return tree; + } +} +``` + +--- + +## 2. Project Template Generator + +**File: `apps/api/src/lib/templates.ts`** + +```typescript +import { FileSystemTree } from '@webcontainer/api'; + +export function generateNextJsTemplate(projectName: string): FileSystemTree { + return { + 'package.json': { + file: { + contents: JSON.stringify( + { + name: projectName, + version: '0.1.0', + private: true, + scripts: { + dev: 'next dev', + build: 'next build', + start: 'next start', + lint: 'next lint' + }, + dependencies: { + react: '^18.3.0', + 'react-dom': '^18.3.0', + next: '^14.2.0', + 'class-variance-authority': '^0.7.0', + clsx: '^2.1.0', + 'tailwind-merge': '^2.2.0', + 'lucide-react': '^0.378.0' + }, + devDependencies: { + typescript: '^5', + '@types/node': '^20', + '@types/react': '^18', + '@types/react-dom': '^18', + postcss: '^8', + tailwindcss: '^3.4.0', + autoprefixer: '^10.0.1', + eslint: '^8', + 'eslint-config-next': '14.2.0' + } + }, + null, + 2 + ) + } + }, + 'tsconfig.json': { + file: { + contents: JSON.stringify( + { + compilerOptions: { + lib: ['dom', 'dom.iterable', 'esnext'], + allowJs: true, + skipLibCheck: true, + strict: true, + noEmit: true, + esModuleInterop: true, + module: 'esnext', + moduleResolution: 'bundler', + resolveJsonModule: true, + isolatedModules: true, + jsx: 'preserve', + incremental: true, + plugins: [ + { + name: 'next' + } + ], + paths: { + '@/*': ['./src/*'] + } + }, + include: ['next-env.d.ts', '**/*.ts', '**/*.tsx', '.next/types/**/*.ts'], + exclude: ['node_modules'] + }, + null, + 2 + ) + } + }, + 'next.config.js': { + file: { + contents: `/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; +` + } + }, + 'tailwind.config.ts': { + file: { + contents: `import type { Config } from "tailwindcss"; + +const config: Config = { + darkMode: ["class"], + content: [ + "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", + "./src/components/**/*.{js,ts,jsx,tsx,mdx}", + "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + colors: { + 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))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + }, + }, + plugins: [], +}; + +export default config; +` + } + }, + 'postcss.config.js': { + file: { + contents: `module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; +` + } + }, + '.eslintrc.json': { + file: { + contents: JSON.stringify( + { + extends: 'next/core-web-vitals' + }, + null, + 2 + ) + } + }, + src: { + directory: { + app: { + directory: { + 'layout.tsx': { + file: { + contents: `import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "${projectName}", + description: "Built with Lovable", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} +` + } + }, + 'page.tsx': { + file: { + contents: `export default function Home() { + return ( +
+
+

+ Welcome to ${projectName} +

+

+ Built with Lovable - Start building your dream app! +

+
+
+ ); +} +` + } + }, + 'globals.css': { + file: { + contents: `@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: 221.2 83.2% 53.3%; + --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: 221.2 83.2% 53.3%; + --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: 217.2 91.2% 59.8%; + --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: 224.3 76.3% 48%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} +` + } + } + } + }, + components: { + directory: { + ui: { + directory: {} + } + } + }, + lib: { + directory: { + 'utils.ts': { + file: { + contents: `import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} +` + } + } + } + } + } + }, + public: { + directory: {} + } + }; +} + +export async function initializeProjectTemplate( + projectId: string, + framework: 'next' | 'vite' = 'next' +): Promise { + const fs = require('fs/promises'); + const path = require('path'); + + const projectPath = path.join(process.env.PROJECTS_DIR!, projectId); + + // Create project directory + await fs.mkdir(projectPath, { recursive: true }); + + // Generate template + const template = + framework === 'next' + ? generateNextJsTemplate(projectId) + : generateViteTemplate(projectId); + + // Write files + await writeFileSystemTree(projectPath, template); +} + +async function writeFileSystemTree( + basePath: string, + tree: FileSystemTree +): Promise { + const fs = require('fs/promises'); + const path = require('path'); + + for (const [name, node] of Object.entries(tree)) { + const fullPath = path.join(basePath, name); + + if ('directory' in node) { + await fs.mkdir(fullPath, { recursive: true }); + await writeFileSystemTree(fullPath, node.directory); + } else if ('file' in node) { + await fs.writeFile(fullPath, node.file.contents, 'utf-8'); + } + } +} + +function generateViteTemplate(projectName: string): FileSystemTree { + // Similar structure for Vite + React + return { + // ... Vite template structure + } as FileSystemTree; +} +``` + +--- + +# πŸ“¦ II. STATE MANAGEMENT (Zustand Stores) + +## 1. Chat Store + +**File: `apps/web/stores/chat-store.ts`** + +```typescript +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +export interface Message { + id: string; + role: 'user' | 'assistant' | 'system'; + content: string; + timestamp: Date; + toolCalls?: ToolCall[]; +} + +export interface ToolCall { + name: string; + parameters: Record; +} + +interface ChatState { + messages: Message[]; + projectId: string | null; + conversationId: string | null; + isLoading: boolean; + + // Actions + setProjectId: (id: string) => void; + setConversationId: (id: string) => void; + addMessage: (message: Message) => void; + updateMessage: (id: string, updates: Partial) => void; + clearMessages: () => void; + setLoading: (loading: boolean) => void; +} + +export const useChatStore = create()( + persist( + (set, get) => ({ + messages: [], + projectId: null, + conversationId: null, + isLoading: false, + + setProjectId: (id) => set({ projectId: id }), + + setConversationId: (id) => set({ conversationId: id }), + + addMessage: (message) => + set((state) => ({ + messages: [...state.messages, message] + })), + + updateMessage: (id, updates) => + set((state) => ({ + messages: state.messages.map((msg) => + msg.id === id ? { ...msg, ...updates } : msg + ) + })), + + clearMessages: () => set({ messages: [] }), + + setLoading: (loading) => set({ isLoading: loading }) + }), + { + name: 'chat-storage', + partialize: (state) => ({ + messages: state.messages, + projectId: state.projectId, + conversationId: state.conversationId + }) + } + ) +); +``` + +--- + +## 2. Preview Store + +**File: `apps/web/stores/preview-store.ts`** + +```typescript +import { create } from 'zustand'; + +export interface ConsoleLog { + method: 'log' | 'warn' | 'error' | 'info'; + args: any[]; + timestamp: Date; +} + +export interface NetworkRequest { + method: string; + url: string; + status: number; + timestamp: Date; +} + +interface PreviewState { + url: string; + consoleLogs: ConsoleLog[]; + networkRequests: NetworkRequest[]; + + // Actions + setUrl: (url: string) => void; + reload: () => void; + addConsoleLog: (log: ConsoleLog) => void; + addNetworkRequest: (request: NetworkRequest) => void; + clearConsoleLogs: () => void; + clearNetworkRequests: () => void; +} + +export const usePreviewStore = create((set) => ({ + url: '', + consoleLogs: [], + networkRequests: [], + + setUrl: (url) => set({ url }), + + reload: () => set((state) => ({ url: state.url + '?t=' + Date.now() })), + + addConsoleLog: (log) => + set((state) => ({ + consoleLogs: [...state.consoleLogs, log] + })), + + addNetworkRequest: (request) => + set((state) => ({ + networkRequests: [...state.networkRequests, request] + })), + + clearConsoleLogs: () => set({ consoleLogs: [] }), + + clearNetworkRequests: () => set({ networkRequests: [] }) +})); +``` + +--- + +## 3. Theme Store + +**File: `apps/web/stores/theme-store.ts`** + +```typescript +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +export interface Theme { + colors: { + primary: string; + secondary: string; + accent: string; + background: string; + foreground: string; + }; + typography: { + fontFamily: string; + fontSize: string; + }; + layout: { + borderRadius: string; + maxWidth: string; + }; +} + +interface ThemeState { + theme: Theme; + updateTheme: (updates: Partial) => void; + resetTheme: () => void; +} + +const defaultTheme: Theme = { + colors: { + primary: '#3b82f6', + secondary: '#8b5cf6', + accent: '#f59e0b', + background: '#ffffff', + foreground: '#000000' + }, + typography: { + fontFamily: 'Inter', + fontSize: '16px' + }, + layout: { + borderRadius: '0.5rem', + maxWidth: '1280px' + } +}; + +export const useThemeStore = create()( + persist( + (set) => ({ + theme: defaultTheme, + + updateTheme: (updates) => + set((state) => ({ + theme: { + ...state.theme, + ...updates, + colors: { + ...state.theme.colors, + ...(updates.colors || {}) + }, + typography: { + ...state.theme.typography, + ...(updates.typography || {}) + }, + layout: { + ...state.theme.layout, + ...(updates.layout || {}) + } + } + })), + + resetTheme: () => set({ theme: defaultTheme }) + }), + { + name: 'theme-storage' + } + ) +); +``` + +--- + +## 4. Project Store + +**File: `apps/web/stores/project-store.ts`** + +```typescript +import { create } from 'zustand'; + +export interface Project { + id: string; + name: string; + description?: string; + framework: 'next' | 'vite'; + createdAt: Date; + updatedAt: Date; +} + +interface ProjectState { + projects: Project[]; + currentProject: Project | null; + + // Actions + setProjects: (projects: Project[]) => void; + setCurrentProject: (project: Project | null) => void; + addProject: (project: Project) => void; + updateProject: (id: string, updates: Partial) => void; + deleteProject: (id: string) => void; +} + +export const useProjectStore = create((set) => ({ + projects: [], + currentProject: null, + + setProjects: (projects) => set({ projects }), + + setCurrentProject: (project) => set({ currentProject: project }), + + addProject: (project) => + set((state) => ({ + projects: [...state.projects, project] + })), + + updateProject: (id, updates) => + set((state) => ({ + projects: state.projects.map((p) => + p.id === id ? { ...p, ...updates } : p + ), + currentProject: + state.currentProject?.id === id + ? { ...state.currentProject, ...updates } + : state.currentProject + })), + + deleteProject: (id) => + set((state) => ({ + projects: state.projects.filter((p) => p.id !== id), + currentProject: + state.currentProject?.id === id ? null : state.currentProject + })) +})); +``` + +--- + +# πŸ—„οΈ III. DATABASE SCHEMA (Prisma) + +**File: `packages/database/prisma/schema.prisma`** + +```prisma +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(cuid()) + email String @unique + name String? + avatar String? + passwordHash String? + + // OAuth + oauthProvider String? + oauthId String? + + // Subscription + subscription Subscription? + + // Relations + projects Project[] + usage Usage[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([email]) +} + +model Subscription { + id String @id @default(cuid()) + userId String @unique + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + plan String // 'free' | 'pro' | 'enterprise' + status String @default("active") // 'active' | 'canceled' | 'past_due' + + // Limits + monthlyTokens Int @default(50000) + monthlyProjects Int @default(3) + + // Stripe + stripeCustomerId String? @unique + stripeSubscriptionId String? @unique + stripePriceId String? + stripeCurrentPeriodEnd DateTime? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Project { + id String @id @default(cuid()) + name String + description String? + + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // Framework + framework String @default("next") // 'next' | 'vite' | 'remix' + + // Project data + fileTree Json @default("{}") + designSystem Json @default("{}") + dependencies Json @default("{}") + + // Conversation + conversationId String? + conversation Conversation? @relation(fields: [conversationId], references: [id]) + + // Deployments + deployments Deployment[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([userId]) +} + +model Conversation { + id String @id @default(cuid()) + messages Message[] + projects Project[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Message { + id String @id @default(cuid()) + + conversationId String + conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) + + role String // 'user' | 'assistant' | 'system' + content String @db.Text + toolCalls Json? + + createdAt DateTime @default(now()) + + @@index([conversationId]) +} + +model Deployment { + id String @id @default(cuid()) + + projectId String + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + + provider String // 'vercel' | 'netlify' | 'cloudflare' + url String + status String // 'pending' | 'building' | 'ready' | 'error' + + buildLogs String? @db.Text + error String? @db.Text + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([projectId]) +} + +model Usage { + id String @id @default(cuid()) + + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + tokens Int + type String // 'generation' | 'chat' + + timestamp DateTime @default(now()) + + @@index([userId, timestamp]) +} + +model ApiKey { + id String @id @default(cuid()) + key String @unique + name String + + userId String + + lastUsed DateTime? + createdAt DateTime @default(now()) + + @@index([userId]) +} +``` + +--- + +# πŸ” IV. ENVIRONMENT CONFIGURATION + +## 1. Backend .env + +**File: `apps/api/.env.example`** + +```env +# Server +NODE_ENV=development +PORT=3001 + +# Frontend URL +FRONTEND_URL=http://localhost:3000 + +# Database +DATABASE_URL=postgresql://user:password@localhost:5432/lovable + +# Supabase (alternative to PostgreSQL) +SUPABASE_URL=https://xxxxx.supabase.co +SUPABASE_ANON_KEY=eyJxxx... +SUPABASE_SERVICE_KEY=eyJxxx... + +# AI Providers +AI_PROVIDER=openai # 'openai' | 'anthropic' +AI_MODEL=gpt-4-turbo-preview + +# OpenAI +OPENAI_API_KEY=sk-... + +# Anthropic +ANTHROPIC_API_KEY=sk-ant-... + +# JWT +JWT_SECRET=your-super-secret-jwt-key-change-this + +# Redis (for caching) +REDIS_URL=redis://localhost:6379 + +# Projects Directory +PROJECTS_DIR=/var/projects + +# Stripe (for billing) +STRIPE_SECRET_KEY=sk_test_... +STRIPE_WEBHOOK_SECRET=whsec_... + +# Email (Resend / SendGrid) +RESEND_API_KEY=re_... +FROM_EMAIL=noreply@lovable.dev + +# Deployment Providers +VERCEL_TOKEN=... +NETLIFY_TOKEN=... + +# Monitoring +SENTRY_DSN=https://...@sentry.io/... +POSTHOG_API_KEY=phc_... + +# Vector DB (for semantic search) +PINECONE_API_KEY=... +PINECONE_ENVIRONMENT=us-west1-gcp +``` + +--- + +## 2. Frontend .env + +**File: `apps/web/.env.local.example`** + +```env +# API +NEXT_PUBLIC_API_URL=http://localhost:3001 + +# Supabase (if using for auth) +NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJxxx... + +# Analytics +NEXT_PUBLIC_POSTHOG_KEY=phc_... +NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com + +# Google Analytics +NEXT_PUBLIC_GA_ID=G-... + +# Feature Flags +NEXT_PUBLIC_ENABLE_WEBCONTAINER=true +NEXT_PUBLIC_ENABLE_GITHUB_INTEGRATION=true +``` + +--- + +# πŸ“ V. PACKAGE.JSON CONFIGS + +## 1. Root package.json (Monorepo) + +**File: `package.json`** + +```json +{ + "name": "lovable-clone", + "version": "1.0.0", + "private": true, + "workspaces": [ + "apps/*", + "packages/*" + ], + "scripts": { + "dev": "turbo run dev", + "build": "turbo run build", + "lint": "turbo run lint", + "clean": "turbo run clean && rm -rf node_modules", + "db:generate": "cd packages/database && npx prisma generate", + "db:migrate": "cd packages/database && npx prisma migrate dev", + "db:studio": "cd packages/database && npx prisma studio" + }, + "devDependencies": { + "turbo": "^1.13.0", + "typescript": "^5.4.0", + "prettier": "^3.2.0", + "eslint": "^8.57.0" + }, + "engines": { + "node": ">=18.0.0" + } +} +``` + +--- + +## 2. Turbo Config + +**File: `turbo.json`** + +```json +{ + "$schema": "https://turbo.build/schema.json", + "globalDependencies": ["**/.env.*local"], + "pipeline": { + "build": { + "dependsOn": ["^build"], + "outputs": [".next/**", "!.next/cache/**", "dist/**"] + }, + "dev": { + "cache": false, + "persistent": true + }, + "lint": { + "outputs": [] + }, + "clean": { + "cache": false + } + } +} +``` + +--- + +# πŸš€ VI. DOCKER CONFIGURATION + +## 1. Dockerfile (API) + +**File: `apps/api/Dockerfile`** + +```dockerfile +FROM node:18-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Generate Prisma Client +RUN npx prisma generate + +RUN npm run build + +# Production image +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV production + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nodejs + +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./package.json + +USER nodejs + +EXPOSE 3001 + +ENV PORT 3001 + +CMD ["node", "dist/index.js"] +``` + +--- + +## 2. Docker Compose + +**File: `docker-compose.yml`** + +```yaml +version: '3.8' + +services: + postgres: + image: postgres:15-alpine + environment: + POSTGRES_USER: lovable + POSTGRES_PASSWORD: password + POSTGRES_DB: lovable + ports: + - '5432:5432' + volumes: + - postgres_data:/var/lib/postgresql/data + + redis: + image: redis:7-alpine + ports: + - '6379:6379' + volumes: + - redis_data:/data + + api: + build: + context: ./apps/api + dockerfile: Dockerfile + ports: + - '3001:3001' + environment: + DATABASE_URL: postgresql://lovable:password@postgres:5432/lovable + REDIS_URL: redis://redis:6379 + NODE_ENV: production + depends_on: + - postgres + - redis + volumes: + - projects_data:/var/projects + + web: + build: + context: ./apps/web + dockerfile: Dockerfile + ports: + - '3000:3000' + environment: + NEXT_PUBLIC_API_URL: http://api:3001 + depends_on: + - api + +volumes: + postgres_data: + redis_data: + projects_data: +``` + +--- + +# πŸ“š VII. TYPESCRIPT CONFIGS + +## 1. Shared tsconfig + +**File: `packages/config/tsconfig.json`** + +```json +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020"], + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "allowJs": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true + } +} +``` + +--- + +**HoΓ n tαΊ₯t! Giờ bαΊ‘n cΓ³ Δ‘αΊ§y Δ‘α»§ code templates để bαΊ―t Δ‘αΊ§u build Lovable Clone! πŸŽ‰** diff --git a/LOVABLE_CLONE_FRONTEND_TEMPLATES.md b/LOVABLE_CLONE_FRONTEND_TEMPLATES.md new file mode 100644 index 00000000..135f23d4 --- /dev/null +++ b/LOVABLE_CLONE_FRONTEND_TEMPLATES.md @@ -0,0 +1,1087 @@ +# 🎨 Lovable Clone - Frontend Templates + +> Complete React/Next.js component templates + +--- + +# πŸ’¬ I. CHAT INTERFACE COMPONENTS + +## 1. Main Chat Panel + +**File: `apps/web/components/chat/chat-panel.tsx`** + +```typescript +'use client'; + +import { useState, useRef, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Textarea } from '@/components/ui/textarea'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { ChatMessage } from './chat-message'; +import { Send, Loader2 } from 'lucide-react'; +import { useChatStore } from '@/stores/chat-store'; +import { api } from '@/lib/api'; + +export function ChatPanel() { + const [input, setInput] = useState(''); + const [isStreaming, setIsStreaming] = useState(false); + const scrollRef = useRef(null); + + const { messages, addMessage, projectId, conversationId } = useChatStore(); + + const scrollToBottom = () => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + const sendMessage = async () => { + if (!input.trim() || isStreaming) return; + + const userMessage = { + id: Date.now().toString(), + role: 'user' as const, + content: input, + timestamp: new Date() + }; + + addMessage(userMessage); + setInput(''); + setIsStreaming(true); + + try { + // Use EventSource for streaming + const eventSource = new EventSource( + `${process.env.NEXT_PUBLIC_API_URL}/api/chat/stream`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${localStorage.getItem('token')}` + }, + body: JSON.stringify({ + message: input, + projectId, + conversationId + }) + } + ); + + let assistantMessage = { + id: (Date.now() + 1).toString(), + role: 'assistant' as const, + content: '', + timestamp: new Date() + }; + + addMessage(assistantMessage); + + eventSource.onmessage = (event) => { + if (event.data === '[DONE]') { + eventSource.close(); + setIsStreaming(false); + return; + } + + const data = JSON.parse(event.data); + + if (data.token) { + assistantMessage.content += data.token; + // Update message in store + useChatStore.getState().updateMessage(assistantMessage.id, { + content: assistantMessage.content + }); + } + }; + + eventSource.onerror = () => { + eventSource.close(); + setIsStreaming(false); + console.error('Stream error'); + }; + } catch (error) { + console.error('Send message error:', error); + setIsStreaming(false); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + sendMessage(); + } + }; + + return ( +
+ {/* Header */} +
+

Chat

+

+ Describe what you want to build +

+
+ + {/* Messages */} + +
+ {messages.length === 0 ? ( +
+
+

+ Start a new conversation +

+

+ Describe your app and I'll help you build it +

+
+
+ ) : ( + messages.map((message) => ( + + )) + )} + + {isStreaming && ( +
+ + Thinking... +
+ )} +
+
+ + {/* Input */} +
+
+