import { useEffect, useRef, useState } from 'react'; import { BotIcon, SendIcon, SparklesIcon, XIcon } from 'lucide-react'; import { track } from '../analytics'; type ChatMessage = { role: 'user' | 'assistant'; content: string; streaming?: boolean; error?: boolean; }; type SseFrame = { content?: string; error?: string; done?: boolean; }; /** شناسه گفتگو برای هر نشست — در sessionStorage می‌ماند تا رفرش صفحه تاریخچه را نگه دارد */ function getThreadId(): string { const KEY = 'chat_thread_id'; let id = sessionStorage.getItem(KEY); if (!id) { id = `t_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`; sessionStorage.setItem(KEY, id); } return id; } /** خواندن فریم‌های SSE از پاسخ fetch (استریم توکن‌به‌توکن) */ async function readSse(response: Response, onFrame: (frame: SseFrame) => void) { const reader = response.body?.getReader(); if (!reader) throw new Error('پاسخ استریمی ندارد'); const decoder = new TextDecoder(); let buffer = ''; for (;;) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() ?? ''; for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith('data: ')) { try { onFrame(JSON.parse(trimmed.slice(6)) as SseFrame); } catch { // فریم ناقص — نادیده بگیر } } } } } export function ChatWidget() { const [open, setOpen] = useState(false); const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); const [suggestions, setSuggestions] = useState([]); const scrollEndRef = useRef(null); const inputRef = useRef(null); const assistantIndexRef = useRef(null); // پیشنهاد پرسش‌ها فقط یک بار برای هر بازشدن اول useEffect(() => { if (!open || suggestions.length > 0) return; fetch('/chat/suggestions') .then((r) => (r.ok ? r.json() : Promise.reject(new Error(String(r.status))))) .then((d) => setSuggestions((d.questions ?? []).slice(0, 4))) .catch(() => setSuggestions([])); }, [open, suggestions.length]); useEffect(() => { scrollEndRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }); }, [messages, open]); const sendMessage = async (text: string) => { const question = text.trim(); if (!question || isLoading) return; track('chat-question') setInput(''); setIsLoading(true); setMessages((prev) => { const next: ChatMessage[] = [ ...prev, { role: 'user', content: question }, { role: 'assistant', content: '', streaming: true }, ]; assistantIndexRef.current = next.length - 1; return next; }); const patchAssistant = (patch: Partial) => { setMessages((prev) => { const idx = assistantIndexRef.current; if (idx === null || !prev[idx]) return prev; const updated = [...prev]; updated[idx] = { ...updated[idx], ...patch }; return updated; }); }; try { const params = new URLSearchParams({ message: question, thread_id: getThreadId() }); const response = await fetch(`/chat/stream?${params.toString()}`, { headers: { Accept: 'text/event-stream' }, }); if (!response.ok) throw new Error(`HTTP ${response.status}`); let accumulated = ''; await readSse(response, (frame) => { if (frame.error) { patchAssistant({ content: frame.error, streaming: false, error: true }); return; } if (frame.content) { accumulated += frame.content; patchAssistant({ content: accumulated }); } if (frame.done) patchAssistant({ content: accumulated || 'پاسخی دریافت نشد.', streaming: false }); }); patchAssistant({ streaming: false }); } catch (err) { patchAssistant({ content: 'سرویس گفتگو در دسترس نیست؛ لطفاً کمی بعد دوباره تلاش کنید.', streaming: false, error: true, }); } finally { setIsLoading(false); inputRef.current?.focus(); } }; return ( <> {/* دکمه شناور — گوشه راست‌پایین (دکمه تماس سمت چپ است) */} {/* پنل گفتگو */} {open && (
{/* سربرگ */}

دستیار هوشمند آرامش

پاسخ از محتوای همین سایت

{/* پیام‌ها */}
{messages.length === 0 && (

سلام! هر سؤالی درباره خدمات، پروژه‌ها و مقالات گروه ساختمانی آرامش دارید بپرسید.

)} {messages.map((msg, i) => (
{msg.content || (msg.streaming ? '…' : '')} {msg.streaming && msg.content && ( )}
))}
{/* پیشنهاد پرسش‌ها */} {suggestions.length > 0 && messages.length === 0 && (
{suggestions.map((q) => ( ))}
)} {/* ورودی */}
{ e.preventDefault(); sendMessage(input); }} className="flex items-center gap-2 border-t border-ivory/10 bg-ink px-3 py-3"> setInput(e.target.value)} placeholder="پرسش خود را بنویسید…" disabled={isLoading} className="flex-1 rounded-full border border-ivory/15 bg-char px-4 py-2 text-[13px] text-ivory placeholder:text-ivory/30 focus:border-copper/60 focus:outline-none disabled:opacity-50" />
)} ); }