"use client"; import { useState, useEffect, useRef } from "react"; import { ModelDropdown } from "@/components/model/ModelDropdown"; import { ModelPullDialog } from "@/components/model/ModelPullDialog"; import { ChatMessage, ChatInput } from "@/components/chat/ChatMessage"; import { SystemPrompt } from "@/components/chat/SystemPrompt"; import type { ModelInfo } from "@/lib/models"; import type { Message } from "@/lib/chat"; import { createMessage } from "@/lib/chat"; import { useUserPrefs, buildOllamaOptions } from "@/hooks/useUserPrefs"; interface ChatContainerProps { models: ModelInfo[]; offline: boolean; } export function ChatContainer({ models, offline }: ChatContainerProps) { const { prefs, ready } = useUserPrefs(); const [messages, setMessages] = useState([]); const [selectedModel, setSelectedModel] = useState(null); const [isStreaming, setIsStreaming] = useState(false); const [streamingContent, setStreamingContent] = useState(""); const abortRef = useRef(null); const [showPullDialog, setShowPullDialog] = useState(false); const [modelToPull, setModelToPull] = useState(""); const [systemPrompt, setSystemPrompt] = useState(""); const messagesEndRef = useRef(null); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; useEffect(() => { scrollToBottom(); }, [messages, streamingContent]); const handleSendMessage = async (content: string) => { if (!selectedModel || isStreaming) return; const userMessage = createMessage("user", content); const newMessages = [...messages, userMessage]; // Add system prompt if it exists and this is the first user message const messagesToSend = systemPrompt && messages.length === 0 ? [createMessage("system", systemPrompt), ...newMessages] : newMessages; setMessages(newMessages); setIsStreaming(true); setStreamingContent(""); try { const options = buildOllamaOptions( prefs.device, prefs.numGpu, prefs.gpuIndex ); abortRef.current = new AbortController(); const response = await fetch("/api/ollama/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: selectedModel, messages: messagesToSend, options, system: systemPrompt || undefined, }), signal: abortRef.current.signal, }); if (!response.ok) { throw new Error("Failed to send message"); } const reader = response.body?.getReader(); if (!reader) throw new Error("No response body"); const decoder = new TextDecoder(); let buffer = ""; let assistantContent = ""; while (true) { 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) { if (!line.trim()) continue; try { const data = JSON.parse(line); if (data.token) { assistantContent += data.token; setStreamingContent(assistantContent); } if (data.done) { const assistantMessage = createMessage( "assistant", assistantContent ); setMessages((prev) => [...prev, assistantMessage]); setStreamingContent(""); } if (data.error) { throw new Error(data.error); } } catch (e) { console.error("Error parsing stream:", e); } } } } catch (error) { console.error("Chat error:", error); const errorMessage = createMessage( "assistant", "Sorry, I encountered an error. Please try again." ); setMessages((prev) => [...prev, errorMessage]); } finally { setIsStreaming(false); setStreamingContent(""); abortRef.current = null; } }; const handleStop = () => { if (abortRef.current && isStreaming) { abortRef.current.abort(); } }; const handleSelectModel = (modelId: string) => { const model = models.find((m) => m.id === modelId); if (model) { setSelectedModel(modelId); // Optional: clear chat when switching models // setMessages([]); } }; const handlePullModel = async (modelName: string) => { try { const response = await fetch("/api/ollama/pull", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: modelName }), }); if (!response.ok) { throw new Error("Failed to pull model"); } const reader = response.body?.getReader(); if (!reader) throw new Error("No response body"); const decoder = new TextDecoder(); let buffer = ""; while (true) { 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) { if (!line.trim()) continue; try { const data = JSON.parse(line); if (data.status === "success") { // Model pulled successfully, refresh models list window.location.reload(); } } catch (e) { console.error("Error parsing pull stream:", e); } } } } catch (error) { throw error; } }; const selectedModelData = models.find((m) => m.id === selectedModel); return (
{/* Sidebar */}

Models

{offline && (

Ollama is offline. Using mock data.

)} {selectedModelData && (

Selected Model

Name: {selectedModelData.name}
Size: {selectedModelData.sizeGB} GB
Quantization: {selectedModelData.quantization}
Device: {selectedModelData.device}
)}
{/* Chat Area */}
{/* Chat Header */}

{selectedModelData ? `Chat with ${selectedModelData.name}` : "Select a model to start chatting"}

{/* Messages */}
{messages.length === 0 ? (

Welcome to Ollahub

Select a model and start chatting!

) : ( <> {messages.map((message) => ( ))} {isStreaming && streamingContent && ( )}
)}
{/* Input */}
{/* Pull Dialog */} setShowPullDialog(false)} modelName={modelToPull} onConfirmPull={handlePullModel} />
); }