/** * AI Design Advisor — GPT-4o powered design tips, material recommendations, and DALL-E 3 renders * Helps customers feel confident about their wardrobe design choices. */ import OpenAI from 'openai'; import crypto from 'crypto'; let openaiClient = null; function getOpenAIClient() { if (!openaiClient) { if (!process.env.OPENAI_API_KEY) { throw new Error('OPENAI_API_KEY is not configured. Add it to server/.env'); } openaiClient = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); } return openaiClient; } /* -------------------------------------------------------------------------- */ /* 1. Design Tips (GPT-4o text) */ /* -------------------------------------------------------------------------- */ const DESIGN_TIP_PROMPT = `Tu es un designer d'intérieur suisse spécialisé dans les armoires sur mesure haut de gamme. À partir de la configuration ci-dessous, donne UN SEUL conseil ou commentaire positif en 1-2 phrases maximum. Le ton doit être chaleureux, professionnel et encourageant — le client doit se sentir confiant dans son choix. Varie tes réponses : parfois un compliment sur la combinaison, parfois un conseil pratique, parfois une info sur les tendances. Réponds UNIQUEMENT en JSON : { "tip": "…votre conseil en français…", "category": "compliment" | "conseil" | "tendance" }`; /** * Generate a design tip for the current configuration * @param {object} config - { width, height, depth, columns (array of {type, shelves, drawers, hasRod}), material, installationType } * @returns {{ tip: string, category: string }} */ export async function getDesignTip(config) { const client = getOpenAIClient(); const configSummary = buildConfigSummary(config); const response = await client.chat.completions.create({ model: 'gpt-4o', temperature: 0.9, max_tokens: 200, messages: [ { role: 'system', content: DESIGN_TIP_PROMPT }, { role: 'user', content: configSummary } ], response_format: { type: 'json_object' } }); const raw = response.choices[0]?.message?.content; try { return JSON.parse(raw); } catch { return { tip: raw, category: 'conseil' }; } } /* -------------------------------------------------------------------------- */ /* 2. Material Recommendations (GPT-4o) */ /* -------------------------------------------------------------------------- */ const MATERIAL_REC_PROMPT = `Tu es un conseiller en matériaux et couleurs pour armoires sur mesure suisses haut de gamme. À partir de la configuration actuelle et de la liste de matériaux disponibles, recommande 3 à 5 matériaux qui iraient parfaitement ensemble. Critères de recommandation : - Harmonie des couleurs avec les matériaux déjà sélectionnés - Cohérence avec le style d'armoire (dimensions, configuration) - Tendances actuelles en design d'intérieur suisse - Si un contexte de pièce est fourni, s'accorder avec le style de la pièce Réponds UNIQUEMENT en JSON : { "recommendations": [ { "finishId": , "reason": "…raison courte en français…" } ] }`; /** * Recommend materials that complement the current configuration * @param {object} config - Current wardrobe configuration * @param {Array} availableFinishes - [{ id, name, collection, code }] * @param {object} [roomAnalysis] - Optional room photo analysis * @returns {{ recommendations: Array<{ finishId: number, reason: string }> }} */ export async function recommendMaterials(config, availableFinishes, roomAnalysis) { const client = getOpenAIClient(); const configSummary = buildConfigSummary(config); // Compact finish list (only names + ids to save tokens) const finishList = availableFinishes .slice(0, 80) // limit to avoid token overflow .map(f => `${f.id}: ${f.name || f.label} (${f.collection || '?'})`) .join('\n'); let userMessage = `CONFIGURATION ACTUELLE:\n${configSummary}\n\nMATÉRIAUX DISPONIBLES:\n${finishList}`; if (roomAnalysis) { userMessage += `\n\nCONTEXTE PIÈCE:\n${JSON.stringify(roomAnalysis, null, 1)}`; } const response = await client.chat.completions.create({ model: 'gpt-4o', temperature: 0.7, max_tokens: 400, messages: [ { role: 'system', content: MATERIAL_REC_PROMPT }, { role: 'user', content: userMessage } ], response_format: { type: 'json_object' } }); const raw = response.choices[0]?.message?.content; try { const parsed = JSON.parse(raw); return { recommendations: (parsed.recommendations || []).slice(0, 5) }; } catch { return { recommendations: [] }; } } /* -------------------------------------------------------------------------- */ /* 3. Photorealistic Render (DALL-E 3) */ /* -------------------------------------------------------------------------- */ // Simple in-memory render cache (config hash → image URL) const renderCache = new Map(); /** * Generate a photorealistic render of the configured wardrobe using DALL-E 3 * @param {object} config - Current wardrobe configuration * @param {object} [roomContext] - Optional room analysis for context * @returns {{ imageUrl: string, cached: boolean }} */ export async function generateRender(config, roomContext) { const client = getOpenAIClient(); // Cache by config hash const hash = crypto.createHash('md5').update(JSON.stringify(config)).digest('hex'); if (renderCache.has(hash)) { return { imageUrl: renderCache.get(hash), cached: true }; } const prompt = buildRenderPrompt(config, roomContext); const response = await client.images.generate({ model: 'dall-e-3', prompt, n: 1, size: '1792x1024', quality: 'hd', style: 'natural' }); const imageUrl = response.data[0]?.url; if (imageUrl) { renderCache.set(hash, imageUrl); // Auto-evict after 30 minutes (DALL-E URLs expire after ~1h) setTimeout(() => renderCache.delete(hash), 30 * 60 * 1000); } return { imageUrl, cached: false }; } /* -------------------------------------------------------------------------- */ /* Helpers */ /* -------------------------------------------------------------------------- */ function buildConfigSummary(config) { const columns = (config.columns || config.modules || []) .map((c, i) => ` Colonne ${i + 1}: ${c.type || c.moduleCode || '?'} — ${c.width || '?'}mm, ${c.shelves || 0} étagères, ${c.drawers || 0} tiroirs${c.hasRod ? ', tringle' : ''}`) .join('\n'); return `Armoire sur mesure SIGNATURE Dimensions: ${config.width || '?'}mm × ${config.height || '?'}mm × ${config.depth || '?'}mm Installation: ${config.installationType || 'libre'} Colonnes:\n${columns} Matériau principal: ${config.materialName || config.finishLabel || 'non défini'} Portes: ${config.doorType || 'battantes'}`; } function buildRenderPrompt(config, roomContext) { const cols = (config.columns || config.modules || []); const colDesc = cols.map((c, i) => { const type = c.type || c.moduleCode || 'standard'; const parts = []; if (c.hasRod) parts.push('a hanging rod'); if (c.shelves > 0) parts.push(`${c.shelves} shelves`); if (c.drawers > 0) parts.push(`${c.drawers} drawers`); return `column ${i + 1}: ${type} with ${parts.join(', ') || 'empty space'}`; }).join('; '); const widthM = ((config.width || 2400) / 1000).toFixed(1); const heightM = ((config.height || 2400) / 1000).toFixed(1); let roomDesc = 'a modern, well-lit bedroom with neutral walls'; if (roomContext) { const walls = roomContext.walls || []; const wallColors = walls.filter(w => w.color).map(w => w.color).join(', '); roomDesc = `a ${roomContext.roomType || 'bedroom'} with ${wallColors || 'neutral'} walls`; } return `Professional interior design photograph of a premium Swiss-made custom wardrobe in ${roomDesc}. The wardrobe is ${widthM}m wide and ${heightM}m tall, built-in style, with ${cols.length} columns: ${colDesc}. Material: ${config.materialName || 'light oak wood finish'}. Doors are ${config.doorType === 'coulissantes' ? 'sliding' : 'hinged'}, partially open to reveal the interior. Style: clean Scandinavian-Swiss aesthetic, warm natural lighting, photorealistic quality. Shot with a wide-angle lens at eye level. The wardrobe should look premium, well-crafted, and perfectly integrated into the room.`; }