import { useState, useRef, useEffect } from "react"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import Markdown from "react-markdown"; import { Message, ProviderConfig } from "../types"; interface ChatProps { projectPath: string; onCloseProject: () => void; } export function Chat({ projectPath, onCloseProject }: ChatProps) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); const [model, setModel] = useState("llama3.1"); // Default local model const [enableTools, setEnableTools] = useState(true); const [availableModels, setAvailableModels] = useState([]); const messagesEndRef = useRef(null); useEffect(() => { invoke("get_ollama_models") .then(async (models) => { if (models.length > 0) { setAvailableModels(models); // Check backend store for saved model try { const savedModel = await invoke( "get_model_preference", ); if (savedModel && models.includes(savedModel)) { setModel(savedModel); } else if (!models.includes(model)) { setModel(models[0]); } } catch (e) { console.error(e); } } }) .catch((err) => console.error(err)); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { const unlistenPromise = listen("chat:update", (event) => { setMessages(event.payload); }); return () => { unlistenPromise.then((unlisten) => unlisten()); }; }, []); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; useEffect(scrollToBottom, [messages]); const sendMessage = async () => { if (!input.trim() || loading) return; const userMsg: Message = { role: "user", content: input }; const newHistory = [...messages, userMsg]; setMessages(newHistory); setInput(""); setLoading(true); try { const config: ProviderConfig = { provider: "ollama", model: model, base_url: "http://localhost:11434", enable_tools: enableTools, }; // Invoke backend chat command // We rely on 'chat:update' events to update the state in real-time await invoke("chat", { messages: newHistory, config: config, }); } catch (e) { console.error(e); setMessages((prev) => [ ...prev, { role: "assistant", content: `**Error:** ${e}` }, ]); } finally { setLoading(false); } }; return (
{/* Sticky Header */}
{/* Project Info */}
{projectPath}
{/* Model Controls */}
{availableModels.length > 0 ? ( ) : ( { const newModel = e.target.value; setModel(newModel); invoke("set_model_preference", { model: newModel }).catch( console.error, ); }} placeholder="Model" style={{ padding: "6px 12px", borderRadius: "99px", border: "none", fontSize: "0.9em", background: "#2f2f2f", color: "#ececec", outline: "none", }} /> )}
{/* Messages Area */}
{messages.map((msg, idx) => (
{msg.role === "user" ? ( msg.content ) : msg.role === "tool" ? (
Tool Output
{msg.content}
) : (
{/* Assuming global CSS handles standard markdown styling now */} {msg.content}
)} {/* Show Tool Calls if present */} {msg.tool_calls && (
{msg.tool_calls.map((tc, i) => (
Running: {tc.function.name}
))}
)}
))} {loading && (
Thinking...
)}
{/* Input Area */}
setInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && sendMessage()} placeholder="Send a message..." style={{ width: "100%", padding: "14px 20px", paddingRight: "50px", // space for button borderRadius: "24px", border: "1px solid #333", outline: "none", fontSize: "1rem", fontWeight: "500", background: "#2f2f2f", color: "#ececec", boxShadow: "0 2px 6px rgba(0,0,0,0.02)", }} />
); }