113 lines
4.2 KiB
JavaScript
113 lines
4.2 KiB
JavaScript
import { useEffect, useRef, useState } from 'react';
|
|
import { SendHorizontal, Sparkles } from 'lucide-react';
|
|
import { ChatMessage } from './ChatMessage';
|
|
import { Spinner } from './Atoms';
|
|
import { api } from '../api/client';
|
|
|
|
const SUGGESTIONS = [
|
|
'Recommend a low sugar biscuit',
|
|
'What snacks come in family packs?',
|
|
'Show me something under ₹20',
|
|
'Any options with no added preservatives?',
|
|
];
|
|
|
|
export function ChatPanel({ brand, onSelectSource }) {
|
|
const [messages, setMessages] = useState([]);
|
|
const [input, setInput] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState(null);
|
|
const scrollRef = useRef(null);
|
|
|
|
useEffect(() => {
|
|
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' });
|
|
}, [messages, loading]);
|
|
|
|
async function send(text) {
|
|
const query = (text ?? input).trim();
|
|
if (!query || loading) return;
|
|
|
|
setInput('');
|
|
setError(null);
|
|
const history = messages.map((m) => ({ role: m.role, content: m.content }));
|
|
setMessages((prev) => [...prev, { role: 'user', content: query }]);
|
|
setLoading(true);
|
|
|
|
try {
|
|
const res = await api.chat({ query, brand: brand || undefined, history });
|
|
setMessages((prev) => [...prev, { role: 'assistant', content: res.answer, sources: res.sources, detectedCategory: res.detected_category }]);
|
|
} catch (err) {
|
|
setError(err.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-full flex-col">
|
|
<div ref={scrollRef} className="flex-1 space-y-4 overflow-y-auto px-6 py-5">
|
|
{messages.length === 0 && (
|
|
<div className="flex h-full flex-col items-center justify-center gap-4 text-center">
|
|
<span className="flex h-12 w-12 items-center justify-center rounded-full bg-amber-100 text-amber-600">
|
|
<Sparkles className="h-6 w-6" />
|
|
</span>
|
|
<div>
|
|
<p className="font-display font-semibold text-ink-950">Ask about the catalog</p>
|
|
<p className="mt-1 max-w-sm text-sm text-slate-500">
|
|
I'll answer using only the products actually in the catalog
|
|
{brand ? ` for ${brand}` : ''}, and show you exactly which ones I used.
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-wrap justify-center gap-2">
|
|
{SUGGESTIONS.map((s) => (
|
|
<button
|
|
key={s}
|
|
onClick={() => send(s)}
|
|
className="rounded-full border border-ink-900/15 px-3 py-1.5 text-xs text-ink-700 transition hover:border-amber-500 hover:text-amber-600"
|
|
>
|
|
{s}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{messages.map((m, i) => (
|
|
<ChatMessage key={i} role={m.role} content={m.content} sources={m.sources} detectedCategory={m.detectedCategory} onSelectSource={onSelectSource} />
|
|
))}
|
|
|
|
{loading && (
|
|
<div className="flex items-center gap-2 pl-9">
|
|
<Spinner label="Thinking through the catalog…" />
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<p className="rounded-lg bg-maroon-100 px-3 py-2 text-sm text-maroon-600">{error}</p>
|
|
)}
|
|
</div>
|
|
|
|
<form
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
send();
|
|
}}
|
|
className="flex items-center gap-2 border-t border-ink-900/10 bg-paper-50 px-4 py-3"
|
|
>
|
|
<input
|
|
value={input}
|
|
onChange={(e) => setInput(e.target.value)}
|
|
placeholder={brand ? `Ask about ${brand}'s products…` : 'Ask about any product in the catalog…'}
|
|
className="flex-1 rounded-full border border-ink-900/10 bg-white px-4 py-2.5 text-sm focus:border-amber-500 focus:outline-none focus:ring-2 focus:ring-amber-500/20"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
disabled={loading || !input.trim()}
|
|
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-ink-900 text-paper-100 transition hover:bg-ink-800 disabled:opacity-40"
|
|
>
|
|
<SendHorizontal className="h-4 w-4" />
|
|
</button>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|