import { useState, useRef, useEffect, forwardRef, useImperativeHandle, } from "react"; import { MessageSquarePlus, PanelLeft } from "lucide-react"; import { isTauri } from "@tauri-apps/api/core"; import { ChatMessage } from "./ChatMessage"; import type { Chat, Message } from "./types"; import { tauriFetch } from "../../api/tauriFetchClient"; import { useWebSocket } from "../../hooks/useWebSocket"; import { useChatStore } from "../../stores/chatStore"; import { useWindows } from "../../hooks/useWindows"; import { clientEnv } from "@/utils/env"; import { useAppStore } from '@/stores/appStore'; interface ChatAIProps { inputValue: string; isTransitioned: boolean; changeInput: (val: string) => void; } export interface ChatAIRef { init: () => void; cancelChat: () => void; connected: boolean; reconnect: () => void; } const ChatAI = forwardRef( ({ inputValue, isTransitioned, changeInput }, ref) => { useImperativeHandle(ref, () => ({ init: init, cancelChat: cancelChat, connected: connected, reconnect: reconnect })); const appStore = useAppStore(); const { createWin } = useWindows(); const { curChatEnd, setCurChatEnd, setConnected } = useChatStore(); const [activeChat, setActiveChat] = useState(); const [isTyping, setIsTyping] = useState(false); const messagesEndRef = useRef(null); const [websocketId, setWebsocketId] = useState(""); const [curMessage, setCurMessage] = useState(""); const [curId, setCurId] = useState(""); const curChatEndRef = useRef(curChatEnd); curChatEndRef.current = curChatEnd; const curIdRef = useRef(curId); curIdRef.current = curId; console.log("chat useWebSocket", clientEnv.COCO_WEBSOCKET_URL) const { messages, setMessages, connected, reconnect } = useWebSocket( `${appStore.endpoint_websocket || clientEnv.COCO_WEBSOCKET_URL}`, (msg) => { console.log("msg", msg); if (msg.includes("websocket-session-id")) { const array = msg.split(" "); setWebsocketId(array[2]); } if (msg.includes("PRIVATE")) { if ( msg.includes("assistant finished output") || curChatEndRef.current ) { setCurChatEnd(true); } else { const cleanedData = msg.replace(/^PRIVATE /, ""); try { const chunkData = JSON.parse(cleanedData); if (chunkData.reply_to_message === curIdRef.current) { setCurMessage((prev) => prev + chunkData.message_chunk); return chunkData.message_chunk; } } catch (error) { console.error("JSON Parse error:", error); } } } } ); useEffect(()=>{ setConnected(connected) }, [connected]) // websocket useEffect(() => { if (messages.length === 0 || !activeChat?._id) return; const simulateAssistantResponse = () => { console.log("messages", messages); const assistantMessage: Message = { _id: activeChat._id, _source: { type: "assistant", message: messages, }, }; const updatedChat = { ...activeChat, messages: [...(activeChat.messages || []), assistantMessage], }; setMessages(""); setCurMessage(""); setActiveChat(updatedChat); setTimeout(() => setIsTyping(false), 1000); }; if (curChatEnd) { simulateAssistantResponse(); } }, [messages, curChatEnd]); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth", block: "end", }); }; useEffect(() => { scrollToBottom(); }, [activeChat?.messages, isTyping, curMessage]); const createNewChat = async () => { chatClose(); try { const response = await tauriFetch({ url: "/chat/_new", method: "POST", baseURL: appStore.endpoint_http, }); console.log("_new", response); const newChat: Chat = response.data; setActiveChat(newChat); handleSendMessage(inputValue, newChat); } catch (error) { console.error("Failed to fetch user data:", error); } }; const init = () => { if (!curChatEnd) return; if (!activeChat?._id) { createNewChat(); } else { handleSendMessage(inputValue); } }; const handleSendMessage = async (content: string, newChat?: Chat) => { newChat = newChat || activeChat; if (!newChat?._id || !content) return; try { const response = await tauriFetch({ url: `/chat/${newChat?._id}/_send`, method: "POST", headers: { "WEBSOCKET-SESSION-ID": websocketId, }, body: JSON.stringify({ message: content }), baseURL: appStore.endpoint_http, }); console.log("_send", response, websocketId); setCurId(response.data[0]?._id); const updatedChat: Chat = { ...newChat, messages: [...(newChat?.messages || []), ...(response.data || [])], }; changeInput(""); setActiveChat(updatedChat); setIsTyping(true); setCurChatEnd(false); } catch (error) { console.error("Failed to fetch user data:", error); } }; const chatClose = async () => { if (!activeChat?._id) return; try { const response = await tauriFetch({ url: `/chat/${activeChat._id}/_close`, method: "POST", baseURL: appStore.endpoint_http, }); console.log("_close", response); } catch (error) { console.error("Failed to fetch user data:", error); } }; const cancelChat = async () => { setCurChatEnd(true); setIsTyping(false); if (!activeChat?._id) return; try { const response = await tauriFetch({ url: `/chat/${activeChat._id}/_cancel`, method: "POST", baseURL: appStore.endpoint_http, }); console.log("_cancel", response); } catch (error) { console.error("Failed to fetch user data:", error); } }; async function openChatAI() { if (isTauri()) { createWin && createWin({ label: "chat", title: "Coco Chat", dragDropEnabled: true, center: true, width: 900, height: 800, alwaysOnTop: true, skipTaskbar: true, decorations: true, closable: true, url: "/ui/chat", }); } } if (!isTransitioned) return null; return (
{/* Chat messages */}
{activeChat?.messages?.map((message, index) => ( ))} {!curChatEnd && activeChat?._id ? ( ) : null}
); } ); export default ChatAI;