import { useState, useEffect, useRef, useCallback } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Send, Brain, Upload, Paperclip, Zap, ChevronDown, Menu, X, } from "lucide-react"; import MessageBubble from "./MessageBubble"; import TypingIndicator from "./TypingIndicator"; import UploadSection from "./UploadSection"; import MemoryPanel from "./MemoryPanel"; import ModelSelector from "./ModelSelector"; import { useChat } from "../hooks/useChat"; import { getSessionMessages, getMemories } from "../services/api"; export default function ChatWindow({ sessionId, onNewSession, onSidebarToggle, }) { const [input, setInput] = useState(""); const [selectedModel, setSelectedModel] = useState("llama3"); const [showUpload, setShowUpload] = useState(false); const [showMemory, setShowMemory] = useState(false); const [memories, setMemories] = useState([]); const [showScrollButton, setShowScrollButton] = useState(false); const [isLoadingHistory, setIsLoadingHistory] = useState(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const textareaRef = useRef(null); const { messages, setMessages, isStreaming, streamingContent, error, sendMessage, } = useChat(sessionId); // Load session history when session changes useEffect(() => { if (sessionId) { loadSessionHistory(); loadMemories(); } else { setMessages([]); } }, [sessionId]); // Auto-scroll on new messages useEffect(() => { if (!showScrollButton) { scrollToBottom(); } }, [messages, streamingContent]); const loadSessionHistory = async () => { setIsLoadingHistory(true); try { const data = await getSessionMessages(sessionId); setMessages(data); } catch (err) { console.error("Failed to load history:", err); } finally { setIsLoadingHistory(false); } }; const loadMemories = async () => { if (!sessionId) return; try { const data = await getMemories(sessionId); setMemories(data.memories || []); } catch (err) { console.error("Failed to load memories:", err); } }; const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; const handleScroll = () => { const container = messagesContainerRef.current; if (!container) return; const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight; setShowScrollButton(distanceFromBottom > 200); }; const handleSend = async () => { if (!input.trim() || isStreaming) return; const message = input.trim(); setInput(""); // Auto-resize textarea back to single line if (textareaRef.current) { textareaRef.current.style.height = "auto"; } // If no session exists yet, create one NOW and pass it directly let activeSessionId = sessionId; if (!activeSessionId) { activeSessionId = onNewSession(); console.log("Created new session:", activeSessionId); } // Pass activeSessionId as override so sendMessage uses it immediately // (React state update for sessionId may not have propagated yet) await sendMessage(message, selectedModel, activeSessionId); // Refresh memories after a short delay setTimeout(() => loadMemories(), 2500); }; const handleKeyDown = (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } }; const handleTextareaChange = (e) => { setInput(e.target.value); // Auto-resize const ta = e.target; ta.style.height = "auto"; ta.style.height = Math.min(ta.scrollHeight, 160) + "px"; }; // ── Welcome Screen ────────────────────────────────────────────────────────── if (!sessionId && messages.length === 0) { return (
{}} />
); } return (
{/* Main Chat Area */}
{/* Top Bar */} {/* Messages Area */}
{isLoadingHistory ? (
Loading history...
) : ( <> {/* Message list */} {messages.map((message) => ( ))} {/* Streaming message */} {isStreaming && streamingContent && ( )} {/* Typing indicator (before first token) */} {isStreaming && !streamingContent && } {/* Scroll anchor */}
)}
{/* Scroll to bottom button */} {showScrollButton && ( )} {/* Upload Panel (collapsible above input) */} {showUpload && ( loadMemories()} /> )} {/* Input Bar */} loadMemories()} />
{/* Memory Side Panel */} {showMemory && sessionId && ( setShowMemory(false)} onMemoryChange={loadMemories} /> )}
); } // ── Sub-components ───────────────────────────────────────────────────────────── function TopBar({ onSidebarToggle, selectedModel, setSelectedModel, showMemory, setShowMemory, memoryCount, sessionId, }) { return (
{/* Left side */}
{/* Mobile sidebar toggle */}
Nexus Memory
{/* Right side — fixed width, no overflow */}
{/* Memory toggle */} {sessionId && ( )} {/* Model selector — always visible, dropdown-aware */}
); } function InputBar({ input, setInput, onSend, isStreaming, onKeyDown, textareaRef, showUpload, setShowUpload, sessionId, onUploadComplete, }) { return (
{/* Upload toggle */} {/* Textarea */}