fix: switch server assistant and session session unchanged (#540)

* fix: switch server assistant and session session unchanged

* docs: update notes
This commit is contained in:
BiggerRain
2025-05-21 11:34:03 +08:00
committed by GitHub
parent 5ef06bfc95
commit ef8cd569e4
9 changed files with 248 additions and 229 deletions

View File

@@ -29,17 +29,21 @@ Information about release notes of Coco Server is provided here.
### 🐛 Bug fix
- fix: solve the problem of modifying the assistant in the chat #476
- fix: several issues around search #502
- fix: fixed the newly created session has no title when it is deleted #511
- fix: loading chat history for potential empty attachments
- fix: datasource & MCP list synchronization update #521
- fix: app icon & category icon #529
- fix: show only enabled datasource & MCP list
- fix: server image loading failure #534
- fix: panic when fetching app metadata on Windows #538
- fix: service switching error #539
- fix: switch server assistant and session session unchanged #540
### ✈️ Improvements
- chore: adjust list error message #475
- fix: solve the problem of modifying the assistant in the chat #476
- chore: refine wording on search failure
- choresearch and MCP show hidden logic #494
- chore: greetings show hidden logic #496
@@ -54,12 +58,9 @@ Information about release notes of Coco Server is provided here.
- refactor: optimizing list styles in markdown content #520
- feat: add a component for text reading aloud #522
- style: history component styles #528
- fix: app icon & category icon #529
- style: search error styles #533
- 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

@@ -0,0 +1,118 @@
import { useRef } from "react";
import { Post } from "@/api/axiosRequest";
import platformAdapter from "@/utils/platformAdapter";
import { useConnectStore } from "@/stores/connectStore";
import { useAppStore } from "@/stores/appStore";
interface AssistantFetcherProps {
debounceKeyword?: string;
assistantIDs?: string[];
}
export const AssistantFetcher = ({
debounceKeyword = "",
assistantIDs = [],
}: AssistantFetcherProps) => {
const isTauri = useAppStore((state) => state.isTauri);
const currentService = useConnectStore((state) => state.currentService);
const currentAssistant = useConnectStore((state) => state.currentAssistant);
const setCurrentAssistant = useConnectStore((state) => {
return state.setCurrentAssistant;
});
const lastServerId = useRef<string | null>(null);
const fetchAssistant = async (params: {
current: number;
pageSize: number;
}) => {
try {
const { pageSize, current } = params;
const from = (current - 1) * pageSize;
const size = pageSize;
let response: any;
const body: Record<string, any> = {
serverId: currentService?.id,
from,
size,
};
body.query = {
bool: {
must: [{ term: { enabled: true } }],
},
};
if (debounceKeyword) {
body.query.bool.must.push({
query_string: {
fields: ["combined_fulltext"],
query: debounceKeyword,
fuzziness: "AUTO",
fuzzy_prefix_length: 2,
fuzzy_max_expansions: 10,
fuzzy_transpositions: true,
allow_leading_wildcard: false,
},
});
}
if (assistantIDs.length > 0) {
body.query.bool.must.push({
terms: {
id: assistantIDs.map((id) => id),
},
});
}
if (isTauri) {
if (!currentService?.id) {
throw new Error("currentService is undefined");
}
response = await platformAdapter.commands("assistant_search", body);
} else {
const [error, res] = await Post(`/assistant/_search`, body);
if (error) {
throw new Error(error);
}
response = res;
}
let assistantList = response?.hits?.hits ?? [];
console.log("assistantList", assistantList);
if (
!currentAssistant?._id ||
currentService?.id !== lastServerId.current
) {
setCurrentAssistant(assistantList[0]);
}
lastServerId.current = currentService?.id;
return {
total: response.hits.total.value,
list: assistantList,
};
} catch (error) {
setCurrentAssistant(null);
console.error("assistant_search", error);
return {
total: 0,
list: [],
};
}
};
return { fetchAssistant };
};

View File

@@ -1,4 +1,4 @@
import { useState, useRef, useCallback, useMemo } from "react";
import { useState, useRef, useCallback } from "react";
import {
ChevronDownIcon,
RefreshCw,
@@ -10,40 +10,28 @@ 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";
import platformAdapter from "@/utils/platformAdapter";
import VisibleKey from "@/components/Common/VisibleKey";
import { useConnectStore } from "@/stores/connectStore";
import FontIcon from "@/components/Common/Icons/FontIcon";
import { useChatStore } from "@/stores/chatStore";
import { useShortcutsStore } from "@/stores/shortcutsStore";
import { Post } from "@/api/axiosRequest";
import NoDataImage from "../Common/NoDataImage";
import PopoverInput from "../Common/PopoverInput";
import NoDataImage from "@/components/Common/NoDataImage";
import PopoverInput from "@/components/Common/PopoverInput";
import { AssistantFetcher } from "./AssistantFetcher";
interface AssistantListProps {
assistantIDs?: string[];
}
interface State {
allAssistants: any[];
}
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);
const currentAssistant = useConnectStore((state) => state.currentAssistant);
const setCurrentAssistant = useConnectStore((state) => {
@@ -57,130 +45,15 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
const searchInputRef = useRef<HTMLInputElement>(null);
const [keyword, setKeyword] = useState("");
const debounceKeyword = useDebounce(keyword, { wait: 500 });
const state = useReactive<State>({
allAssistants: [],
const { fetchAssistant } = AssistantFetcher({
debounceKeyword,
assistantIDs,
});
const currentServiceId = useMemo(() => {
return currentService?.id;
}, [connected, currentService?.id]);
const fetchAssistant = async (params: {
current: number;
pageSize: number;
}) => {
try {
const { pageSize, current } = params;
const from = (current - 1) * pageSize;
const size = pageSize;
let response: any;
const body: Record<string, any> = {
serverId: currentServiceId,
from,
size,
};
body.query = {
bool: {
must: [{ term: { enabled: true } }],
},
};
if (debounceKeyword) {
body.query.bool.must.push({
query_string: {
fields: ["combined_fulltext"],
query: debounceKeyword,
fuzziness: "AUTO",
fuzzy_prefix_length: 2,
fuzzy_max_expansions: 10,
fuzzy_transpositions: true,
allow_leading_wildcard: false,
},
});
}
if (assistantIDs.length > 0) {
body.query.bool.must.push({
terms: {
id: assistantIDs.map((id) => id),
},
});
}
if (isTauri) {
if (!currentServiceId) {
throw new Error("currentServiceId is undefined");
}
response = await platformAdapter.commands("assistant_search", body);
} else {
const [error, res] = await Post(`/assistant/_search`, body);
if (error) {
throw new Error(error);
}
response = res;
}
let assistantList = response?.hits?.hits ?? [];
console.log("assistantList", assistantList);
for (const item of assistantList) {
const index = state.allAssistants.findIndex((allItem: any) => {
return item._id === allItem._id;
});
if (index === -1) {
state.allAssistants.push(item);
} else {
state.allAssistants[index] = item;
}
}
//console.log("state.allAssistants", state.allAssistants);
const matched = state.allAssistants.find((item: any) => {
return item._id === currentAssistant?._id;
});
//console.log("matched", matched);
if (matched) {
setCurrentAssistant(matched);
} else {
setCurrentAssistant(assistantList[0]);
}
return {
total: response.hits.total.value,
list: assistantList,
};
} catch (error) {
setCurrentAssistant(null);
console.error("assistant_search", error);
return {
total: 0,
list: [],
};
}
};
useAsyncEffect(async () => {
const data = await fetchAssistant({ current: 1, pageSize: 1000 });
setAssistantList(data.list);
}, [currentServiceId]);
const { pagination, runAsync } = usePagination(fetchAssistant, {
defaultPageSize: 5,
refreshDeps: [currentServiceId, debounceKeyword],
refreshDeps: [currentService?.id, debounceKeyword],
onSuccess(data) {
setAssistants(data.list);
},

View File

@@ -75,7 +75,6 @@ const ChatAI = memo(
const { curChatEnd, setCurChatEnd, connected, setConnected } =
useChatStore();
const currentService = useConnectStore((state) => state.currentService);
const visibleStartPage = useConnectStore((state) => {
return state.visibleStartPage;
});
@@ -151,7 +150,6 @@ const ChatAI = memo(
handleRename,
handleDelete,
} = useChatActions(
currentService?.id,
setActiveChat,
setCurChatEnd,
setTimedoutShow,
@@ -366,6 +364,7 @@ const ChatAI = memo(
loadingStep={loadingStep}
timedoutShow={timedoutShow}
Question={Question}
assistantIDs={assistantIDs}
handleSendMessage={(value) =>
handleSendMessage(value, activeChat)
}

View File

@@ -1,7 +1,5 @@
import { useRef, useEffect, UIEvent, useState } from "react";
import { useTranslation } from "react-i18next";
import { ArrowDown } from "lucide-react";
import clsx from "clsx";
import { ChatMessage } from "@/components/ChatMessage";
import { Greetings } from "./Greetings";
@@ -9,10 +7,10 @@ import FileList from "@/components/Assistant/FileList";
import { useChatScroll } from "@/hooks/useChatScroll";
import { useChatStore } from "@/stores/chatStore";
import type { Chat, IChunkData } from "@/types/chat";
// import SessionFile from "./SessionFile";
import { useConnectStore } from "@/stores/connectStore";
import SessionFile from "./SessionFile";
import Splash from "./Splash";
import ScrollToBottom from "@/components/Common/ScrollToBottom";
interface ChatContentProps {
activeChat?: Chat;
@@ -27,6 +25,7 @@ interface ChatContentProps {
loadingStep?: Record<string, boolean>;
timedoutShow: boolean;
Question: string;
assistantIDs?: string[];
handleSendMessage: (content: string, newChat?: Chat) => void;
getFileUrl: (path: string) => string;
}
@@ -44,6 +43,7 @@ export const ChatContent = ({
loadingStep,
timedoutShow,
Question,
assistantIDs,
handleSendMessage,
getFileUrl,
}: ChatContentProps) => {
@@ -173,24 +173,9 @@ export const ChatContent = ({
{sessionId && <SessionFile sessionId={sessionId} />}
<Splash />
<Splash assistantIDs={assistantIDs}/>
<button
className={clsx(
"absolute right-4 bottom-4 flex items-center justify-center size-8 border bg-white rounded-full shadow dark:border-[#272828] dark:bg-black dark:shadow-white/15",
{
hidden: isAtBottom,
}
)}
onClick={() => {
scrollRef.current?.scrollTo({
top: scrollRef.current?.scrollHeight,
behavior: "smooth",
});
}}
>
<ArrowDown className="size-5" />
</button>
<ScrollToBottom scrollRef={scrollRef} isAtBottom={isAtBottom} />
</div>
);
};

View File

@@ -114,6 +114,7 @@ export function ChatHeader({
activeChat?._source?.message ||
activeChat?._id}
</h2>
{isTauri ? (
<div className="flex items-center gap-2">
<button

View File

@@ -1,14 +1,14 @@
import { useMemo, useState } from "react";
import { useMemo, useState, useEffect } from "react";
import { CircleX, MoveRight } from "lucide-react";
import { useMount } from "ahooks";
import { useAppStore } from "@/stores/appStore";
import platformAdapter from "@/utils/platformAdapter";
import { useConnectStore } from "@/stores/connectStore";
import { useThemeStore } from "@/stores/themeStore";
import FontIcon from "../Common/Icons/FontIcon";
import FontIcon from "@/components/Common/Icons/FontIcon";
import logoImg from "@/assets/icon.svg";
import { Get } from "@/api/axiosRequest";
import { AssistantFetcher } from "./AssistantFetcher";
interface StartPage {
enabled?: boolean;
@@ -28,7 +28,11 @@ export interface Response {
};
}
const Splash = () => {
interface SplashProps {
assistantIDs?: string[];
}
const Splash = ({ assistantIDs = [] }: SplashProps) => {
const isTauri = useAppStore((state) => state.isTauri);
const currentService = useConnectStore((state) => state.currentService);
const [settings, setSettings] = useState<StartPage>();
@@ -36,49 +40,52 @@ const Splash = () => {
const setVisibleStartPage = useConnectStore((state) => {
return state.setVisibleStartPage;
});
const addError = useAppStore((state) => state.addError);
const isDark = useThemeStore((state) => state.isDark);
const assistantList = useConnectStore((state) => state.assistantList);
const setAssistantList = useConnectStore((state) => state.setAssistantList);
const setCurrentAssistant = useConnectStore((state) => {
return state.setCurrentAssistant;
});
useMount(async () => {
try {
const serverId = currentService.id;
let response: Response = {};
if (isTauri) {
response = await platformAdapter.invokeBackend<Response>(
"get_system_settings",
{
serverId,
}
);
} else {
const [err, result] = await Get("/settings");
if (err) {
throw new Error(err);
}
response = result as Response;
}
const settings = response?.app_settings?.chat?.start_page;
setVisibleStartPage(Boolean(settings?.enabled));
setSettings(settings);
} catch (error) {
addError(String(error), "error");
}
const { fetchAssistant } = AssistantFetcher({
assistantIDs,
});
const settingsAssistantList = useMemo(() => {
//console.log("assistantList", assistantList);
const fetchData = async () => {
const data = await fetchAssistant({ current: 1, pageSize: 1000 });
setAssistantList(data.list || []);
};
const getSettings = async () => {
const serverId = currentService.id;
let response: Response = {};
if (isTauri) {
response = await platformAdapter.invokeBackend<Response>(
"get_system_settings",
{
serverId,
}
);
} else {
const [err, result] = await Get("/settings");
if (err) {
setSettings(undefined);
}
response = result as Response;
}
const settings = response?.app_settings?.chat?.start_page;
setVisibleStartPage(Boolean(settings?.enabled));
setSettings(settings);
};
useEffect(() => {
getSettings();
fetchData();
}, [currentService?.id]);
const settingsAssistantList = useMemo(() => {
return assistantList.filter((item) => {
return settings?.display_assistants?.includes(item?._source?.id);
});

View File

@@ -0,0 +1,34 @@
import { RefObject } from "react";
import clsx from "clsx";
import { ArrowDown } from "lucide-react";
interface ScrollToBottomProps {
scrollRef: RefObject<HTMLDivElement>;
isAtBottom: boolean;
}
const ScrollToBottom = ({
scrollRef,
isAtBottom,
}: ScrollToBottomProps) => {
return (
<button
className={clsx(
"absolute right-4 bottom-4 flex items-center justify-center size-8 border bg-white rounded-full shadow dark:border-[#272828] dark:bg-black dark:shadow-white/15",
{
hidden: isAtBottom,
}
)}
onClick={() => {
scrollRef.current?.scrollTo({
top: scrollRef.current?.scrollHeight,
behavior: "smooth",
});
}}
>
<ArrowDown className="size-5" />
</button>
);
};
export default ScrollToBottom;

View File

@@ -9,7 +9,6 @@ import { useChatStore } from "@/stores/chatStore";
import { useSearchStore } from "@/stores/searchStore";
export function useChatActions(
currentServiceId: string | undefined,
setActiveChat: (chat: Chat | undefined) => void,
setCurChatEnd: (value: boolean) => void,
setTimedoutShow: (value: boolean) => void,
@@ -34,6 +33,7 @@ export function useChatActions(
const setVisibleStartPage = useConnectStore((state) => {
return state.setVisibleStartPage;
});
const currentService = useConnectStore((state) => state.currentService);
const [keyword, setKeyword] = useState("");
@@ -43,9 +43,9 @@ export function useChatActions(
let response: any;
if (isTauri) {
if (!currentServiceId) return;
if (!currentService?.id) return;
response = await platformAdapter.commands("close_session_chat", {
serverId: currentServiceId,
serverId: currentService?.id,
sessionId: activeChat?._id,
});
response = response ? JSON.parse(response) : null;
@@ -59,7 +59,7 @@ export function useChatActions(
console.log("_close", response);
},
[currentServiceId, isTauri]
[currentService?.id, isTauri]
);
const cancelChat = useCallback(
@@ -68,9 +68,9 @@ export function useChatActions(
if (!activeChat?._id) return;
let response: any;
if (isTauri) {
if (!currentServiceId) return;
if (!currentService?.id) return;
response = await platformAdapter.commands("cancel_session_chat", {
serverId: currentServiceId,
serverId: currentService?.id,
sessionId: activeChat?._id,
});
response = response ? JSON.parse(response) : null;
@@ -83,7 +83,7 @@ export function useChatActions(
}
console.log("_cancel", response);
},
[currentServiceId, isTauri]
[currentService?.id, isTauri]
);
const chatHistory = useCallback(
@@ -92,9 +92,9 @@ export function useChatActions(
let response: any;
if (isTauri) {
if (!currentServiceId) return;
if (!currentService?.id) return;
response = await platformAdapter.commands("session_chat_history", {
serverId: currentServiceId,
serverId: currentService?.id,
sessionId: chat?._id,
from: 0,
size: 100,
@@ -118,7 +118,7 @@ export function useChatActions(
callback && callback(updatedChat);
setVisibleStartPage(false);
},
[currentServiceId, isTauri]
[currentService?.id, isTauri]
);
const createNewChat = useCallback(
@@ -146,9 +146,9 @@ export function useChatActions(
};
let response: any;
if (isTauri) {
if (!currentServiceId) return;
if (!currentService?.id) return;
response = await platformAdapter.commands("new_chat", {
serverId: currentServiceId,
serverId: currentService?.id,
websocketId: sessionId,
message: value,
queryParams,
@@ -186,7 +186,7 @@ export function useChatActions(
},
[
isTauri,
currentServiceId,
currentService?.id,
sourceDataIds,
MCPIds,
isSearchActive,
@@ -222,9 +222,9 @@ export function useChatActions(
}
let response: any;
if (isTauri) {
if (!currentServiceId) return;
if (!currentService?.id) return;
response = await platformAdapter.commands("send_message", {
serverId: currentServiceId,
serverId: currentService?.id,
websocketId: sessionId,
sessionId: newChat?._id,
queryParams,
@@ -260,7 +260,7 @@ export function useChatActions(
},
[
isTauri,
currentServiceId,
currentService?.id,
sourceDataIds,
MCPIds,
isSearchActive,
@@ -295,9 +295,9 @@ export function useChatActions(
let response: any;
if (isTauri) {
if (!currentServiceId) return;
if (!currentService?.id) return;
response = await platformAdapter.commands("open_session_chat", {
serverId: currentServiceId,
serverId: currentService?.id,
sessionId: chat?._id,
});
response = response ? JSON.parse(response) : null;
@@ -310,19 +310,19 @@ export function useChatActions(
return response;
},
[currentServiceId, isTauri]
[currentService?.id, isTauri]
);
const getChatHistory = useCallback(async () => {
let response: any;
if (isTauri) {
if (!currentServiceId || !isLogin) {
if (!currentService?.id || !isLogin) {
setChats([]);
return
}
response = await platformAdapter.commands("chat_history", {
serverId: currentServiceId,
serverId: currentService?.id,
from: 0,
size: 100,
query: keyword,
@@ -336,14 +336,15 @@ export function useChatActions(
});
response = res;
}
console.log("_history", response);
const hits = response?.hits?.hits || [];
setChats(hits);
}, [currentServiceId, keyword, isTauri]);
}, [currentService?.id, keyword, isTauri]);
useEffect(() => {
showChatHistory && connected && getChatHistory();
}, [showChatHistory, connected, getChatHistory]);
}, [showChatHistory, connected, getChatHistory, currentService?.id]);
const createChatWindow = useCallback(async (createWin: any) => {
if (isTauri) {
@@ -371,20 +372,20 @@ export function useChatActions(
};
const handleRename = useCallback(async (chatId: string, title: string) => {
if (!currentServiceId) return;
if (!currentService?.id) return;
await platformAdapter.commands("update_session_chat", {
serverId: currentServiceId,
serverId: currentService?.id,
sessionId: chatId,
title,
});
}, [currentServiceId]);
}, [currentService?.id]);
const handleDelete = useCallback(async (chatId: string) => {
if (!currentServiceId) return;
if (!currentService?.id) return;
await platformAdapter.commands("delete_session_chat", currentServiceId, chatId);
}, [currentServiceId]);
await platformAdapter.commands("delete_session_chat", currentService?.id, chatId);
}, [currentService?.id]);
return {
chatClose,