@@ -64,40 +64,107 @@ export async function getDesignTip(config) {
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
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.
|
||||
À partir de la configuration actuelle, des PRIORITÉS UTILISATEUR (échelle 1–5) et de la liste de matériaux disponibles, recommande 3 à 5 finitions.
|
||||
|
||||
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ègles :
|
||||
- Les priorités indiquent ce qui compte le PLUS pour le client (5 = très important, 1 = peu important). Tu dois PONDÉRER tes choix en conséquence (ex. : priorité « prix » élevée → privilégier des décors à bon rapport qualité-prix et l'expliquer ; priorité « qualité » élevée → insister sur la noblesse de la finition, la précision du relief, etc.).
|
||||
- Si un prix indicatif [CHF] est fourni pour un matériau, utilise-le honnêtement pour le critère prix.
|
||||
- Pour CHAQUE recommandation, explique CLAIREMENT POURQUOI tu proposes CE matériau : lien explicite avec les priorités du client (pas seulement « joli »).
|
||||
- Remplis les champs criteria avec une phrase courte CHACUN (en français) : ce que ce décor apporte sur ce critère, au regard des priorités.
|
||||
|
||||
Réponds UNIQUEMENT en JSON :
|
||||
Réponds UNIQUEMENT en JSON valide :
|
||||
{
|
||||
"recommendations": [
|
||||
{ "finishId": <number>, "reason": "…raison courte en français…" }
|
||||
{
|
||||
"finishId": <number>,
|
||||
"summary": "2 phrases max : pourquoi ce choix au regard des priorités",
|
||||
"criteria": {
|
||||
"price": "phrase sur le rapport prix / gamme / accessibilité",
|
||||
"quality": "phrase sur la qualité de finition, rendu, sensation",
|
||||
"durability": "phrase sur tenue dans le temps, entretien, résistance",
|
||||
"aesthetics": "phrase sur l'harmonie couleur/style avec le projet"
|
||||
},
|
||||
"reason": "une ligne très courte pour infobulle (optionnel, peut résumer summary)"
|
||||
}
|
||||
]
|
||||
}`;
|
||||
|
||||
function clampPriority(n, fallback = 3) {
|
||||
const x = Number(n);
|
||||
if (!Number.isFinite(x)) return fallback;
|
||||
return Math.min(5, Math.max(1, Math.round(x)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} [p] - { price, quality, durability, aesthetics } each 1–5
|
||||
*/
|
||||
export function normalizeRecommendationPriorities(p) {
|
||||
const d = { price: 3, quality: 3, durability: 3, aesthetics: 3 };
|
||||
if (!p || typeof p !== 'object') return { ...d };
|
||||
return {
|
||||
price: clampPriority(p.price, d.price),
|
||||
quality: clampPriority(p.quality, d.quality),
|
||||
durability: clampPriority(p.durability, d.durability),
|
||||
aesthetics: clampPriority(p.aesthetics, d.aesthetics)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeMaterialRecommendation(rec) {
|
||||
if (!rec || rec.finishId == null) return null;
|
||||
const criteria = rec.criteria && typeof rec.criteria === 'object' ? rec.criteria : {};
|
||||
const summary = (rec.summary || rec.reason || '').trim();
|
||||
const reason = (rec.reason || summary).trim();
|
||||
return {
|
||||
finishId: Number(rec.finishId),
|
||||
summary,
|
||||
reason,
|
||||
criteria: {
|
||||
price: String(criteria.price || '').trim(),
|
||||
quality: String(criteria.quality || '').trim(),
|
||||
durability: String(criteria.durability || '').trim(),
|
||||
aesthetics: String(criteria.aesthetics || '').trim()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildPrioritiesBlock(priorities) {
|
||||
const p = normalizeRecommendationPriorities(priorities);
|
||||
return `PRIORITÉS UTILISATEUR (1 = peu important, 5 = très important — ajuste tes recommandations et tes explications en conséquence) :
|
||||
- Prix / rapport qualité-prix : ${p.price}
|
||||
- Qualité perçue & finition : ${p.quality}
|
||||
- Durabilité & entretien : ${p.durability}
|
||||
- Esthétique & harmonie avec le projet : ${p.aesthetics}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recommend materials that complement the current configuration
|
||||
* @param {object} config - Current wardrobe configuration
|
||||
* @param {Array} availableFinishes - [{ id, name, collection, code }]
|
||||
* @param {Array} availableFinishes - [{ id, name, collection, code, price_chf? }]
|
||||
* @param {object} [roomAnalysis] - Optional room photo analysis
|
||||
* @returns {{ recommendations: Array<{ finishId: number, reason: string }> }}
|
||||
* @param {object} [priorities] - { price, quality, durability, aesthetics } each 1–5
|
||||
* @returns {{ recommendations: Array<{ finishId, summary, reason, criteria }> }}
|
||||
*/
|
||||
export async function recommendMaterials(config, availableFinishes, roomAnalysis) {
|
||||
export async function recommendMaterials(config, availableFinishes, roomAnalysis, priorities) {
|
||||
const client = getOpenAIClient();
|
||||
|
||||
const configSummary = buildConfigSummary(config);
|
||||
const priBlock = buildPrioritiesBlock(priorities);
|
||||
|
||||
// Compact finish list (only names + ids to save tokens)
|
||||
// Compact finish list (names + ids + optional price for fair "prix" explanations)
|
||||
const finishList = availableFinishes
|
||||
.slice(0, 80) // limit to avoid token overflow
|
||||
.map(f => `${f.id}: ${f.name || f.label} (${f.collection || '?'})`)
|
||||
.map((f) => {
|
||||
const name = f.name || f.label || '?';
|
||||
const col = f.collection || '?';
|
||||
const price =
|
||||
f.price_chf != null && Number.isFinite(Number(f.price_chf))
|
||||
? ` — indicatif ${Number(f.price_chf).toFixed(2)} CHF`
|
||||
: '';
|
||||
return `${f.id}: ${name} (${col})${price}`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
let userMessage = `CONFIGURATION ACTUELLE:\n${configSummary}\n\nMATÉRIAUX DISPONIBLES:\n${finishList}`;
|
||||
let userMessage = `${priBlock}\n\nCONFIGURATION ACTUELLE:\n${configSummary}\n\nMATÉRIAUX DISPONIBLES:\n${finishList}`;
|
||||
|
||||
if (roomAnalysis) {
|
||||
userMessage += `\n\nCONTEXTE PIÈCE:\n${JSON.stringify(roomAnalysis, null, 1)}`;
|
||||
@@ -105,8 +172,8 @@ export async function recommendMaterials(config, availableFinishes, roomAnalysis
|
||||
|
||||
const response = await client.chat.completions.create({
|
||||
model: 'gpt-4o',
|
||||
temperature: 0.7,
|
||||
max_tokens: 400,
|
||||
temperature: 0.65,
|
||||
max_tokens: 1400,
|
||||
messages: [
|
||||
{ role: 'system', content: MATERIAL_REC_PROMPT },
|
||||
{ role: 'user', content: userMessage }
|
||||
@@ -117,9 +184,13 @@ export async function recommendMaterials(config, availableFinishes, roomAnalysis
|
||||
const raw = response.choices[0]?.message?.content;
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return { recommendations: (parsed.recommendations || []).slice(0, 5) };
|
||||
const rawList = (parsed.recommendations || []).slice(0, 5);
|
||||
const recommendations = rawList
|
||||
.map(normalizeMaterialRecommendation)
|
||||
.filter(Boolean);
|
||||
return { recommendations, priorities: normalizeRecommendationPriorities(priorities) };
|
||||
} catch {
|
||||
return { recommendations: [] };
|
||||
return { recommendations: [], priorities: normalizeRecommendationPriorities(priorities) };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user