diff --git a/docs/content.en/docs/release-notes/_index.md b/docs/content.en/docs/release-notes/_index.md index 0fbe1b82..f4135e8c 100644 --- a/docs/content.en/docs/release-notes/_index.md +++ b/docs/content.en/docs/release-notes/_index.md @@ -59,6 +59,8 @@ Information about release notes of Coco Server is provided here. - fix: server image loading failure #534 - chore: skip register server that not logged in #536 - refactor: service info related components #537 +- fix: service switching error #539 +- chore: chat content can be copied #539 ## 0.4.0 (2025-04-27) diff --git a/src-tauri/src/assistant/mod.rs b/src-tauri/src/assistant/mod.rs index c4eb4467..da5a5e7a 100644 --- a/src-tauri/src/assistant/mod.rs +++ b/src-tauri/src/assistant/mod.rs @@ -141,6 +141,8 @@ pub async fn new_chat( let body_text = common::http::get_response_body_text(response).await?; + log::debug!("New chat response: {}", &body_text); + let chat_response: GetResponse = serde_json::from_str(&body_text).map_err(|e| format!("Failed to parse response JSON: {}", e))?; @@ -248,8 +250,8 @@ pub async fn assistant_search( None, Some(reqwest::Body::from(body.to_string())), ) - .await - .map_err(|e| format!("Error searching assistants: {}", e))?; + .await + .map_err(|e| format!("Error searching assistants: {}", e))?; response .json::() diff --git a/src-tauri/src/server/websocket.rs b/src-tauri/src/server/websocket.rs index 4d480301..87107c73 100644 --- a/src-tauri/src/server/websocket.rs +++ b/src-tauri/src/server/websocket.rs @@ -95,8 +95,8 @@ pub async fn connect_to_server( true, // disable_nagle Some(connector), // Connector ) - .await - .map_err(|e| format!("WebSocket TLS error: {:?}", e))?; + .await + .map_err(|e| format!("WebSocket TLS error: {:?}", e))?; let (cancel_tx, mut cancel_rx) = mpsc::channel(1); @@ -125,6 +125,7 @@ pub async fn connect_to_server( let _ = app_handle_clone.emit(&format!("ws-message-{}", client_id_clone), text); }, Some(Err(_)) | None => { + log::debug!("WebSocket connection closed or error"); let _ = app_handle_clone.emit(&format!("ws-error-{}", client_id_clone), id.clone()); break; } @@ -132,7 +133,8 @@ pub async fn connect_to_server( } } _ = cancel_rx.recv() => { - let _ = app_handle_clone.emit(&format!("ws-error-{}", client_id_clone), id.clone()); + log::debug!("WebSocket connection cancelled"); + let _ = app_handle_clone.emit(&format!("ws-cancel-{}", client_id_clone), id.clone()); break; } } diff --git a/src/components/Assistant/AssistantList.tsx b/src/components/Assistant/AssistantList.tsx index cdc13ee8..3514b06e 100644 --- a/src/components/Assistant/AssistantList.tsx +++ b/src/components/Assistant/AssistantList.tsx @@ -7,6 +7,16 @@ import { ChevronRight, } from "lucide-react"; import { useTranslation } from "react-i18next"; +import { isNil } from "lodash-es"; +import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; +import { + useAsyncEffect, + useDebounce, + useKeyPress, + usePagination, + useReactive, +} from "ahooks"; +import clsx from "clsx"; import { useAppStore } from "@/stores/appStore"; import logoImg from "@/assets/icon.svg"; @@ -17,18 +27,8 @@ import FontIcon from "@/components/Common/Icons/FontIcon"; import { useChatStore } from "@/stores/chatStore"; import { useShortcutsStore } from "@/stores/shortcutsStore"; import { Post } from "@/api/axiosRequest"; -import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; -import { - useAsyncEffect, - useDebounce, - useKeyPress, - usePagination, - useReactive, -} from "ahooks"; -import clsx from "clsx"; import NoDataImage from "../Common/NoDataImage"; import PopoverInput from "../Common/PopoverInput"; -import { isNil } from "lodash-es"; interface AssistantListProps { assistantIDs?: string[]; @@ -41,6 +41,7 @@ interface State { export function AssistantList({ assistantIDs = [] }: AssistantListProps) { const { t } = useTranslation(); const { connected } = useChatStore(); + const isTauri = useAppStore((state) => state.isTauri); const setAssistantList = useConnectStore((state) => state.setAssistantList); const currentService = useConnectStore((state) => state.currentService); diff --git a/src/components/Assistant/Chat.tsx b/src/components/Assistant/Chat.tsx index 632e5240..6405781e 100644 --- a/src/components/Assistant/Chat.tsx +++ b/src/components/Assistant/Chat.tsx @@ -134,7 +134,6 @@ const ChatAI = memo( clientId, connected, setConnected, - currentService, dealMsgRef, onWebsocketSessionId, }); @@ -165,7 +164,8 @@ const ChatAI = memo( isMCPActive, changeInput, websocketSessionId, - showChatHistory + showChatHistory, + isLogin ); const { dealMsg } = useMessageHandler( @@ -330,7 +330,6 @@ const ChatAI = memo( isSidebarOpen={isSidebarOpenChat} chats={chats} activeChat={activeChat} - // onNewChat={clearChat} onSelectChat={onSelectChat} onDeleteChat={deleteChat} fetchChatHistory={getChatHistory} @@ -340,7 +339,7 @@ const ChatAI = memo( )} +
void; + clearChat: () => void; onOpenChatAI: () => void; setIsSidebarOpen: () => void; isSidebarOpen: boolean; @@ -31,7 +31,7 @@ interface ChatHeaderProps { } export function ChatHeader({ - onCreateNewChat, + clearChat, onOpenChatAI, isSidebarOpen, setIsSidebarOpen, @@ -99,10 +99,10 @@ export function ChatHeader({ {showChatHistory ? ( @@ -131,7 +131,7 @@ export function ChatHeader({ isLogin={isLogin} setIsLogin={setIsLogin} reconnect={reconnect} - onCreateNewChat={onCreateNewChat} + clearChat={clearChat} /> {isChatPage ? null : ( diff --git a/src/components/Assistant/ChatSidebar.tsx b/src/components/Assistant/ChatSidebar.tsx index 4aac6f7f..e091eeea 100644 --- a/src/components/Assistant/ChatSidebar.tsx +++ b/src/components/Assistant/ChatSidebar.tsx @@ -1,6 +1,5 @@ import React from "react"; -// import { Sidebar } from "@/components/Assistant/Sidebar"; import type { Chat } from "@/types/chat"; import HistoryList from "../Common/HistoryList"; import { HISTORY_PANEL_ID } from "@/constants"; @@ -9,7 +8,6 @@ interface ChatSidebarProps { isSidebarOpen: boolean; chats: Chat[]; activeChat?: Chat; - // onNewChat: () => void; onSelectChat: (chat: any) => void; onDeleteChat: (chatId: string) => void; fetchChatHistory: () => void; @@ -21,7 +19,6 @@ export const ChatSidebar: React.FC = ({ isSidebarOpen, chats, activeChat, - // onNewChat, onSelectChat, onDeleteChat, fetchChatHistory, @@ -52,14 +49,6 @@ export const ChatSidebar: React.FC = ({ onRemove={onDeleteChat} /> )} - {/* */}
); }; diff --git a/src/components/Assistant/ServerList.tsx b/src/components/Assistant/ServerList.tsx index 3c274418..41f3e565 100644 --- a/src/components/Assistant/ServerList.tsx +++ b/src/components/Assistant/ServerList.tsx @@ -20,14 +20,14 @@ interface ServerListProps { isLogin: boolean; setIsLogin: (isLogin: boolean) => void; reconnect: (server?: IServer) => void; - onCreateNewChat: () => void; + clearChat: () => void; } export function ServerList({ isLogin, setIsLogin, reconnect, - onCreateNewChat, + clearChat, }: ServerListProps) { const { t } = useTranslation(); @@ -110,12 +110,13 @@ export function ServerList({ setCurrentService(server); setEndpoint(server.endpoint); setMessages(""); // Clear previous messages - onCreateNewChat(); + clearChat(); // if (!server.public && !server.profile) { setIsLogin(false); return; } + // setIsLogin(true); // The Rust backend will automatically disconnect, // so we don't need to handle disconnection on the frontend diff --git a/src/components/Assistant/Sidebar.tsx b/src/components/Assistant/Sidebar.tsx deleted file mode 100644 index efdf26d3..00000000 --- a/src/components/Assistant/Sidebar.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { MessageSquare, Plus, RefreshCw } from "lucide-react"; - -import type { Chat } from "@/types/chat"; - -interface SidebarProps { - chats: Chat[]; - activeChat: Chat | undefined; - onNewChat: () => void; - onSelectChat: (chat: Chat) => void; - onDeleteChat: (chatId: string) => void; - className?: string; - fetchChatHistory: () => void; -} - -export function Sidebar({ - chats, - activeChat, - onNewChat, - onSelectChat, - className = "", - fetchChatHistory, -}: SidebarProps) { - const { t } = useTranslation(); - - const [isRefreshing, setIsRefreshing] = useState(false); - - return ( -
-
- - -
-
- {chats.map((chat) => ( -
- - {activeChat?._id === chat._id && ( -
- )} -
- ))} -
-
- ); -} diff --git a/src/hooks/useChatActions.ts b/src/hooks/useChatActions.ts index 13d07383..f67d3f3c 100644 --- a/src/hooks/useChatActions.ts +++ b/src/hooks/useChatActions.ts @@ -22,7 +22,8 @@ export function useChatActions( isMCPActive?: boolean, changeInput?: (val: string) => void, websocketSessionId?: string, - showChatHistory?: boolean + showChatHistory?: boolean, + isLogin?: boolean, ) { const isTauri = useAppStore((state) => state.isTauri); const addError = useAppStore((state) => state.addError); @@ -315,8 +316,11 @@ export function useChatActions( const getChatHistory = useCallback(async () => { let response: any; if (isTauri) { + if (!currentServiceId || !isLogin) { + setChats([]); + return + } - if (!currentServiceId) return []; response = await platformAdapter.commands("chat_history", { serverId: currentServiceId, from: 0, @@ -334,7 +338,6 @@ export function useChatActions( } console.log("_history", response); const hits = response?.hits?.hits || []; - setChats(hits); }, [currentServiceId, keyword, isTauri]); diff --git a/src/hooks/useWebSocket.ts b/src/hooks/useWebSocket.ts index ccd62330..f33c24a7 100644 --- a/src/hooks/useWebSocket.ts +++ b/src/hooks/useWebSocket.ts @@ -4,6 +4,7 @@ import { useWebSocket as useWebSocketAHook } from "ahooks"; import { useAppStore } from "@/stores/appStore"; import platformAdapter from "@/utils/platformAdapter"; import { Server } from "@/types/server"; +import { useConnectStore } from "@/stores/connectStore"; enum ReadyState { Connecting = 0, @@ -16,7 +17,6 @@ interface WebSocketProps { clientId: string; connected: boolean; setConnected: (connected: boolean) => void; - currentService: Server | null; dealMsgRef: React.MutableRefObject<((msg: string) => void) | null>; onWebsocketSessionId?: (sessionId: string) => void; } @@ -25,7 +25,6 @@ export default function useWebSocket({ clientId, connected, setConnected, - currentService, dealMsgRef, onWebsocketSessionId, }: WebSocketProps) { @@ -33,6 +32,8 @@ export default function useWebSocket({ const endpoint_websocket = useAppStore((state) => state.endpoint_websocket); const addError = useAppStore((state) => state.addError); + const currentService = useConnectStore((state) => state.currentService); + const websocketIdRef = useRef(""); const messageQueue = useRef([]); const processingRef = useRef(false); @@ -58,6 +59,7 @@ export default function useWebSocket({ connect(); // web } }, [isTauri, connect]); + const processMessage = useCallback( (msg: string) => { try { @@ -103,6 +105,7 @@ export default function useWebSocket({ // src/components/Search/InputBox.tsx const reconnect = useCallback( async (server?: Server) => { + setConnected(false); // Disconnect before attempting to reconnect if (isTauri) { const targetServer = server || currentService; if (!targetServer?.id) return; @@ -117,7 +120,7 @@ export default function useWebSocket({ connect(); } }, - [currentService] + [currentService, clientId] ); const disconnectWS = useCallback(async () => { if (!connected) return; @@ -140,23 +143,13 @@ export default function useWebSocket({ }, [dealMsgRef] ); + + const unlistenErrorRef = useRef void> | null>(null); + const unlistenCancelRef = useRef void> | null>(null); useEffect(() => { - if (!currentService?.id) return; + if (!isTauri || !currentService?.id) return; - let unlisten_error = null; - let unlisten_message = null; - - if (!isTauri) return; - - unlisten_error = platformAdapter.listenEvent(`ws-error-${clientId}`, (event) => { - console.error(`ws-error-${clientId}`, event, connected); - if (connected) { - addError("WebSocket connection failed."); - } - setConnected(false); // error - }); - - unlisten_message = platformAdapter.listenEvent(`ws-message-${clientId}`, (event) => { + const unlisten_message = platformAdapter.listenEvent(`ws-message-${clientId}`, (event) => { const msg = event.payload as string; // console.log(`ws-message-${clientId}`, msg); if (msg.includes("websocket-session-id")) { @@ -167,16 +160,37 @@ export default function useWebSocket({ if (onWebsocketSessionId) { onWebsocketSessionId(sessionId); } + // Listen for errors + unlistenErrorRef.current = platformAdapter.listenEvent(`ws-error-${clientId}`, (event) => { + if (connected) { + const id = event.payload as string; + console.error(`ws-error-${clientId}`, id === currentService?.id, connected); + if (id === currentService?.id) { + addError("WebSocket connection failed."); + } + } + setConnected(false); // error + }); + // Listen for cancel + unlistenCancelRef.current = platformAdapter.listenEvent(`ws-cancel-${clientId}`, () => { + setConnected(false); + }); + return; } - dealMsgRef.current && dealMsgRef.current(msg); + dealMsgRef.current?.(msg); }); return () => { - unlisten_error?.then((fn: any) => fn()); - unlisten_message?.then((fn: any) => fn()); + unlisten_message.then((fn) => fn()); + if (unlistenErrorRef.current) { + unlistenErrorRef.current.then((fn) => fn()); + } + if (unlistenCancelRef.current) { + unlistenCancelRef.current.then((fn) => fn()); + } }; - }, [dealMsgRef]); + }, [currentService?.id, dealMsgRef, connected, clientId]); return { reconnect, disconnectWS, updateDealMsg }; } \ No newline at end of file diff --git a/src/main.css b/src/main.css index 23690395..36e1b630 100644 --- a/src/main.css +++ b/src/main.css @@ -241,4 +241,13 @@ -ms-user-select: none; user-select: none; } + + .user-select-text { + -webkit-touch-callout: text; + -webkit-user-select: text; + -khtml-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; + } } diff --git a/src/pages/chat/index.tsx b/src/pages/chat/index.tsx index d9883228..e4672f5d 100644 --- a/src/pages/chat/index.tsx +++ b/src/pages/chat/index.tsx @@ -348,9 +348,7 @@ export default function Chat({}: ChatProps) {
{/* Input area */} -
+