This commit is contained in:
lejay
2026-02-13 00:21:53 +01:00
parent 81d2412835
commit 6923c319a1
85 changed files with 17836 additions and 0 deletions
+121
View File
@@ -0,0 +1,121 @@
/**
* DesignTip - AI-powered design advice card that fades in below the 3D preview.
* Shows contextual tips, compliments, and trends based on current configuration.
*/
import { useState, useEffect, useRef, useCallback } from 'react';
import styles from './DesignTip.module.css';
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:3001';
const CATEGORY_ICONS = {
compliment: '✨',
conseil: '💡',
tendance: '🎯'
};
export default function DesignTip({ config }) {
const [tip, setTip] = useState(null);
const [loading, setLoading] = useState(false);
const [visible, setVisible] = useState(false);
const debounceRef = useRef(null);
const abortRef = useRef(null);
const fetchTip = useCallback(async (cfg) => {
// Cancel previous request
if (abortRef.current) abortRef.current.abort();
const controller = new AbortController();
abortRef.current = controller;
setLoading(true);
try {
const body = {
config: {
width: cfg.totalWidth,
height: cfg.height,
depth: cfg.depth,
installationType: cfg.installationType,
columns: cfg.modules?.map(m => ({
type: m.type || m.moduleCode,
width: m.width,
shelves: m.shelves,
drawers: m.drawers,
hasRod: m.hasRod
})),
materialName: cfg.finishLabel || null,
doorType: cfg.doorType
}
};
const res = await fetch(`${API_BASE}/api/ai/design-tips`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal: controller.signal
});
if (!res.ok) throw new Error('API error');
const data = await res.json();
if (data.tip) {
setTip(data);
setVisible(true);
}
} catch (err) {
if (err.name !== 'AbortError') {
// Silently fail — tip is optional
setVisible(false);
}
} finally {
setLoading(false);
}
}, []);
// Debounced fetch on config change
useEffect(() => {
if (!config || !config.modules?.length) return;
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => {
fetchTip(config);
}, 2000); // 2s debounce to avoid spamming on rapid changes
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current);
};
}, [
config?.totalWidth,
config?.height,
config?.modules?.length,
config?.modules?.map(m => m.type).join(','),
config?.finishId,
config?.doorType,
fetchTip
]);
// Cleanup on unmount
useEffect(() => {
return () => {
if (abortRef.current) abortRef.current.abort();
};
}, []);
if (!visible && !loading) return null;
return (
<div className={`${styles.container} ${visible && tip ? styles.visible : ''}`}>
{loading && !tip && (
<div className={styles.shimmer}>
<div className={styles.shimmerLine} />
<div className={styles.shimmerLineShort} />
</div>
)}
{tip && (
<div className={styles.tipContent}>
<span className={styles.icon}>{CATEGORY_ICONS[tip.category] || '✨'}</span>
<p className={styles.text}>{tip.tip}</p>
<span className={styles.badge}>IA</span>
</div>
)}
</div>
);
}