v1.0
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* 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.`;
|
||||
}
|
||||
Reference in New Issue
Block a user