Files
apex/server/services/designAdvisor.js
T
2026-02-13 00:21:53 +01:00

214 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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": <number>, "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.`;
}