Spaces:
Paused
Paused
| import fs from 'fs/promises'; | |
| import path from 'path'; | |
| import 'dotenv/config'; // Loader variabler fra .env | |
| interface AIResponse { | |
| code: string; | |
| modelUsed: string; | |
| } | |
| export class EvolutionService { | |
| // Stier til mapperne | |
| private liveDir = path.resolve(__dirname, '../../../../apps/matrix-frontend/src/widgets/generated'); | |
| private stagingDir = path.resolve(__dirname, '../../../../apps/matrix-frontend/src/widgets/staging'); | |
| // SYSTEM PROMPT: WidgeTDC Design Systemet (Opdateret med strengere regler) | |
| private systemPrompt = ` | |
| Du er "The Architect", en elite React-udvikler for WidgeTDC platformen. | |
| Din opgave er at generere en moderne, responsiv Widget baseret på brugerens prompt. | |
| DESIGN REGLER (SKAL OVERHOLDES): | |
| 1. **Glassmorphism:** Brug mørke baggrunde med transparens (f.eks. 'bg-[#051e3c]/90', 'border-white/10', 'backdrop-blur-md'). | |
| 2. **Icons:** Brug KUN 'lucide-react' til ikoner. | |
| 3. **Layout:** Widgetten skal fylde 'h-full w-full' og håndtere overflow pænt. | |
| 4. **Interactivity:** Brug React hooks (useState, useEffect) til at gøre den levende. | |
| 5. **No Placeholders:** Brug ALDRIG 'Lorem Ipsum' eller 'Content here'. Generer realistisk mock-data hvis nødvendigt. | |
| 6. **Robustness:** Inkluder altid fejlhåndtering i komponenten. | |
| TEKNISKE KRAV: | |
| 1. Output KUN ren kode. Ingen markdown blokke (\`\`\`), ingen forklaringer før/efter. | |
| 2. Komponenten skal eksporteres som 'default'. | |
| 3. Importer React korrekt. | |
| `; | |
| // --- HOVEDMETODE: GENERER TIL STAGING --- | |
| async generateWidget(userPrompt: string): Promise<{ success: boolean; filename?: string; code?: string; error?: string; model?: string }> { | |
| try { | |
| console.log(`[ARCHITECT] 🧠 Modtog ordre: "${userPrompt.substring(0, 50)}..."`); | |
| const componentName = 'DynamicWidget_' + Date.now(); | |
| const filename = `${componentName}.tsx`; | |
| // 1. KALD AI (Smart routing) | |
| const aiResult = await this.callSmartLLM(userPrompt, componentName); | |
| let rawCode = aiResult.code; | |
| // 2. RENS KODEN (Fjern markdown hvis AI'en glemte reglerne) | |
| rawCode = rawCode.replace(/```tsx/g, '').replace(/```typescript/g, '').replace(/```/g, '').trim(); | |
| // Sikkerhedsnet: Tjek at koden ser valid ud | |
| if (!rawCode.includes('export default')) { | |
| throw new Error("AI genererede ugyldig kode (manglede export default)"); | |
| } | |
| // 3. GEM TIL STAGING (Klar til review) | |
| await fs.mkdir(this.stagingDir, { recursive: true }); | |
| await fs.writeFile(path.join(this.stagingDir, filename), rawCode); | |
| console.log(`[ARCHITECT] ✨ Kode genereret med ${aiResult.modelUsed}: ${filename}`); | |
| return { success: true, filename, code: rawCode, model: aiResult.modelUsed }; | |
| } catch (error: any) { | |
| console.error("[ARCHITECT] ❌ Critical Failure:", error); | |
| return { success: false, error: error.message }; | |
| } | |
| } | |
| // --- INTELLIGENT LLM ROUTER --- | |
| private async callSmartLLM(userPrompt: string, componentName: string): Promise<AIResponse> { | |
| const anthropicKey = process.env.ANTHROPIC_API_KEY; | |
| const deepseekKey = process.env.DEEPSEEK_API_KEY || process.env.OPENAI_API_KEY; | |
| // Debug: Tjek om nøgler findes (uden at logge dem) | |
| if (!anthropicKey && !deepseekKey) { | |
| console.error("[ARCHITECT] INGEN API NØGLER FUNDET I .env!"); | |
| throw new Error("Ingen API nøgler konfigureret. Tjek .env filen i backend."); | |
| } | |
| const fullPrompt = `Lav en React komponent kaldet '${componentName}'.\nFunktionalitet: ${userPrompt}`; | |
| // OPTION A: ANTHROPIC (Claude 3.5 Sonnet - Foretrukket til kode) | |
| if (anthropicKey) { | |
| try { | |
| console.log("[ARCHITECT] Forbinder til Anthropic (Claude 3.5 Sonnet)..."); | |
| const response = await fetch('[https://api.anthropic.com/v1/messages](https://api.anthropic.com/v1/messages)', { | |
| method: 'POST', | |
| headers: { | |
| 'x-api-key': anthropicKey, | |
| 'anthropic-version': '2023-06-01', | |
| 'content-type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| model: 'claude-3-5-sonnet-20240620', | |
| max_tokens: 4000, | |
| system: this.systemPrompt, | |
| messages: [{ role: 'user', content: fullPrompt }] | |
| }) | |
| }); | |
| if (!response.ok) { | |
| const errText = await response.text(); | |
| throw new Error(`Anthropic API Error: ${errText}`); | |
| } | |
| const data: any = await response.json(); | |
| return { code: data.content[0].text, modelUsed: 'Claude 3.5 Sonnet' }; | |
| } catch (err: any) { | |
| console.warn("[ARCHITECT] Anthropic fejlede, forsøger fallback...", err.message); | |
| // Fortsæt til fallback hvis muligt... | |
| } | |
| } | |
| // OPTION B: DEEPSEEK / OPENAI (Fallback) | |
| if (deepseekKey) { | |
| console.log("[ARCHITECT] Forbinder til DeepSeek/OpenAI Compatible..."); | |
| const baseURL = process.env.DEEPSEEK_BASE_URL || '[https://api.deepseek.com/v1](https://api.deepseek.com/v1)'; | |
| const response = await fetch(`${baseURL}/chat/completions`, { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': `Bearer ${deepseekKey}`, | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| model: process.env.DEEPSEEK_MODEL || 'deepseek-coder', | |
| messages: [ | |
| { role: 'system', content: this.systemPrompt }, | |
| { role: 'user', content: fullPrompt } | |
| ], | |
| temperature: 0.2 | |
| }) | |
| }); | |
| if (!response.ok) { | |
| const errText = await response.text(); | |
| throw new Error(`DeepSeek/OpenAI API Error: ${errText}`); | |
| } | |
| const data: any = await response.json(); | |
| return { code: data.choices[0].message.content, modelUsed: 'DeepSeek/OpenAI' }; | |
| } | |
| throw new Error("Alle AI providers fejlede. Tjek internetforbindelse og API status."); | |
| } | |
| // --- DEPLOYMENT LOGIK --- | |
| async promoteWidget(filename: string): Promise<{ success: boolean; error?: string }> { | |
| try { | |
| const sourcePath = path.join(this.stagingDir, filename); | |
| const destPath = path.join(this.liveDir, filename); | |
| // Tjek om staging filen eksisterer | |
| try { | |
| await fs.access(sourcePath); | |
| } catch { | |
| throw new Error(`Kan ikke finde filen ${filename} i staging. Måske er den allerede deployet?`); | |
| } | |
| // Flyt filen | |
| await fs.rename(sourcePath, destPath); | |
| console.log(`[ARCHITECT] 🚀 DEPLOYED TO PRODUCTION: ${filename}`); | |
| return { success: true }; | |
| } catch (error: any) { | |
| console.error("[ARCHITECT] Deployment fejl:", error); | |
| return { success: false, error: error.message }; | |
| } | |
| } | |
| async discardWidget(filename: string) { | |
| try { | |
| await fs.unlink(path.join(this.stagingDir, filename)); | |
| console.log(`[ARCHITECT] 🗑️ Kasseret: ${filename}`); | |
| return { success: true }; | |
| } catch (error) { | |
| return { success: false }; | |
| } | |
| } | |
| } | |
| export const evolutionService = new EvolutionService(); |