diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index 854935d..2a3800d 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -7,623 +7,623 @@ import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism"; import type { Message, ProviderConfig } from "../types"; interface ChatProps { - projectPath: string; - onCloseProject: () => void; + 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 [streamingContent, setStreamingContent] = useState(""); - const messagesEndRef = useRef(null); - const inputRef = useRef(null); + 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 [streamingContent, setStreamingContent] = useState(""); + const messagesEndRef = useRef(null); + const inputRef = useRef(null); - useEffect(() => { - invoke("get_ollama_models") - .then(async (models) => { - if (models.length > 0) { - setAvailableModels(models); + 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 - }, [model]); + // 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 + }, [model]); - useEffect(() => { - const unlistenUpdatePromise = listen("chat:update", (event) => { - setMessages(event.payload); - setStreamingContent(""); // Clear streaming content when final update arrives - }); + useEffect(() => { + const unlistenUpdatePromise = listen("chat:update", (event) => { + setMessages(event.payload); + setStreamingContent(""); // Clear streaming content when final update arrives + }); - const unlistenTokenPromise = listen("chat:token", (event) => { - setStreamingContent((prev) => prev + event.payload); - }); + const unlistenTokenPromise = listen("chat:token", (event) => { + setStreamingContent((prev) => prev + event.payload); + }); - return () => { - unlistenUpdatePromise.then((unlisten) => unlisten()); - unlistenTokenPromise.then((unlisten) => unlisten()); - }; - }, []); + return () => { + unlistenUpdatePromise.then((unlisten) => unlisten()); + unlistenTokenPromise.then((unlisten) => unlisten()); + }; + }, []); - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }; + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }; - useEffect(scrollToBottom, []); + useEffect(scrollToBottom, [messages, streamingContent]); - useEffect(() => { - inputRef.current?.focus(); - }, []); + useEffect(() => { + inputRef.current?.focus(); + }, []); - const sendMessage = async () => { - if (!input.trim() || loading) return; + const sendMessage = async () => { + if (!input.trim() || loading) return; - const userMsg: Message = { role: "user", content: input }; - const newHistory = [...messages, userMsg]; + const userMsg: Message = { role: "user", content: input }; + const newHistory = [...messages, userMsg]; - setMessages(newHistory); - setInput(""); - setLoading(true); - setStreamingContent(""); // Clear any previous streaming content + setMessages(newHistory); + setInput(""); + setLoading(true); + setStreamingContent(""); // Clear any previous streaming content - try { - const config: ProviderConfig = { - provider: "ollama", - model: model, - base_url: "http://localhost:11434", - enable_tools: enableTools, - }; + 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); - } - }; + // 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); + } + }; - const clearSession = () => { - const confirmed = window.confirm( - "Are you sure? This will clear all messages and reset the conversation context.", - ); - if (confirmed) { - setMessages([]); - setStreamingContent(""); - setLoading(false); - // TODO: Add backend call to clear context when implemented - // invoke("clear_session").catch(console.error); - } - }; + const clearSession = () => { + const confirmed = window.confirm( + "Are you sure? This will clear all messages and reset the conversation context.", + ); + if (confirmed) { + setMessages([]); + setStreamingContent(""); + setLoading(false); + // TODO: Add backend call to clear context when implemented + // invoke("clear_session").catch(console.error); + } + }; - return ( -
- {/* Sticky Header */} -
- {/* Project Info */} -
-
- {projectPath} -
- -
+ 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", - }} - /> - )} - -
-
+ {/* 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.tool_call_id && ` (${msg.tool_call_id})`} - - -
-											{msg.content}
-										
-
- ) : ( -
- { - const match = /language-(\w+)/.exec(className || ""); - const isInline = !className; - return !isInline && match ? ( - - {String(children).replace(/\n$/, "")} - - ) : ( - - {children} - - ); - }, - }} - > - {msg.content} - -
- )} + {/* Messages Area */} +
+
+ {messages.map((msg, idx) => ( +
+
+ {msg.role === "user" ? ( + msg.content + ) : msg.role === "tool" ? ( +
+ + + + Tool Output + {msg.tool_call_id && ` (${msg.tool_call_id})`} + + +
+                      {msg.content}
+                    
+
+ ) : ( +
+ { + const match = /language-(\w+)/.exec(className || ""); + const isInline = !className; + return !isInline && match ? ( + + {String(children).replace(/\n$/, "")} + + ) : ( + + {children} + + ); + }, + }} + > + {msg.content} + +
+ )} - {/* Show Tool Calls if present */} - {msg.tool_calls && ( -
- {msg.tool_calls.map((tc, i) => { - // Parse arguments to extract key info - let argsSummary = ""; - try { - const args = JSON.parse(tc.function.arguments); - const firstKey = Object.keys(args)[0]; - if (firstKey && args[firstKey]) { - argsSummary = String(args[firstKey]); - // Truncate if too long - if (argsSummary.length > 50) { - argsSummary = `${argsSummary.substring(0, 47)}...`; - } - } - } catch (_e) { - // If parsing fails, just show empty - } + {/* Show Tool Calls if present */} + {msg.tool_calls && ( +
+ {msg.tool_calls.map((tc, i) => { + // Parse arguments to extract key info + let argsSummary = ""; + try { + const args = JSON.parse(tc.function.arguments); + const firstKey = Object.keys(args)[0]; + if (firstKey && args[firstKey]) { + argsSummary = String(args[firstKey]); + // Truncate if too long + if (argsSummary.length > 50) { + argsSummary = `${argsSummary.substring(0, 47)}...`; + } + } + } catch (_e) { + // If parsing fails, just show empty + } - return ( -
- - - {tc.function.name} - {argsSummary && `(${argsSummary})`} - -
- ); - })} -
- )} -
-
- ))} - {loading && streamingContent && ( -
-
- { - const match = /language-(\w+)/.exec(className || ""); - const isInline = !className; - return !isInline && match ? ( - - {String(children).replace(/\n$/, "")} - - ) : ( - - {children} - - ); - }, - }} - > - {streamingContent} - -
-
- )} - {loading && !streamingContent && ( -
- Thinking... -
- )} -
-
-
+ return ( +
+ + + {tc.function.name} + {argsSummary && `(${argsSummary})`} + +
+ ); + })} +
+ )} +
+
+ ))} + {loading && streamingContent && ( +
+
+ { + const match = /language-(\w+)/.exec(className || ""); + const isInline = !className; + return !isInline && match ? ( + + {String(children).replace(/\n$/, "")} + + ) : ( + + {children} + + ); + }, + }} + > + {streamingContent} + +
+
+ )} + {loading && !streamingContent && ( +
+ 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)", - }} - /> - -
-
-
- ); + {/* 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)", + }} + /> + +
+
+
+ ); }