diff --git a/.vscode/settings.json b/.vscode/settings.json index 5d2f1146..65b850a0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -34,6 +34,7 @@ "tailwindcss", "tauri", "thiserror", + "timedout", "titlebar", "tpddns", "traptitech", @@ -53,5 +54,6 @@ }, "i18n-ally.localesPaths": [ "src/locales" - ] + ], + "i18n-ally.keystyle": "nested" } \ No newline at end of file diff --git a/package.json b/package.json index 70e22199..175ae19b 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "dotenv": "^16.4.7", "i18next": "^23.16.2", "i18next-browser-languagedetector": "^8.0.3", - "lodash": "^4.17.21", + "lodash-es": "^4.17.21", "lucide-react": "^0.461.0", "mermaid": "^11.4.0", "react": "^18.2.0", @@ -49,7 +49,7 @@ }, "devDependencies": { "@tauri-apps/cli": "^2.2.7", - "@types/lodash": "^4.17.12", + "@types/lodash-es": "^4.17.12", "@types/markdown-it": "^14.1.2", "@types/node": "^22.8.4", "@types/react": "^18.2.15", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 09bd36c0..3fefc4c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,7 +62,7 @@ importers: i18next-browser-languagedetector: specifier: ^8.0.3 version: 8.0.3 - lodash: + lodash-es: specifier: ^4.17.21 version: 4.17.21 lucide-react: @@ -120,7 +120,7 @@ importers: '@tauri-apps/cli': specifier: ^2.2.7 version: 2.2.7 - '@types/lodash': + '@types/lodash-es': specifier: ^4.17.12 version: 4.17.12 '@types/markdown-it': @@ -875,6 +875,9 @@ packages: '@types/linkify-it@5.0.0': resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + '@types/lodash@4.17.12': resolution: {integrity: sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ==} @@ -2997,6 +3000,10 @@ snapshots: '@types/linkify-it@5.0.0': {} + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.12 + '@types/lodash@4.17.12': {} '@types/markdown-it@14.1.2': diff --git a/src/components/Assistant/Chat.tsx b/src/components/Assistant/Chat.tsx index 21325050..cebdf896 100644 --- a/src/components/Assistant/Chat.tsx +++ b/src/components/Assistant/Chat.tsx @@ -10,6 +10,7 @@ import { import { MessageSquarePlus, PanelLeft } from "lucide-react"; import { isTauri } from "@tauri-apps/api/core"; import { useTranslation } from "react-i18next"; +import { debounce } from "lodash-es"; import { ChatMessage } from "./ChatMessage"; import type { Chat, Message } from "./types"; @@ -18,7 +19,6 @@ 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 { isTransitioned: boolean; @@ -48,20 +48,21 @@ const ChatAI = memo( reconnect: reconnect, })); - // const appStore = useAppStore(); - const { createWin } = useWindows(); const { curChatEnd, setCurChatEnd, setConnected } = useChatStore(); const [activeChat, setActiveChat] = useState(); const [isTyping, setIsTyping] = useState(false); + const [timedoutShow, setTimedoutShow] = useState(false); const messagesEndRef = useRef(null); const [websocketId, setWebsocketId] = useState(""); const [curMessage, setCurMessage] = useState(""); const [curId, setCurId] = useState(""); + const websocketIdRef = useRef(""); + const curChatEndRef = useRef(curChatEnd); curChatEndRef.current = curChatEnd; @@ -72,53 +73,67 @@ const ChatAI = memo( setCurMessage((prev) => prev + chunk); }, []); - // console.log("chat useWebSocket", clientEnv.COCO_WEBSOCKET_URL) - const { messages, setMessages, connected, reconnect } = useWebSocket( - clientEnv.COCO_WEBSOCKET_URL, - (msg) => { - // console.log("msg", msg); + const messageTimeoutRef = useRef(); - if (msg.includes("websocket-session-id")) { - const array = msg.split(" "); - setWebsocketId(array[2]); - } + const dealMsg = useCallback((msg: string) => { + // console.log("msg:", msg); + if (messageTimeoutRef.current) { + clearTimeout(messageTimeoutRef.current); + } - 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) { - handleMessageChunk(chunkData.message_chunk); - return chunkData.message_chunk; - } - } catch (error) { - console.error("JSON Parse error:", error); + if (msg.includes("websocket-session-id")) { + const array = msg.split(" "); + setWebsocketId(array[2]); + websocketIdRef.current = array[2]; + return ""; + } else if (msg.includes("PRIVATE")) { + messageTimeoutRef.current = setTimeout(() => { + if (!curChatEnd && isTyping) { + console.log("AI response timeout"); + setTimedoutShow(true); + cancelChat(); + } + }, 30000); + + if (msg.includes("assistant finished output")) { + if (messageTimeoutRef.current) { + clearTimeout(messageTimeoutRef.current); + } + // console.log("AI finished output"); + simulateAssistantResponse(); + setCurChatEnd(true); + } else { + const cleanedData = msg.replace(/^PRIVATE /, ""); + try { + console.log("cleanedData", cleanedData); + const chunkData = JSON.parse(cleanedData); + if (chunkData.reply_to_message === curIdRef.current) { + handleMessageChunk(chunkData.message_chunk); + setMessages((prev) => prev + chunkData.message_chunk); + return chunkData.message_chunk; } + } catch (error) { + console.error("parse error:", error); } } } + }, []); + + const { messages, setMessages, connected, reconnect } = useWebSocket( + clientEnv.COCO_WEBSOCKET_URL, + dealMsg ); - useEffect(() => { - setConnected(connected); - }, [connected]); - const simulateAssistantResponse = useCallback(() => { - if (messages.length === 0 || !activeChat?._id) return; + if (!activeChat?._id) return; - console.log("messages", messages); + // console.log("curMessage", curMessage); const assistantMessage: Message = { _id: activeChat._id, _source: { type: "assistant", - message: messages, + message: curMessage || messages, }, }; @@ -126,44 +141,38 @@ const ChatAI = memo( ...activeChat, messages: [...(activeChat.messages || []), assistantMessage], }; + + // console.log("updatedChat:", updatedChat); + setActiveChat(updatedChat); setMessages(""); setCurMessage(""); - console.log("updatedChat", updatedChat); - setActiveChat(updatedChat); + setIsTyping(false); + }, [activeChat?._id, curMessage, messages]); - const timer = setTimeout(() => setIsTyping(false), 1000); - return () => clearTimeout(timer); - }, [activeChat?._id]); - - // websocket useEffect(() => { if (curChatEnd) { simulateAssistantResponse(); } - }, [messages, curChatEnd]); + }, [curChatEnd]); - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ - behavior: "smooth", - block: "end", - }); - }; + useEffect(() => { + setConnected(connected); + }, [connected]); + + const scrollToBottom = useCallback( + debounce(() => { + messagesEndRef.current?.scrollIntoView({ + behavior: "smooth", + block: "end", + }); + }, 100), + [] + ); useEffect(() => { scrollToBottom(); }, [activeChat?.messages, isTyping, curMessage]); - useEffect(() => { - return () => { - chatClose(); - setMessages(""); - setCurMessage(""); - setActiveChat(undefined); - setIsTyping(false); - setCurChatEnd(true); - }; - }, []); - const createNewChat = useCallback(async (value: string = "") => { chatClose(); try { @@ -173,7 +182,7 @@ const ChatAI = memo( }); console.log("_new", response); const newChat: Chat = response.data; - + setActiveChat(newChat); handleSendMessage(value, newChat); } catch (error) { @@ -190,33 +199,36 @@ const ChatAI = memo( } }; - const handleSendMessage = async (content: string, newChat?: Chat) => { + const handleSendMessage = useCallback(async (content: string, newChat?: Chat) => { newChat = newChat || activeChat; if (!newChat?._id || !content) return; + setTimedoutShow(false); try { const response = await tauriFetch({ url: `/chat/${newChat?._id}/_send?search=${isSearchActive}&deep_thinking=${isDeepThinkActive}`, method: "POST", headers: { - "WEBSOCKET-SESSION-ID": websocketId, + "WEBSOCKET-SESSION-ID": websocketIdRef.current || websocketId }, body: JSON.stringify({ message: content }), }); console.log("_send", response, websocketId); setCurId(response.data[0]?._id); + const updatedChat: Chat = { ...newChat, messages: [...(newChat?.messages || []), ...(response.data || [])], }; + changeInput(""); - console.log("updatedChat2", updatedChat); + // console.log("updatedChat2", updatedChat); setActiveChat(updatedChat); setIsTyping(true); setCurChatEnd(false); } catch (error) { console.error("Failed to fetch user data:", error); } - }; + }, [JSON.stringify(activeChat), websocketId]); const chatClose = async () => { if (!activeChat?._id) return; @@ -232,6 +244,10 @@ const ChatAI = memo( }; const cancelChat = async () => { + if (curMessage || messages) { + simulateAssistantResponse(); + } + setCurChatEnd(true); setIsTyping(false); if (!activeChat?._id) return; @@ -266,6 +282,21 @@ const ChatAI = memo( } } + useEffect(() => { + return () => { + if (messageTimeoutRef.current) { + clearTimeout(messageTimeoutRef.current); + } + chatClose(); + setMessages(""); + setCurMessage(""); + setActiveChat(undefined); + setIsTyping(false); + setCurChatEnd(true); + scrollToBottom.cancel(); + }; + }, []); + if (!isTransitioned) return null; return ( @@ -295,7 +326,19 @@ const ChatAI = memo( {/* Chat messages */} -
+
+ + {activeChat?.messages?.map((message, index) => ( ))} + {!curChatEnd && activeChat?._id ? ( ) : null} - {!connected && ( -
- {t("assistant.chat.connectionError")} -
- {t("assistant.chat.reconnect")} -
-
- )} + + {timedoutShow ? : null} +
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 0c9c79f5..4a7ff24a 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -21,7 +21,7 @@ "title": "Language", "description": "Choose your preferred language", "english": "English", - "chinese": "中文" + "chinese": "简体中文" }, "tooltip": { "title": "Tooltip", @@ -106,7 +106,9 @@ "openChat": "Open Chat Window", "newChat": "New Chat", "connectionError": "Unable to connect to the server", - "reconnect": "Reconnect" + "reconnect": "Reconnect", + "greetings": "Hi! I’m Coco, nice to meet you. I can help answer your questions by tapping into the internet and your data sources. How can I assist you today?", + "timedout": "Request timed out. Please try again later." }, "input": { "stopMessage": "Stop message", diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index 2f174b44..8659300e 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -21,7 +21,7 @@ "title": "语言", "description": "选择您的首选语言", "english": "English", - "chinese": "中文" + "chinese": "简体中文" }, "tooltip": { "title": "提示", @@ -60,7 +60,7 @@ }, "search": { "textarea": { - "placeholder": "问我任何问题...", + "placeholder": "尝试问我任何问题...", "ariaLabel": "输入你想问的问题" }, "document": { @@ -89,7 +89,7 @@ "connectionError": "无法连接到服务器", "reconnect": "重新连接", "deepThink": "深度思考", - "search": "搜索" + "search": "联网搜索" }, "main": { "noDataAlt": "无数据图片", @@ -106,13 +106,15 @@ "openChat": "打开聊天窗口", "newChat": "新建对话", "connectionError": "无法连接到服务器", - "reconnect": "重新连接" + "reconnect": "重新连接", + "greetings": "嗨!我是 Coco,很高兴认识你。我可以利用互联网和你的数据源来回答你的问题。我今天能为你提供什么帮助?", + "timedout": "请求超时,请稍后再试。" }, "input": { "stopMessage": "停止生成", "deepThink": "深度思考", "deepThinkTooltip": "启用深度思考模式", - "search": "搜索", + "search": "联网搜索", "searchTooltip": "启用搜索模式" }, "message": { diff --git a/vite.config.ts b/vite.config.ts index dd684570..9d058f33 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -59,7 +59,6 @@ export default defineConfig(async () => ({ output: { manualChunks: { vendor: ['react', 'react-dom'], - lodash: ['lodash'], katex: ['rehype-katex'], highlight: ['rehype-highlight'], mermaid: ['mermaid'],