import { AnimatePresence, motion } from "framer-motion"; import { History, Mic, Send, Sparkles, Trash2, X } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; export interface ChatMessage { id: string; role: "user" | "assistant"; content: string; ts: number; } export interface ChatSession { id: string; title: string; messages: ChatMessage[]; createdAt: number; updatedAt: number; } const STORAGE_KEY = "qhub_chat_sessions"; const ACTIVE_KEY = "qhub_active_session"; const WELCOME_MSG: ChatMessage = { id: "w", role: "assistant", content: "Welcome to QHub. The sky listens.", ts: Date.now(), }; function loadSessions(): ChatSession[] { try { const raw = localStorage.getItem(STORAGE_KEY); return raw ? (JSON.parse(raw) as ChatSession[]) : []; } catch { return []; } } function saveSessions(sessions: ChatSession[]) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(sessions)); } catch { /* quota guard */ } } function newSession(): ChatSession { return { id: crypto.randomUUID(), title: `Chat ${new Date().toLocaleString("fa-IR", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })}`, messages: [{ ...WELCOME_MSG, id: crypto.randomUUID(), ts: Date.now() }], createdAt: Date.now(), updatedAt: Date.now(), }; } interface ChatPanelProps { onSpeakingChange?: (speaking: boolean) => void; } export function ChatPanel({ onSpeakingChange }: ChatPanelProps) { const [expanded, setExpanded] = useState(false); const [historyOpen, setHistoryOpen] = useState(false); const [sessions, setSessions] = useState(() => { const saved = loadSessions(); if (saved.length > 0) return saved; const first = newSession(); saveSessions([first]); return [first]; }); const [activeId, setActiveId] = useState(() => { const saved = localStorage.getItem(ACTIVE_KEY); const all = loadSessions(); if (saved && all.find(s => s.id === saved)) return saved; return all[0]?.id ?? newSession().id; }); const activeSession = sessions.find(s => s.id === activeId) ?? sessions[0]; const messages = activeSession?.messages ?? [WELCOME_MSG]; const [draft, setDraft] = useState(""); const [thinking, setThinking] = useState(false); const [listening, setListening] = useState(false); const scrollRef = useRef(null); // Persist whenever sessions change useEffect(() => { saveSessions(sessions); }, [sessions]); useEffect(() => { localStorage.setItem(ACTIVE_KEY, activeId); }, [activeId]); useEffect(() => { scrollRef.current?.scrollTo({ top: 1e9, behavior: "smooth" }); }, [messages, expanded]); const updateActiveMessages = useCallback((updater: (prev: ChatMessage[]) => ChatMessage[]) => { setSessions(prev => prev.map(s => s.id === activeId ? { ...s, messages: updater(s.messages), updatedAt: Date.now() } : s ) ); }, [activeId]); const send = useCallback(() => { const text = draft.trim(); if (!text) return; const userMsg: ChatMessage = { id: crypto.randomUUID(), role: "user", content: text, ts: Date.now() }; updateActiveMessages(m => [...m, userMsg]); // Auto-title from first user message setSessions(prev => prev.map(s => { if (s.id !== activeId) return s; const isDefault = s.title.startsWith("Chat "); return isDefault && s.messages.filter(x => x.role === "user").length === 0 ? { ...s, title: text.slice(0, 40) + (text.length > 40 ? "…" : "") } : s; }) ); setDraft(""); setThinking(true); onSpeakingChange?.(true); setTimeout(() => { const reply: ChatMessage = { id: crypto.randomUUID(), role: "assistant", content: synthesize(text), ts: Date.now(), }; updateActiveMessages(m => [...m, reply]); setThinking(false); onSpeakingChange?.(false); }, 900 + Math.random() * 800); }, [draft, onSpeakingChange, updateActiveMessages, activeId]); const startNewSession = useCallback(() => { const s = newSession(); setSessions(prev => [s, ...prev]); setActiveId(s.id); setHistoryOpen(false); setExpanded(true); }, []); const switchSession = useCallback((id: string) => { setActiveId(id); setHistoryOpen(false); setExpanded(true); }, []); const deleteSession = useCallback((id: string) => { setSessions(prev => { const next = prev.filter(s => s.id !== id); if (next.length === 0) { const fresh = newSession(); saveSessions([fresh]); setActiveId(fresh.id); return [fresh]; } if (id === activeId) setActiveId(next[0].id); return next; }); }, [activeId]); return ( {/* ── History side-panel ── */} {historyOpen && (
تاریخچه مکالمات
{sessions.length === 0 && (

هیچ مکالمه‌ای ذخیره نشده

)} {sessions.map(s => (
switchSession(s.id)} >
{s.title}
{s.messages.length} پیام · {new Date(s.updatedAt).toLocaleDateString("fa-IR")}
))}
)}
{/* ── Messages panel ── */} {expanded && (
{activeSession?.title ?? "Live channel"}
{messages.map(m => (
{m.content}
))} {thinking && (
thinking {[0, 1, 2].map(i => ( ))}
)}
)}
{/* ── Input bar ── */}
{/* History button */} {/* Toggle messages */} setDraft(e.target.value)} onFocus={() => setExpanded(true)} onKeyDown={e => { if (e.key === "Enter") { e.preventDefault(); send(); } }} placeholder="Speak to the sky…" className="flex-1 bg-transparent outline-none text-sm placeholder:text-muted-foreground/60 py-1.5" />
{listening && }
); } function Waveform() { return (
{Array.from({ length: 24 }).map((_, i) => ( ))}
); } function synthesize(input: string) { const seeds = [ "Acknowledged. Routing intent through the cognitive mesh.", "Signal received. Constellations realigned to your query.", "Processed. The orbital memory has been updated.", "Done. A satellite carries your request beyond the haze.", ]; return `${seeds[Math.floor(Math.random() * seeds.length)]} ${input.length > 60 ? "" : "Anything else?"}`; }