fix: service switching error (#539)

* fix: service switching error

* build: build error

* chore: chat content can be copied

* docs: update notes

* fix: service switching error

* chore: change to send cancel event to ws_cancel

* chore: add ws-cancel

---------

Co-authored-by: medcl <m@medcl.net>
This commit is contained in:
BiggerRain
2025-05-21 09:04:57 +08:00
committed by GitHub
parent 2b59addb08
commit 5ef06bfc95
16 changed files with 98 additions and 167 deletions

View File

@@ -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)

View File

@@ -141,6 +141,8 @@ pub async fn new_chat<R: Runtime>(
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<R: Runtime>(
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::<Value>()

View File

@@ -95,8 +95,8 @@ pub async fn connect_to_server<R: Runtime>(
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<R: Runtime>(
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<R: Runtime>(
}
}
_ = 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;
}
}

View File

@@ -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);

View File

@@ -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(
)}
<ChatHeader
onCreateNewChat={clearChat}
clearChat={clearChat}
onOpenChatAI={openChatAI}
setIsSidebarOpen={toggleSidebar}
isSidebarOpen={isSidebarOpenChat}

View File

@@ -96,7 +96,7 @@ export const ChatContent = ({
};
return (
<div className="flex-1 overflow-hidden flex flex-col justify-between relative">
<div className="flex-1 overflow-hidden flex flex-col justify-between relative user-select-text">
<div
ref={scrollRef}
className="flex-1 w-full overflow-x-hidden overflow-y-auto border-t border-[rgba(0,0,0,0.1)] dark:border-[rgba(255,255,255,0.15)] custom-scrollbar relative"

View File

@@ -17,7 +17,7 @@ import { Server } from "@/types/server"
interface ChatHeaderProps {
onCreateNewChat: () => 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 ? (
<button
onClick={onCreateNewChat}
onClick={clearChat}
className="p-2 py-1 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
>
<VisibleKey shortcut={newSession} onKeyPress={onCreateNewChat}>
<VisibleKey shortcut={newSession} onKeyPress={clearChat}>
<MessageSquarePlus className="h-4 w-4 relative top-0.5" />
</VisibleKey>
</button>
@@ -131,7 +131,7 @@ export function ChatHeader({
isLogin={isLogin}
setIsLogin={setIsLogin}
reconnect={reconnect}
onCreateNewChat={onCreateNewChat}
clearChat={clearChat}
/>
{isChatPage ? null : (

View File

@@ -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<ChatSidebarProps> = ({
isSidebarOpen,
chats,
activeChat,
// onNewChat,
onSelectChat,
onDeleteChat,
fetchChatHistory,
@@ -52,14 +49,6 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
onRemove={onDeleteChat}
/>
)}
{/* <Sidebar
chats={chats}
activeChat={activeChat}
onNewChat={onNewChat}
onSelectChat={onSelectChat}
onDeleteChat={onDeleteChat}
fetchChatHistory={fetchChatHistory}
/> */}
</div>
);
};

View File

@@ -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

View File

@@ -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 (
<div className={`h-full flex flex-col ${className}`}>
<div className="flex justify-between gap-1 p-4">
<button
onClick={onNewChat}
className={`flex items-center gap-3 px-4 py-3 text-sm font-medium rounded-xl transition-all border border-[#E6E6E6] dark:border-[#272626] text-gray-700 hover:bg-gray-50/80 active:bg-gray-100/80 dark:text-white dark:hover:bg-gray-600/50 dark:active:bg-gray-500/50`}
>
<Plus className={`h-4 w-4 text-[#0072FF] dark:text-[#0072FF]`} />
{t("assistant.sidebar.newChat")}
</button>
<button
onClick={async () => {
setIsRefreshing(true);
fetchChatHistory();
setTimeout(() => setIsRefreshing(false), 1000);
}}
className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400"
disabled={isRefreshing}
>
<RefreshCw
className={`h-4 w-4 text-[#0287FF] transition-transform duration-1000 ${
isRefreshing ? "animate-spin" : ""
}`}
/>
</button>
</div>
<div className="flex-1 overflow-y-auto px-3 pb-3 space-y-2 custom-scrollbar">
{chats.map((chat) => (
<div
key={chat._id}
className={`group relative rounded-xl transition-all ${
activeChat?._id === chat._id
? "bg-gray-100/80 dark:bg-gray-700/50"
: "hover:bg-gray-50/80 dark:hover:bg-gray-600/30"
}`}
>
<button
className="w-full flex items-center gap-3 px-4 py-3 text-sm text-left"
onClick={() => onSelectChat(chat)}
>
<MessageSquare
className={`h-4 w-4 flex-shrink-0 ${
activeChat?._id === chat._id
? "text-[#0072FF] dark:text-[#0072FF]"
: "text-gray-400 dark:text-gray-500"
}`}
/>
<span
className={`truncate ${
activeChat?._id === chat._id
? "text-gray-900 dark:text-white font-medium"
: "text-gray-600 dark:text-gray-300"
}`}
>
{chat?._source?.title || chat?._id}
</span>
</button>
{activeChat?._id === chat._id && (
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 rounded-full bg-[#0072FF]" />
)}
</div>
))}
</div>
</div>
);
}

View File

@@ -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]);

View File

@@ -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<string>("");
const messageQueue = useRef<string[]>([]);
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<Promise<() => void> | null>(null);
const unlistenCancelRef = useRef<Promise<() => 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 };
}

View File

@@ -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;
}
}

View File

@@ -348,9 +348,7 @@ export default function Chat({}: ChatProps) {
</div>
{/* Input area */}
<div
className={`border-t p-4 pb-0 border-gray-200 dark:border-gray-800`}
>
<div className={`border-t p-2 border-gray-200 dark:border-gray-800`}>
<InputBox
isChatMode={true}
inputValue={input}

View File

@@ -29,13 +29,9 @@ export interface EventPayloads {
datasourceData_change: void;
"ws-error": void;
"ws-message": void;
[key: `ws-error-${string}`]: {
error: {
reason: string;
};
status: number;
};
[key: `ws-error-${string}`]: string;
[key: `ws-message-${string}`]: string;
[key: `ws-cancel-${string}`]: string;
"change-startup-store": IStartupStore;
"change-shortcuts-store": IShortcutsStore;
"change-connect-store": IConnectStore;

View File

@@ -729,4 +729,13 @@ body,
-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;
}
}