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 (
Nexus Memory uses local AI via Ollama — your data stays private.
A local AI assistant that remembers you across sessions, answers questions from your documents, and runs entirely on your machine.
{suggestion}