122 lines
3.3 KiB
React
122 lines
3.3 KiB
React
/**
|
|
* 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>
|
|
);
|
|
}
|