From dd32ec50a4f8bf6350c6eccec6a62601625d53ec Mon Sep 17 00:00:00 2001 From: BiggerRain <15911122312@163.COM> Date: Thu, 20 Feb 2025 15:38:55 +0800 Subject: [PATCH] fix: some bugs in chat (#163) --- src/components/Assistant/Chat.tsx | 194 ++++++++++++---------- src/components/Assistant/ChatMessage.tsx | 28 +--- src/components/Assistant/SourceResult.tsx | 2 + src/components/Search/SearchListItem.tsx | 6 +- src/locales/en/translation.json | 2 +- src/locales/zh/translation.json | 2 +- src/pages/chat/index.tsx | 179 ++------------------ 7 files changed, 144 insertions(+), 269 deletions(-) diff --git a/src/components/Assistant/Chat.tsx b/src/components/Assistant/Chat.tsx index cebdf89..f88f89d 100644 --- a/src/components/Assistant/Chat.tsx +++ b/src/components/Assistant/Chat.tsx @@ -6,6 +6,7 @@ import { useImperativeHandle, useRef, useState, + useMemo, } from "react"; import { MessageSquarePlus, PanelLeft } from "lucide-react"; import { isTauri } from "@tauri-apps/api/core"; @@ -13,7 +14,7 @@ import { useTranslation } from "react-i18next"; import { debounce } from "lodash-es"; import { ChatMessage } from "./ChatMessage"; -import type { Chat, Message } from "./types"; +import type { Chat } from "./types"; import { tauriFetch } from "@/api/tauriFetchClient"; import { useWebSocket } from "@/hooks/useWebSocket"; import { useChatStore } from "@/stores/chatStore"; @@ -22,9 +23,11 @@ import { clientEnv } from "@/utils/env"; interface ChatAIProps { isTransitioned: boolean; - changeInput: (val: string) => void; isSearchActive?: boolean; isDeepThinkActive?: boolean; + isChatPage?: boolean; + activeChatProp?: Chat; + changeInput?: (val: string) => void; } export interface ChatAIRef { @@ -32,12 +35,20 @@ export interface ChatAIRef { cancelChat: () => void; connected: boolean; reconnect: () => void; + handleSendMessage: (value: string) => void; } const ChatAI = memo( forwardRef( ( - { isTransitioned, changeInput, isSearchActive, isDeepThinkActive }, + { + isTransitioned, + changeInput, + isSearchActive, + isDeepThinkActive, + isChatPage = false, + activeChatProp, + }, ref ) => { const { t } = useTranslation(); @@ -46,6 +57,7 @@ const ChatAI = memo( cancelChat: cancelChat, connected: connected, reconnect: reconnect, + handleSendMessage: handleSendMessage, })); const { createWin } = useWindows(); @@ -57,17 +69,18 @@ const ChatAI = memo( 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; - const curIdRef = useRef(curId); - curIdRef.current = curId; + const curIdRef = useRef(""); + + useEffect(() => { + activeChatProp && setActiveChat(activeChatProp); + }, [activeChatProp]); const handleMessageChunk = useCallback((chunk: string) => { setCurMessage((prev) => prev + chunk); @@ -83,7 +96,6 @@ const ChatAI = memo( if (msg.includes("websocket-session-id")) { const array = msg.split(" "); - setWebsocketId(array[2]); websocketIdRef.current = array[2]; return ""; } else if (msg.includes("PRIVATE")) { @@ -105,7 +117,7 @@ const ChatAI = memo( } else { const cleanedData = msg.replace(/^PRIVATE /, ""); try { - console.log("cleanedData", cleanedData); + // console.log("cleanedData", cleanedData); const chunkData = JSON.parse(cleanedData); if (chunkData.reply_to_message === curIdRef.current) { handleMessageChunk(chunkData.message_chunk); @@ -117,37 +129,41 @@ const ChatAI = memo( } } } - }, []); + }, [curChatEnd, isTyping]); const { messages, setMessages, connected, reconnect } = useWebSocket( clientEnv.COCO_WEBSOCKET_URL, dealMsg ); - const simulateAssistantResponse = useCallback(() => { - if (!activeChat?._id) return; - - // console.log("curMessage", curMessage); - - const assistantMessage: Message = { + const assistantMessage = useMemo(() => { + if (!activeChat?._id || (!curMessage && !messages)) return null; + return { _id: activeChat._id, _source: { type: "assistant", message: curMessage || messages, }, }; + }, [activeChat?._id, curMessage, messages]); - const updatedChat = { + const updatedChat = useMemo(() => { + if (!activeChat?._id || !assistantMessage) return null; + return { ...activeChat, messages: [...(activeChat.messages || []), assistantMessage], }; + }, [activeChat, assistantMessage]); + + const simulateAssistantResponse = useCallback(() => { + if (!updatedChat) return; // console.log("updatedChat:", updatedChat); setActiveChat(updatedChat); setMessages(""); setCurMessage(""); setIsTyping(false); - }, [activeChat?._id, curMessage, messages]); + }, [updatedChat]); useEffect(() => { if (curChatEnd) { @@ -182,7 +198,7 @@ const ChatAI = memo( }); console.log("_new", response); const newChat: Chat = response.data; - + setActiveChat(newChat); handleSendMessage(value, newChat); } catch (error) { @@ -199,36 +215,42 @@ const ChatAI = memo( } }; - 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": 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); - setActiveChat(updatedChat); - setIsTyping(true); - setCurChatEnd(false); - } catch (error) { - console.error("Failed to fetch user data:", error); - } - }, [JSON.stringify(activeChat), websocketId]); + 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": websocketIdRef.current, + }, + body: JSON.stringify({ message: content }), + }); + console.log("_send", response, websocketIdRef.current); + curIdRef.current = response.data[0]?._id; + + const updatedChat: Chat = { + ...newChat, + messages: [ + ...(newChat?.messages || []), + ...(response.data || []), + ], + }; + + changeInput && changeInput(""); + // console.log("updatedChat2", updatedChat); + setActiveChat(updatedChat); + setIsTyping(true); + setCurChatEnd(false); + } catch (error) { + console.error("Failed to fetch user data:", error); + } + }, + [activeChat?._id] + ); const chatClose = async () => { if (!activeChat?._id) return; @@ -302,39 +324,41 @@ const ChatAI = memo( return (
-
- - - -
+ + + + + )} {/* Chat messages */}
@@ -365,17 +389,19 @@ const ChatAI = memo( /> ) : null} - {timedoutShow ? : null} + {timedoutShow ? ( + + ) : null}
diff --git a/src/components/Assistant/ChatMessage.tsx b/src/components/Assistant/ChatMessage.tsx index 255f2ca..d7c212d 100644 --- a/src/components/Assistant/ChatMessage.tsx +++ b/src/components/Assistant/ChatMessage.tsx @@ -1,37 +1,25 @@ import { Brain, ChevronDown, ChevronUp } from "lucide-react"; -import { useState, useEffect, useRef } from "react"; +import { useState, memo } from "react"; +import { useTranslation } from "react-i18next"; import type { Message } from "./types"; import Markdown from "./Markdown"; import { formatThinkingMessage } from "@/utils/index"; import logoImg from "@/assets/icon.svg"; import { SourceResult } from "./SourceResult"; -import { useTranslation } from "react-i18next"; interface ChatMessageProps { message: Message; isTyping?: boolean; } -export function ChatMessage({ message, isTyping }: ChatMessageProps) { +export const ChatMessage = memo(function ChatMessage({ message, isTyping }: ChatMessageProps) { const { t } = useTranslation(); const [isThinkingExpanded, setIsThinkingExpanded] = useState(true); - const [responseTime, setResponseTime] = useState(0); - const startTimeRef = useRef(null); - const hasStartedRef = useRef(false); + const isAssistant = message._source?.type === "assistant"; - const segments = formatThinkingMessage(message._source.message); - useEffect(() => { - if (isTyping && !hasStartedRef.current) { - startTimeRef.current = Date.now(); - hasStartedRef.current = true; - } else if (!isTyping && hasStartedRef.current && startTimeRef.current) { - const duration = (Date.now() - startTimeRef.current) / 1000; - setResponseTime(duration); - hasStartedRef.current = false; - } - }, [isTyping]); + const segments = formatThinkingMessage(message._source.message); return (
- {t("assistant.message.thoughtTime", { - time: responseTime.toFixed(1), - })} + {t("assistant.message.thoughtTime")} )} @@ -140,4 +126,4 @@ export function ChatMessage({ message, isTyping }: ChatMessageProps) {
); -} +}) diff --git a/src/components/Assistant/SourceResult.tsx b/src/components/Assistant/SourceResult.tsx index b23541b..8cdd9ec 100644 --- a/src/components/Assistant/SourceResult.tsx +++ b/src/components/Assistant/SourceResult.tsx @@ -3,6 +3,7 @@ import { ChevronUp, ChevronDown, SquareArrowOutUpRight, + File, } from "lucide-react"; import { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -79,6 +80,7 @@ export function SourceResult({ text }: SourceResultProps) { >
+
{item.title || item.category}
diff --git a/src/components/Search/SearchListItem.tsx b/src/components/Search/SearchListItem.tsx index 8811d55..fc24057 100644 --- a/src/components/Search/SearchListItem.tsx +++ b/src/components/Search/SearchListItem.tsx @@ -37,7 +37,11 @@ const SearchListItem: React.FC = ({ : "text-[#333] dark:text-[#d8d8d8]" } ${showListRight ? "gap-7" : ""}`} > -
+
{item?.title}
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 4a7ff24..2e5743a 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -121,7 +121,7 @@ "logo": "Coco AI Logo", "aiName": "Coco AI", "thinking": "AI is thinking...", - "thoughtTime": "Thought for {{time}} seconds", + "thoughtTime": "Thought for a few seconds", "thinkingButton": "View thinking process" }, "sidebar": { diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index 8659300..7d47c58 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -121,7 +121,7 @@ "logo": "Coco AI 图标", "aiName": "Coco AI", "thinking": "AI 正在思考...", - "thoughtTime": "思考了 {{time}} 秒", + "thoughtTime": "思考了数秒", "thinkingButton": "查看思考过程" }, "sidebar": { diff --git a/src/pages/chat/index.tsx b/src/pages/chat/index.tsx index aefd18d..6e738f2 100644 --- a/src/pages/chat/index.tsx +++ b/src/pages/chat/index.tsx @@ -2,108 +2,31 @@ import { useState, useRef, useEffect } from "react"; import { PanelRightClose, PanelRightOpen, X } from "lucide-react"; import { isTauri } from "@tauri-apps/api/core"; -import { ChatMessage } from "@/components/Assistant/ChatMessage"; +import ChatAI, { ChatAIRef } from "@/components/Assistant/Chat"; import { ChatInput } from "@/components/Assistant/ChatInput"; import { Sidebar } from "@/components/Assistant/Sidebar"; -import type { Chat, Message } from "@/components/Assistant/types"; +import type { Chat } from "@/components/Assistant/types"; import { tauriFetch } from "@/api/tauriFetchClient"; -import { useWebSocket } from "@/hooks/useWebSocket"; import { useWindows } from "@/hooks/useWindows"; -import { clientEnv } from "@/utils/env"; -// import { useAppStore } from '@/stores/appStore'; import ApiDetails from "@/components/Common/ApiDetails"; -interface ChatAIProps {} +interface ChatProps {} -export default function ChatAI({}: ChatAIProps) { +export default function Chat({}: ChatProps) { const { closeWin } = useWindows(); - // const appStore = useAppStore(); + const chatAIRef = useRef(null); const [chats, setChats] = useState([]); const [activeChat, setActiveChat] = useState(); const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [isTyping, setIsTyping] = useState(false); - const messagesEndRef = useRef(null); - const [websocketId, setWebsocketId] = useState(""); - const [curMessage, setCurMessage] = useState(""); const [curChatEnd, setCurChatEnd] = useState(true); - const [curId, setCurId] = useState(""); - const [isSearchActive, setIsSearchActive] = useState(false); const [isDeepThinkActive, setIsDeepThinkActive] = useState(false); - const curChatEndRef = useRef(curChatEnd); - curChatEndRef.current = curChatEnd; - - const curIdRef = useRef(curId); - curIdRef.current = curId; - - console.log("index useWebSocket", clientEnv.COCO_WEBSOCKET_URL,) - const { messages, setMessages } = useWebSocket( - clientEnv.COCO_WEBSOCKET_URL, - (msg) => { - msg = msg.replace(//g, "AI is thinking...").replace(/<\/think>/g, ""); - - 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); - } - } - } - } - ); - - // 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, isTyping, curChatEnd]); - - // getChatHistory useEffect(() => { getChatHistory(); }, []); @@ -120,39 +43,13 @@ export default function ChatAI({}: ChatAIProps) { if (hits[0]) { onSelectChat(hits[0]); } else { - createNewChat(); + chatAIRef.current?.init(""); } } catch (error) { console.error("Failed to fetch user data:", error); } }; - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ - behavior: "smooth", - block: "end", - }); - }; - - useEffect(() => { - scrollToBottom(); - }, [activeChat?.messages, isTyping, curMessage]); - - const createNewChat = async () => { - try { - const response = await tauriFetch({ - url: "/chat/_new", - method: "POST", - }); - console.log("_new", response); - const newChat: Chat = response.data; - setChats((prev) => [newChat, ...prev]); - setActiveChat(newChat); - } catch (error) { - console.error("Failed to fetch user data:", error); - } - }; - const deleteChat = (chatId: string) => { setChats((prev) => prev.filter((chat) => chat._id !== chatId)); if (activeChat?._id === chatId) { @@ -160,35 +57,13 @@ export default function ChatAI({}: ChatAIProps) { if (remainingChats.length > 0) { setActiveChat(remainingChats[0]); } else { - createNewChat(); + chatAIRef.current?.init(""); } } }; const handleSendMessage = async (content: string) => { - if (!activeChat?._id) return; - try { - const response = await tauriFetch({ - url: `/chat/${activeChat?._id}/_send?search=${isSearchActive}&deep_thinking=${isDeepThinkActive}`, - method: "POST", - headers: { - "WEBSOCKET-SESSION-ID": websocketId, - }, - body: JSON.stringify({ message: content }), - }); - console.log("_send", response, websocketId); - setCurId(response.data[0]?._id); - const updatedChat: Chat = { - ...activeChat, - messages: [...(activeChat?.messages || []), ...(response.data || [])], - }; - - setActiveChat(updatedChat); - setIsTyping(true); - setCurChatEnd(false); - } catch (error) { - console.error("Failed to fetch user data:", error); - } + chatAIRef.current?.handleSendMessage(content); }; const chatHistory = async (chat: Chat) => { @@ -269,7 +144,7 @@ export default function ChatAI({}: ChatAIProps) { chatAIRef.current?.init("")} onSelectChat={onSelectChat} onDeleteChat={deleteChat} /> @@ -297,33 +172,15 @@ export default function ChatAI({}: ChatAIProps) { {/* Chat messages */} -
- {activeChat?.messages?.map((message, index) => ( - - ))} - {!curChatEnd && activeChat?._id ? ( - - ) : null} -
-
+ {/* Input area */}