mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 19:47:43 +01:00
feat: add MCP & call tools (#430)
* feat: add call tools * feat: add chat call tools * feat: add MCP & call LLM tools * docs: update notes * build: build error * chore: replace iconfont * chore: web icon * chore: add
This commit is contained in:
@@ -30,7 +30,8 @@ Information about release notes of Coco Server is provided here.
|
||||
- feat: web components assistant #422
|
||||
- feat: right-click menu support for search #423
|
||||
- feat: add chat mode launch page #424
|
||||
feat: ai assistant supports search and paging #431
|
||||
- feat: add MCP & call LLM tools #430
|
||||
- feat: ai assistant supports search and paging #431
|
||||
- feat: data sources support displaying customized icons #432
|
||||
|
||||
### Bug fix
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -111,7 +111,8 @@ pub fn run() {
|
||||
server::servers::disable_server,
|
||||
server::auth::handle_sso_callback,
|
||||
server::profile::get_user_profiles,
|
||||
server::datasource::get_datasources_by_server,
|
||||
server::datasource::datasource_search,
|
||||
server::datasource::mcp_server_search,
|
||||
server::connector::get_connectors_by_server,
|
||||
search::query_coco_fusion,
|
||||
assistant::chat_history,
|
||||
|
||||
@@ -48,7 +48,7 @@ pub async fn refresh_all_datasources<R: Runtime>(_app_handle: &AppHandle<R>) ->
|
||||
// dbg!("fetch datasources for server: {}", &server.id);
|
||||
|
||||
// Attempt to get datasources by server, and continue even if it fails
|
||||
let connectors = match get_datasources_by_server(server.id.as_str(), None).await {
|
||||
let connectors = match datasource_search(server.id.as_str(), None).await {
|
||||
Ok(connectors) => {
|
||||
// Process connectors only after fetching them
|
||||
let connectors_map: HashMap<String, DataSource> = connectors
|
||||
@@ -90,7 +90,7 @@ pub async fn refresh_all_datasources<R: Runtime>(_app_handle: &AppHandle<R>) ->
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_datasources_by_server(
|
||||
pub async fn datasource_search(
|
||||
id: &str,
|
||||
options: Option<GetDatasourcesByServerOptions>,
|
||||
) -> Result<Vec<DataSource>, String> {
|
||||
@@ -144,3 +144,59 @@ pub async fn get_datasources_by_server(
|
||||
|
||||
Ok(datasources)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mcp_server_search(
|
||||
id: &str,
|
||||
options: Option<GetDatasourcesByServerOptions>,
|
||||
) -> Result<Vec<DataSource>, String> {
|
||||
let from = options.as_ref().and_then(|opt| opt.from).unwrap_or(0);
|
||||
let size = options.as_ref().and_then(|opt| opt.size).unwrap_or(10000);
|
||||
let query = options
|
||||
.and_then(|opt| opt.query)
|
||||
.unwrap_or(String::default());
|
||||
|
||||
let mut body = serde_json::json!({
|
||||
"from": from,
|
||||
"size": size,
|
||||
});
|
||||
|
||||
if !query.is_empty() {
|
||||
body["query"] = serde_json::json!({
|
||||
"bool": {
|
||||
"must": [{
|
||||
"query_string": {
|
||||
"fields": ["combined_fulltext"],
|
||||
"query": query,
|
||||
"fuzziness": "AUTO",
|
||||
"fuzzy_prefix_length": 2,
|
||||
"fuzzy_max_expansions": 10,
|
||||
"fuzzy_transpositions": true,
|
||||
"allow_leading_wildcard": false
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Perform the async HTTP request outside the cache lock
|
||||
let resp = HttpClient::post(
|
||||
id,
|
||||
"/mcp_server/_search",
|
||||
None,
|
||||
Some(reqwest::Body::from(body.to_string())),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Error fetching datasource: {}", e))?;
|
||||
|
||||
// Parse the search results from the response
|
||||
let mcp_server: Vec<DataSource> = parse_search_results(resp).await.map_err(|e| {
|
||||
dbg!("Error parsing search results: {}", &e);
|
||||
e.to_string()
|
||||
})?;
|
||||
|
||||
// Save the updated mcp_server to cache
|
||||
// save_datasource_to_cache(&id, mcp_server.clone());
|
||||
|
||||
Ok(mcp_server)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::common::http::get_response_body_text;
|
||||
use crate::common::register::SearchSourceRegistry;
|
||||
use crate::common::server::{AuthProvider, Provider, Server, ServerAccessToken, Sso, Version};
|
||||
use crate::server::connector::fetch_connectors_by_server;
|
||||
use crate::server::datasource::get_datasources_by_server;
|
||||
use crate::server::datasource::datasource_search;
|
||||
use crate::server::http_client::HttpClient;
|
||||
use crate::server::search::CocoSearchSource;
|
||||
use crate::COCO_TAURI_STORE;
|
||||
@@ -347,7 +347,7 @@ pub async fn refresh_coco_server_info<R: Runtime>(
|
||||
|
||||
// Refresh connectors and datasources (best effort)
|
||||
let _ = fetch_connectors_by_server(&id).await;
|
||||
let _ = get_datasources_by_server(&id, None).await;
|
||||
let _ = datasource_search(&id, None).await;
|
||||
|
||||
Ok(updated_server)
|
||||
}
|
||||
|
||||
@@ -70,12 +70,10 @@ export const Get = <T>(
|
||||
): Promise<[any, FcResponse<T> | undefined]> =>
|
||||
new Promise((resolve) => {
|
||||
const appStore = JSON.parse(localStorage.getItem("app-store") || "{}");
|
||||
// console.log("baseURL", appStore.state?.endpoint_http)
|
||||
|
||||
let baseURL = "";
|
||||
|
||||
if (import.meta.env.PROD) {
|
||||
baseURL = appStore.state?.endpoint_https;
|
||||
let baseURL = appStore.state?.endpoint_http;
|
||||
if (!baseURL || baseURL === "undefined") {
|
||||
baseURL = "";
|
||||
}
|
||||
|
||||
axios
|
||||
@@ -103,12 +101,11 @@ export const Post = <T>(
|
||||
): Promise<[any, FcResponse<T> | undefined]> => {
|
||||
return new Promise((resolve) => {
|
||||
const appStore = JSON.parse(localStorage.getItem("app-store") || "{}");
|
||||
// console.log("baseURL", appStore.state?.endpoint_http)
|
||||
|
||||
let baseURL = "";
|
||||
|
||||
if (import.meta.env.PROD) {
|
||||
baseURL = appStore.state?.endpoint_https;
|
||||
let baseURL = appStore.state?.endpoint_http
|
||||
if (!baseURL || baseURL === "undefined") {
|
||||
baseURL = "";
|
||||
}
|
||||
|
||||
axios
|
||||
|
||||
@@ -103,8 +103,12 @@ export function get_connectors_by_server(id: string): Promise<Connector[]> {
|
||||
return invokeWithErrorHandler(`get_connectors_by_server`, { id });
|
||||
}
|
||||
|
||||
export function get_datasources_by_server(id: string): Promise<DataSource[]> {
|
||||
return invokeWithErrorHandler(`get_datasources_by_server`, { id });
|
||||
export function datasource_search(id: string): Promise<DataSource[]> {
|
||||
return invokeWithErrorHandler(`datasource_search`, { id });
|
||||
}
|
||||
|
||||
export function mcp_server_search(id: string): Promise<DataSource[]> {
|
||||
return invokeWithErrorHandler(`mcp_server_search`, { id });
|
||||
}
|
||||
|
||||
export function connect_to_server(id: string, clientId: string): Promise<void> {
|
||||
|
||||
@@ -67,11 +67,14 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
||||
size,
|
||||
};
|
||||
|
||||
if (debounceKeyword) {
|
||||
if (debounceKeyword || assistantIDs.length > 0) {
|
||||
body.query = {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
must: [],
|
||||
},
|
||||
};
|
||||
if (debounceKeyword) {
|
||||
body.query.bool.must.push({
|
||||
query_string: {
|
||||
fields: ["combined_fulltext"],
|
||||
query: debounceKeyword,
|
||||
@@ -81,10 +84,15 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
||||
fuzzy_transpositions: true,
|
||||
allow_leading_wildcard: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (assistantIDs.length > 0) {
|
||||
body.query.bool.must.push({
|
||||
terms: {
|
||||
id: assistantIDs.map((id) => id),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isTauri) {
|
||||
@@ -107,12 +115,6 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
||||
|
||||
let assistantList = response?.hits?.hits ?? [];
|
||||
|
||||
if (assistantIDs.length > 0) {
|
||||
assistantList = assistantList.filter((item: any) => {
|
||||
return assistantIDs.includes(item._id);
|
||||
});
|
||||
}
|
||||
|
||||
if (assistantList.length > 0) {
|
||||
const matched = assistantList.find((item: any) => {
|
||||
return item._id === currentAssistant?._id;
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { useWindows } from "@/hooks/useWindows";
|
||||
import useMessageChunkData from "@/hooks/useMessageChunkData";
|
||||
import useWebSocket from "@/hooks/useWebSocket";
|
||||
@@ -27,6 +26,7 @@ import { useAppStore } from "@/stores/appStore";
|
||||
interface ChatAIProps {
|
||||
isSearchActive?: boolean;
|
||||
isDeepThinkActive?: boolean;
|
||||
isMCPActive?: boolean;
|
||||
activeChatProp?: Chat;
|
||||
changeInput?: (val: string) => void;
|
||||
setIsSidebarOpen?: (value: boolean) => void;
|
||||
@@ -52,6 +52,7 @@ const ChatAI = memo(
|
||||
changeInput,
|
||||
isSearchActive,
|
||||
isDeepThinkActive,
|
||||
isMCPActive,
|
||||
activeChatProp,
|
||||
setIsSidebarOpen,
|
||||
isSidebarOpen = false,
|
||||
@@ -88,7 +89,6 @@ const ChatAI = memo(
|
||||
|
||||
const [isSidebarOpenChat, setIsSidebarOpenChat] = useState(isSidebarOpen);
|
||||
const [chats, setChats] = useState<Chat[]>([]);
|
||||
const sourceDataIds = useSearchStore((state) => state.sourceDataIds);
|
||||
|
||||
useEffect(() => {
|
||||
activeChatProp && setActiveChat(activeChatProp);
|
||||
@@ -107,6 +107,7 @@ const ChatAI = memo(
|
||||
const {
|
||||
data: {
|
||||
query_intent,
|
||||
tools,
|
||||
fetch_source,
|
||||
pick_source,
|
||||
deep_read,
|
||||
@@ -119,6 +120,7 @@ const ChatAI = memo(
|
||||
|
||||
const [loadingStep, setLoadingStep] = useState<Record<string, boolean>>({
|
||||
query_intent: false,
|
||||
tools: false,
|
||||
fetch_source: false,
|
||||
pick_source: false,
|
||||
deep_read: false,
|
||||
@@ -161,10 +163,10 @@ const ChatAI = memo(
|
||||
setChats,
|
||||
isSearchActive,
|
||||
isDeepThinkActive,
|
||||
sourceDataIds,
|
||||
isMCPActive,
|
||||
changeInput,
|
||||
websocketSessionId,
|
||||
showChatHistory
|
||||
showChatHistory,
|
||||
);
|
||||
|
||||
const { dealMsg, messageTimeoutRef } = useMessageHandler(
|
||||
@@ -388,6 +390,7 @@ const ChatAI = memo(
|
||||
activeChat={activeChat}
|
||||
curChatEnd={curChatEnd}
|
||||
query_intent={query_intent}
|
||||
tools={tools}
|
||||
fetch_source={fetch_source}
|
||||
pick_source={pick_source}
|
||||
deep_read={deep_read}
|
||||
|
||||
@@ -16,6 +16,7 @@ interface ChatContentProps {
|
||||
activeChat?: Chat;
|
||||
curChatEnd: boolean;
|
||||
query_intent?: IChunkData;
|
||||
tools?: IChunkData;
|
||||
fetch_source?: IChunkData;
|
||||
pick_source?: IChunkData;
|
||||
deep_read?: IChunkData;
|
||||
@@ -32,6 +33,7 @@ export const ChatContent = ({
|
||||
activeChat,
|
||||
curChatEnd,
|
||||
query_intent,
|
||||
tools,
|
||||
fetch_source,
|
||||
pick_source,
|
||||
deep_read,
|
||||
@@ -94,6 +96,7 @@ export const ChatContent = ({
|
||||
))}
|
||||
{(!curChatEnd ||
|
||||
query_intent ||
|
||||
tools ||
|
||||
fetch_source ||
|
||||
pick_source ||
|
||||
deep_read ||
|
||||
@@ -113,6 +116,7 @@ export const ChatContent = ({
|
||||
onResend={handleSendMessage}
|
||||
isTyping={!curChatEnd}
|
||||
query_intent={query_intent}
|
||||
tools={tools}
|
||||
fetch_source={fetch_source}
|
||||
pick_source={pick_source}
|
||||
deep_read={deep_read}
|
||||
|
||||
@@ -72,7 +72,9 @@ export function ServerList({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
isTauri && fetchServers(true);
|
||||
if (!isTauri) return;
|
||||
|
||||
fetchServers(true);
|
||||
|
||||
const unlisten = platformAdapter.listenEvent("login_or_logout", (event) => {
|
||||
console.log("Login or Logout:", currentService, event.payload);
|
||||
|
||||
74
src/components/ChatMessage/CallTools.tsx
Normal file
74
src/components/ChatMessage/CallTools.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Loader, Hammer, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import type { IChunkData } from "@/components/Assistant/types";
|
||||
import Markdown from "./Markdown";
|
||||
|
||||
interface CallToolsProps {
|
||||
Detail?: any;
|
||||
ChunkData?: IChunkData;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export const CallTools = ({ Detail, ChunkData, loading }: CallToolsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [isThinkingExpanded, setIsThinkingExpanded] = useState(false);
|
||||
|
||||
const [Data, setData] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!Detail?.description) return;
|
||||
setData(Detail?.description);
|
||||
}, [Detail?.description]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ChunkData?.message_chunk) return;
|
||||
setData(ChunkData?.message_chunk);
|
||||
}, [ChunkData?.message_chunk, Data]);
|
||||
|
||||
// Must be after hooks !!!
|
||||
if (!ChunkData && !Detail) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-2 mb-3 w-full">
|
||||
<button
|
||||
onClick={() => setIsThinkingExpanded((prev) => !prev)}
|
||||
className="inline-flex items-center gap-2 px-2 py-1 rounded-xl transition-colors border border-[#E6E6E6] dark:border-[#272626]"
|
||||
>
|
||||
{loading ? (
|
||||
<Loader className="w-4 h-4 animate-spin text-[#1990FF]" />
|
||||
) : (
|
||||
<Hammer className="w-4 h-4 text-[#38C200]" />
|
||||
)}
|
||||
<span className="text-xs text-[#999999] italic">
|
||||
{t(`assistant.message.steps.${ChunkData?.chunk_type}`)}
|
||||
</span>
|
||||
{isThinkingExpanded ? (
|
||||
<ChevronUp className="w-4 h-4" />
|
||||
) : (
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
{isThinkingExpanded && (
|
||||
<div className="pl-2 border-l-2 border-[#e5e5e5] dark:border-[#4e4e56]">
|
||||
<div className="text-[#8b8b8b] dark:text-[#a6a6a6] space-y-2">
|
||||
<Markdown
|
||||
content={Data || ""}
|
||||
loading={loading}
|
||||
onDoubleClickCapture={() => {}}
|
||||
/>
|
||||
{/* {Data?.split("\n").map(
|
||||
(paragraph, idx) =>
|
||||
paragraph.trim() && (
|
||||
<p key={idx} className="text-sm">
|
||||
{paragraph}
|
||||
</p>
|
||||
)
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
import { memo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import clsx from "clsx";
|
||||
|
||||
import logoImg from "@/assets/icon.svg";
|
||||
import type { Message, IChunkData } from "@/components/Assistant/types";
|
||||
import { QueryIntent } from "./QueryIntent";
|
||||
import { CallTools } from "./CallTools";
|
||||
import { FetchSource } from "./FetchSource";
|
||||
import { PickSource } from "./PickSource";
|
||||
import { DeepRead } from "./DeepRead";
|
||||
@@ -14,12 +16,12 @@ import { SuggestionList } from "./SuggestionList";
|
||||
import { UserMessage } from "./UserMessage";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import FontIcon from "@/components/Common/Icons/FontIcon";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface ChatMessageProps {
|
||||
message: Message;
|
||||
isTyping?: boolean;
|
||||
query_intent?: IChunkData;
|
||||
tools?: IChunkData;
|
||||
fetch_source?: IChunkData;
|
||||
pick_source?: IChunkData;
|
||||
deep_read?: IChunkData;
|
||||
@@ -33,6 +35,7 @@ export const ChatMessage = memo(function ChatMessage({
|
||||
message,
|
||||
isTyping,
|
||||
query_intent,
|
||||
tools,
|
||||
fetch_source,
|
||||
pick_source,
|
||||
deep_read,
|
||||
@@ -73,6 +76,13 @@ export const ChatMessage = memo(function ChatMessage({
|
||||
getSuggestion={getSuggestion}
|
||||
loading={loadingStep?.query_intent}
|
||||
/>
|
||||
|
||||
<CallTools
|
||||
Detail={details.find((item) => item.type === "tools")}
|
||||
ChunkData={tools}
|
||||
loading={loadingStep?.tools}
|
||||
/>
|
||||
|
||||
<FetchSource
|
||||
Detail={details.find((item) => item.type === "fetch_source")}
|
||||
ChunkData={fetch_source}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { DataSourceItem } from "./DataSourceItem";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import {
|
||||
get_connectors_by_server,
|
||||
get_datasources_by_server,
|
||||
datasource_search,
|
||||
} from "@/commands";
|
||||
|
||||
export function DataSourcesList({ server }: { server: string }) {
|
||||
@@ -27,9 +27,9 @@ export function DataSourcesList({ server }: { server: string }) {
|
||||
.finally(() => {});
|
||||
|
||||
//fetch datasource data
|
||||
get_datasources_by_server(server)
|
||||
datasource_search(server)
|
||||
.then((res: any) => {
|
||||
// console.log("get_datasources_by_server", res);
|
||||
// console.log("datasource_search", res);
|
||||
setDatasourceData(res, server);
|
||||
})
|
||||
.finally(() => {});
|
||||
|
||||
@@ -6,7 +6,7 @@ interface NoDataImageProps {
|
||||
}
|
||||
|
||||
const NoDataImage: FC<NoDataImageProps> = (props) => {
|
||||
const { size } = props;
|
||||
const { size = 64 } = props;
|
||||
const isDark = useThemeStore((state) => state.isDark);
|
||||
|
||||
const color = useMemo(() => {
|
||||
@@ -29,9 +29,9 @@ const NoDataImage: FC<NoDataImageProps> = (props) => {
|
||||
<g
|
||||
id="聊天记录"
|
||||
stroke="none"
|
||||
stroke-width="1"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
fill-rule="evenodd"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<g id="画板" transform="translate(-1164, -252)">
|
||||
<g id="编组" transform="translate(1164, 252)">
|
||||
@@ -43,7 +43,7 @@ const NoDataImage: FC<NoDataImageProps> = (props) => {
|
||||
d="M42.6791711,10.4781665 C43.2314559,10.4781665 43.6791711,10.9258817 43.6791711,11.4781665 C43.6791711,12.0304512 43.2314559,12.4781665 42.6791711,12.4781665 L22.9821691,12.4781665 C20.8942749,12.4781665 19.1821691,14.272673 19.1821691,16.5091825 L19.1821691,52.5647975 C19.1821691,54.801307 20.8942749,56.5958135 22.9821691,56.5958135 L52.5821691,56.5958135 C54.6700634,56.5958135 56.3821691,54.801307 56.3821691,52.5647975 L56.3821691,35.1361358 C56.3821691,34.5838511 56.8298844,34.1361358 57.3821691,34.1361358 C57.9344539,34.1361358 58.3821691,34.5838511 58.3821691,35.1361358 L58.3821691,52.5647975 C58.3821691,55.8853949 55.7962085,58.5958135 52.5821691,58.5958135 L22.9821691,58.5958135 C19.7681298,58.5958135 17.1821691,55.8853949 17.1821691,52.5647975 L17.1821691,16.5091825 C17.1821691,13.1885852 19.7681298,10.4781665 22.9821691,10.4781665 L42.6791711,10.4781665 Z"
|
||||
id="路径"
|
||||
fill={color}
|
||||
fill-rule="nonzero"
|
||||
fillRule="nonzero"
|
||||
mask="url(#mask-2)"
|
||||
transform="translate(37.7822, 34.537) scale(-1, -1) translate(-37.7822, -34.537)"
|
||||
></path>
|
||||
@@ -51,31 +51,31 @@ const NoDataImage: FC<NoDataImageProps> = (props) => {
|
||||
d="M13.0320895,0.82004591 C13.5510674,0.631153401 14.1249097,0.898740484 14.3138023,1.41771839 L15.8235355,5.56567639 C16.012428,6.0846543 15.7448409,6.65849665 15.225863,6.84738916 C14.7068851,7.03628167 14.1330428,6.76869458 13.9441503,6.24971668 L12.434417,2.10175867 C12.2455245,1.58278077 12.5131116,1.00893842 13.0320895,0.82004591 Z"
|
||||
id="路径-4"
|
||||
fill={color}
|
||||
fill-rule="nonzero"
|
||||
fillRule="nonzero"
|
||||
mask="url(#mask-2)"
|
||||
></path>
|
||||
<path
|
||||
d="M21.302281,2.6499257 C21.6572828,2.22685104 22.2880384,2.17166708 22.7111131,2.52666887 C23.1341877,2.88167066 23.1893717,3.51242626 22.8343699,3.93550092 L19.9969995,7.31694727 C19.6419977,7.74002193 19.0112421,7.7952059 18.5881675,7.4402041 C18.1650928,7.08520231 18.1099088,6.45444672 18.4649106,6.03137205 L21.302281,2.6499257 Z"
|
||||
id="路径-4"
|
||||
fill={color}
|
||||
fill-rule="nonzero"
|
||||
fillRule="nonzero"
|
||||
mask="url(#mask-2)"
|
||||
></path>
|
||||
<path
|
||||
d="M12.7902454,12.7943281 C13.1452472,12.3712534 13.7760028,12.3160695 14.1990774,12.6710713 C14.6221521,13.0260731 14.677336,13.6568287 14.3223342,14.0799033 L11.4849639,17.4613497 C11.1299621,17.8844243 10.4992065,17.9396083 10.0761318,17.5846065 C9.65305715,17.2296047 9.59787319,16.5988491 9.95287498,16.1757745 L12.7902454,12.7943281 Z"
|
||||
id="路径-4"
|
||||
fill={color}
|
||||
fill-rule="nonzero"
|
||||
fillRule="nonzero"
|
||||
mask="url(#mask-2)"
|
||||
></path>
|
||||
<path
|
||||
d="M7.75703715,7.52422963 L7.8730548,7.53785515 L12.2201584,8.30436681 C12.7640527,8.40027005 13.1272212,8.91892844 13.031318,9.46282274 C12.9354148,10.006717 12.4167564,10.3698856 11.8728621,10.2739823 L7.52575844,9.50747066 C6.98186414,9.41156742 6.61869562,8.89290903 6.71459887,8.34901473 C6.81050211,7.80512042 7.32916049,7.44195191 7.8730548,7.53785515 L7.75703715,7.52422963 Z"
|
||||
id="路径-4"
|
||||
fill={color}
|
||||
fill-rule="nonzero"
|
||||
fillRule="nonzero"
|
||||
mask="url(#mask-2)"
|
||||
></path>
|
||||
<g id="编组-7" mask="url(#mask-2)" fill={color} fill-rule="nonzero">
|
||||
<g id="编组-7" mask="url(#mask-2)" fill={color} fillRule="nonzero">
|
||||
<g transform="translate(21.7188, 46.9969) rotate(18) translate(-21.7188, -46.9969)translate(1.0617, 26.0967)">
|
||||
<path
|
||||
d="M29.4293481,8.47638559 C29.6966411,8.47287826 29.9542113,8.57653439 30.1445628,8.76421534 L34.821948,13.37598 C35.2152213,13.7637359 35.219694,14.3968851 34.8319381,14.7901583 C34.4441822,15.1834316 33.811033,15.1879043 33.4177598,14.8001484 L29.0372657,10.4802995 L10.8832657,10.7192995 L7.15822377,14.4638747 C6.79868609,14.8253024 6.23152968,14.8545189 5.83844017,14.5505235 L5.74401507,14.4675822 C5.35246833,14.078083 5.35080843,13.4449202 5.74030758,13.0533735 L9.75247731,9.02011185 C9.93695686,8.8346625 10.1867572,8.72888061 10.4483149,8.72544853 L29.4293481,8.47638559 Z"
|
||||
@@ -105,21 +105,21 @@ const NoDataImage: FC<NoDataImageProps> = (props) => {
|
||||
d="M49.865098,19.7058824 C50.4173828,19.7058824 50.865098,20.1535976 50.865098,20.7058824 C50.865098,21.2581671 50.4173828,21.7058824 49.865098,21.7058824 L26.0259167,21.7058824 C25.4736319,21.7058824 25.0259167,21.2581671 25.0259167,20.7058824 C25.0259167,20.1535976 25.4736319,19.7058824 26.0259167,19.7058824 L49.865098,19.7058824 Z"
|
||||
id="路径-7"
|
||||
fill={color}
|
||||
fill-rule="nonzero"
|
||||
fillRule="nonzero"
|
||||
mask="url(#mask-2)"
|
||||
></path>
|
||||
<path
|
||||
d="M49.865098,39.4705882 C50.4173828,39.4705882 50.865098,39.9183035 50.865098,40.4705882 C50.865098,41.022873 50.4173828,41.4705882 49.865098,41.4705882 L43.2941176,41.4705882 C42.7418329,41.4705882 42.2941176,41.022873 42.2941176,40.4705882 C42.2941176,39.9183035 42.7418329,39.4705882 43.2941176,39.4705882 L49.865098,39.4705882 Z"
|
||||
id="路径-7备份"
|
||||
fill={color}
|
||||
fill-rule="nonzero"
|
||||
fillRule="nonzero"
|
||||
mask="url(#mask-2)"
|
||||
></path>
|
||||
<path
|
||||
d="M49.865098,47 C50.4173828,47 50.865098,47.4477153 50.865098,48 C50.865098,48.5522847 50.4173828,49 49.865098,49 L43.2941176,49 C42.7418329,49 42.2941176,48.5522847 42.2941176,48 C42.2941176,47.4477153 42.7418329,47 43.2941176,47 L49.865098,47 Z"
|
||||
id="路径-7备份-2"
|
||||
fill={color}
|
||||
fill-rule="nonzero"
|
||||
fillRule="nonzero"
|
||||
mask="url(#mask-2)"
|
||||
></path>
|
||||
</g>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useAppStore } from "@/stores/appStore";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { metaOrCtrlKey } from "@/utils/keyboardUtils";
|
||||
import SearchPopover from "./SearchPopover";
|
||||
import MCPPopover from "./MCPPopover";
|
||||
// import AudioRecording from "../AudioRecording";
|
||||
import { DataSource } from "@/types/commands";
|
||||
// import InputExtra from "./InputExtra";
|
||||
@@ -33,6 +34,8 @@ interface ChatInputProps {
|
||||
setIsSearchActive: () => void;
|
||||
isDeepThinkActive: boolean;
|
||||
setIsDeepThinkActive: () => void;
|
||||
isMCPActive: boolean;
|
||||
setIsMCPActive: () => void;
|
||||
isChatPage?: boolean;
|
||||
getDataSourcesByServer: (
|
||||
serverId: string,
|
||||
@@ -42,6 +45,14 @@ interface ChatInputProps {
|
||||
query?: string;
|
||||
}
|
||||
) => Promise<DataSource[]>;
|
||||
getMCPByServer: (
|
||||
serverId: string,
|
||||
options?: {
|
||||
from?: number;
|
||||
size?: number;
|
||||
query?: string;
|
||||
}
|
||||
) => Promise<DataSource[]>;
|
||||
setupWindowFocusListener: (callback: () => void) => Promise<() => void>;
|
||||
checkScreenPermission: () => Promise<boolean>;
|
||||
requestScreenPermission: () => void;
|
||||
@@ -73,8 +84,11 @@ export default function ChatInput({
|
||||
setIsSearchActive,
|
||||
isDeepThinkActive,
|
||||
setIsDeepThinkActive,
|
||||
isMCPActive,
|
||||
setIsMCPActive,
|
||||
isChatPage = false,
|
||||
getDataSourcesByServer,
|
||||
getMCPByServer,
|
||||
setupWindowFocusListener,
|
||||
hideCoco,
|
||||
hasModules = [],
|
||||
@@ -483,8 +497,17 @@ export default function ChatInput({
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentAssistant?._source?.mcp_servers?.visible && (
|
||||
<MCPPopover
|
||||
isMCPActive={isMCPActive}
|
||||
setIsMCPActive={setIsMCPActive}
|
||||
getMCPByServer={getMCPByServer}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!currentAssistant?._source?.datasource?.visible &&
|
||||
!currentAssistant?._source?.config?.visible ? (
|
||||
!currentAssistant?._source?.config?.visible &&
|
||||
!currentAssistant?._source?.mcp_servers?.visible ? (
|
||||
<div className="px-[9px]">
|
||||
<Copyright />
|
||||
</div>
|
||||
|
||||
347
src/components/Search/MCPPopover.tsx
Normal file
347
src/components/Search/MCPPopover.tsx
Normal file
@@ -0,0 +1,347 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { Input, Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
RefreshCw,
|
||||
Layers,
|
||||
Hammer,
|
||||
ChevronRight,
|
||||
ChevronLeft,
|
||||
} from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDebounce } from "ahooks";
|
||||
|
||||
import TypeIcon from "@/components/Common/Icons/TypeIcon";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { DataSource } from "@/types/commands";
|
||||
import Checkbox from "@/components/Common/Checkbox";
|
||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||
import VisibleKey from "@/components/Common/VisibleKey";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import NoDataImage from "@/components/Common/NoDataImage";
|
||||
|
||||
interface SearchPopoverProps {
|
||||
isMCPActive: boolean;
|
||||
setIsMCPActive: () => void;
|
||||
getMCPByServer: (
|
||||
serverId: string,
|
||||
options?: {
|
||||
from?: number;
|
||||
size?: number;
|
||||
query?: string;
|
||||
}
|
||||
) => Promise<DataSource[]>;
|
||||
}
|
||||
|
||||
export default function SearchPopover({
|
||||
isMCPActive,
|
||||
setIsMCPActive,
|
||||
getMCPByServer,
|
||||
}: SearchPopoverProps) {
|
||||
const { t } = useTranslation();
|
||||
const { connected } = useChatStore();
|
||||
|
||||
const [isRefreshDataSource, setIsRefreshDataSource] = useState(false);
|
||||
const [dataList, setDataList] = useState<DataSource[]>([]);
|
||||
|
||||
const MCPIds = useSearchStore((state) => state.MCPIds);
|
||||
const setMCPIds = useSearchStore((state) => state.setMCPIds);
|
||||
|
||||
const currentService = useConnectStore((state) => state.currentService);
|
||||
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const debouncedKeyword = useDebounce(keyword, { wait: 500 });
|
||||
|
||||
const getDataSourceList = useCallback(async () => {
|
||||
try {
|
||||
setPage(1);
|
||||
|
||||
const res: DataSource[] = await getMCPByServer(currentService?.id, {
|
||||
query: debouncedKeyword,
|
||||
});
|
||||
|
||||
console.log("getMCPByServer", res);
|
||||
|
||||
if (res?.length === 0) {
|
||||
setDataList([]);
|
||||
return;
|
||||
}
|
||||
const data = res?.length
|
||||
? [
|
||||
{
|
||||
id: "all",
|
||||
name: "search.input.searchPopover.allScope",
|
||||
},
|
||||
...res,
|
||||
]
|
||||
: [];
|
||||
|
||||
setDataList(data);
|
||||
} catch (err) {
|
||||
setDataList([]);
|
||||
console.error("datasource_search", err);
|
||||
}
|
||||
}, [currentService?.id, debouncedKeyword]);
|
||||
|
||||
const popoverButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const internetSearch = useShortcutsStore((state) => state.internetSearch);
|
||||
const internetSearchScope = useShortcutsStore((state) => {
|
||||
return state.internetSearchScope;
|
||||
});
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPage, setTotalPage] = useState(0);
|
||||
const [visibleList, setVisibleList] = useState<DataSource[]>([]);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (dataList.length > 0) {
|
||||
setMCPIds(dataList.slice(1).map((item) => item.id));
|
||||
}
|
||||
}, [dataList]);
|
||||
|
||||
useEffect(() => {
|
||||
connected && getDataSourceList();
|
||||
}, [connected, currentService?.id, debouncedKeyword]);
|
||||
|
||||
useEffect(() => {
|
||||
setTotalPage(Math.max(Math.ceil(dataList.length / 10), 1));
|
||||
}, [dataList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dataList.length === 0) {
|
||||
return setVisibleList([]);
|
||||
}
|
||||
|
||||
const startIndex = (page - 1) * 9;
|
||||
const endIndex = startIndex + 9;
|
||||
|
||||
const list = [
|
||||
dataList[0],
|
||||
...dataList.slice(1).slice(startIndex, endIndex),
|
||||
];
|
||||
|
||||
setVisibleList(list);
|
||||
}, [dataList, page]);
|
||||
|
||||
const onSelectDataSource = useCallback(
|
||||
(id: string, checked: boolean, isAll: boolean) => {
|
||||
let nextSourceDataIds = new Set(MCPIds);
|
||||
|
||||
const ids = isAll ? visibleList.slice(1).map((item) => item.id) : [id];
|
||||
|
||||
for (const id of ids) {
|
||||
if (checked) {
|
||||
nextSourceDataIds.add(id);
|
||||
} else {
|
||||
nextSourceDataIds.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
setMCPIds(Array.from(nextSourceDataIds));
|
||||
},
|
||||
[visibleList, MCPIds]
|
||||
);
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setIsRefreshDataSource(true);
|
||||
|
||||
await getDataSourceList();
|
||||
|
||||
setTimeout(() => {
|
||||
setIsRefreshDataSource(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
if (page === 1) return;
|
||||
|
||||
setPage(page - 1);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (page === totalPage) return;
|
||||
|
||||
setPage(page + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"flex items-center gap-1 p-[3px] pr-1 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126] cursor-pointer",
|
||||
{
|
||||
"!bg-[rgba(0,114,255,0.3)]": isMCPActive,
|
||||
}
|
||||
)}
|
||||
onClick={setIsMCPActive}
|
||||
>
|
||||
<VisibleKey shortcut={internetSearch} onKeyPress={setIsMCPActive}>
|
||||
<Hammer
|
||||
className={`size-3 ${
|
||||
isMCPActive
|
||||
? "text-[#0072FF] dark:text-[#0072FF]"
|
||||
: "text-[#333] dark:text-white"
|
||||
}`}
|
||||
/>
|
||||
</VisibleKey>
|
||||
|
||||
{isMCPActive && (
|
||||
<>
|
||||
<span
|
||||
className={`${isMCPActive ? "text-[#0072FF]" : "dark:text-white"}`}
|
||||
>
|
||||
{t("search.input.MCP")}
|
||||
</span>
|
||||
|
||||
<Popover className="relative">
|
||||
<PopoverButton ref={popoverButtonRef} className="flex items-center">
|
||||
<VisibleKey
|
||||
shortcut={internetSearchScope}
|
||||
onKeyPress={() => {
|
||||
popoverButtonRef.current?.click();
|
||||
}}
|
||||
>
|
||||
<ChevronDownIcon
|
||||
className={clsx("size-3", [
|
||||
isMCPActive
|
||||
? "text-[#0072FF] dark:text-[#0072FF]"
|
||||
: "text-[#333] dark:text-white",
|
||||
])}
|
||||
/>
|
||||
</VisibleKey>
|
||||
</PopoverButton>
|
||||
|
||||
<PopoverPanel className="absolute z-50 left-0 bottom-6 w-[240px] overflow-y-auto bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700">
|
||||
<div
|
||||
className="text-sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className="p-3">
|
||||
<div className="flex justify-between">
|
||||
<span>{t("search.input.searchPopover.title")}</span>
|
||||
|
||||
<div
|
||||
onClick={handleRefresh}
|
||||
className="size-[24px] flex justify-center items-center rounded-lg border border-black/10 dark:border-white/10 cursor-pointer"
|
||||
>
|
||||
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
||||
<RefreshCw
|
||||
className={`size-3 text-[#0287FF] transition-transform duration-1000 ${
|
||||
isRefreshDataSource ? "animate-spin" : ""
|
||||
}`}
|
||||
/>
|
||||
</VisibleKey>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative h-8 my-2">
|
||||
<div className="absolute inset-0 flex items-center px-2 pointer-events-none">
|
||||
<VisibleKey
|
||||
shortcut="F"
|
||||
shortcutClassName="translate-x-0"
|
||||
onKeyPress={() => {
|
||||
searchInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
autoFocus
|
||||
ref={searchInputRef}
|
||||
className="size-full px-2 rounded-lg border dark:border-white/10 bg-transparent"
|
||||
onChange={(e) => {
|
||||
setKeyword(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{visibleList.length > 0 ? (
|
||||
<ul className="flex flex-col gap-2">
|
||||
{visibleList?.map((item, index) => {
|
||||
const { id, name } = item;
|
||||
|
||||
const isAll = index === 0;
|
||||
|
||||
const isChecked = () => {
|
||||
if (isAll) {
|
||||
return visibleList.slice(1).every((item) => {
|
||||
return MCPIds.includes(item.id);
|
||||
});
|
||||
} else {
|
||||
return MCPIds.includes(id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<li
|
||||
key={id}
|
||||
className="flex justify-between items-center"
|
||||
>
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
{isAll ? (
|
||||
<Layers className="size-[16px] text-[#0287FF]" />
|
||||
) : (
|
||||
<TypeIcon item={item} className="size-[16px]" />
|
||||
)}
|
||||
|
||||
<span className="truncate">
|
||||
{isAll && name ? t(name) : name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<VisibleKey
|
||||
shortcut={index === 9 ? "0" : String(index + 1)}
|
||||
shortcutClassName="-translate-x-3"
|
||||
onKeyPress={() => {
|
||||
onSelectDataSource(id, !isChecked(), isAll);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex justify-center items-center size-[24px]">
|
||||
<Checkbox
|
||||
checked={isChecked()}
|
||||
indeterminate={isAll}
|
||||
onChange={(value) =>
|
||||
onSelectDataSource(id, value, isAll)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<NoDataImage />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{visibleList.length > 0 && (
|
||||
<div className="flex items-center justify-between h-8 px-3 border-t dark:border-t-[#202126]">
|
||||
<VisibleKey shortcut="leftarrow" onKeyPress={handlePrev}>
|
||||
<ChevronLeft className="size-4" onClick={handlePrev} />
|
||||
</VisibleKey>
|
||||
|
||||
<div className="text-xs">
|
||||
{page}/{totalPage}
|
||||
</div>
|
||||
|
||||
<VisibleKey shortcut="rightarrow" onKeyPress={handleNext}>
|
||||
<ChevronRight className="size-4" onClick={handleNext} />
|
||||
</VisibleKey>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -85,7 +85,7 @@ export default function SearchPopover({
|
||||
setDataSourceList(data);
|
||||
} catch (err) {
|
||||
setDataSourceList([]);
|
||||
console.error("get_datasources_by_server", err);
|
||||
console.error("datasource_search", err);
|
||||
}
|
||||
}, [currentService?.id, debouncedKeyword]);
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ function SearchChat({
|
||||
...initialAppState,
|
||||
isDeepThinkActive: currentAssistant?._source?.type === "deep_think",
|
||||
isSearchActive: currentAssistant?._source?.datasource?.enabled === true,
|
||||
isMCPActive: currentAssistant?._source?.mcp_servers?.enabled === true,
|
||||
};
|
||||
|
||||
const [state, dispatch] = useReducer(appReducer, customInitialState);
|
||||
@@ -73,6 +74,7 @@ function SearchChat({
|
||||
isTransitioned,
|
||||
isSearchActive,
|
||||
isDeepThinkActive,
|
||||
isMCPActive,
|
||||
isTyping,
|
||||
} = state;
|
||||
const [isWin10, setIsWin10] = useState(false);
|
||||
@@ -150,6 +152,10 @@ function SearchChat({
|
||||
dispatch({ type: "TOGGLE_DEEP_THINK_ACTIVE" });
|
||||
}, []);
|
||||
|
||||
const toggleMCPActive = useCallback(() => {
|
||||
dispatch({ type: "TOGGLE_MCP_ACTIVE" });
|
||||
}, []);
|
||||
|
||||
const LoadingFallback = () => (
|
||||
<div className="flex items-center justify-center h-full">loading...</div>
|
||||
);
|
||||
@@ -178,7 +184,7 @@ function SearchChat({
|
||||
): Promise<DataSource[]> => {
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
response = platformAdapter.invokeBackend("get_datasources_by_server", {
|
||||
response = platformAdapter.invokeBackend("datasource_search", {
|
||||
id: serverId,
|
||||
options,
|
||||
});
|
||||
@@ -202,7 +208,45 @@ function SearchChat({
|
||||
}
|
||||
return response || [];
|
||||
},
|
||||
[]
|
||||
[JSON.stringify(currentAssistant)]
|
||||
);
|
||||
|
||||
const getMCPByServer = useCallback(
|
||||
async (
|
||||
serverId: string,
|
||||
options?: {
|
||||
from?: number;
|
||||
size?: number;
|
||||
query?: string;
|
||||
}
|
||||
): Promise<DataSource[]> => {
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
response = platformAdapter.invokeBackend("mcp_server_search", {
|
||||
id: serverId,
|
||||
options,
|
||||
});
|
||||
} else {
|
||||
const [error, res]: any = await Get("/mcp_server/_search");
|
||||
if (error) {
|
||||
console.error("_search", error);
|
||||
return [];
|
||||
}
|
||||
response = res?.hits?.hits?.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
id: item._source.id,
|
||||
name: item._source.name,
|
||||
};
|
||||
});
|
||||
}
|
||||
let ids = currentAssistant?._source?.datasource?.ids;
|
||||
if (Array.isArray(ids) && ids.length > 0 && !ids.includes("*")) {
|
||||
response = response?.filter((item: any) => ids.includes(item.id));
|
||||
}
|
||||
return response || [];
|
||||
},
|
||||
[JSON.stringify(currentAssistant)]
|
||||
);
|
||||
|
||||
const setupWindowFocusListener = useCallback(async (callback: () => void) => {
|
||||
@@ -310,7 +354,10 @@ function SearchChat({
|
||||
setIsSearchActive={toggleSearchActive}
|
||||
isDeepThinkActive={isDeepThinkActive}
|
||||
setIsDeepThinkActive={toggleDeepThinkActive}
|
||||
isMCPActive={isMCPActive}
|
||||
setIsMCPActive={toggleMCPActive}
|
||||
getDataSourcesByServer={getDataSourcesByServer}
|
||||
getMCPByServer={getMCPByServer}
|
||||
setupWindowFocusListener={setupWindowFocusListener}
|
||||
checkScreenPermission={checkScreenPermission}
|
||||
requestScreenPermission={requestScreenPermission}
|
||||
@@ -363,6 +410,7 @@ function SearchChat({
|
||||
changeInput={setInput}
|
||||
isSearchActive={isSearchActive}
|
||||
isDeepThinkActive={isDeepThinkActive}
|
||||
isMCPActive={isMCPActive}
|
||||
getFileUrl={getFileUrl}
|
||||
showChatHistory={showChatHistory}
|
||||
assistantIDs={assistantIDs}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Get, Post } from "@/api/axiosRequest";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
|
||||
export function useChatActions(
|
||||
currentServiceId: string | undefined,
|
||||
@@ -18,7 +19,7 @@ export function useChatActions(
|
||||
setChats: (chats: Chat[]) => void,
|
||||
isSearchActive?: boolean,
|
||||
isDeepThinkActive?: boolean,
|
||||
sourceDataIds?: string[],
|
||||
isMCPActive?: boolean,
|
||||
changeInput?: (val: string) => void,
|
||||
websocketSessionId?: string,
|
||||
showChatHistory?: boolean
|
||||
@@ -27,6 +28,8 @@ export function useChatActions(
|
||||
const addError = useAppStore((state) => state.addError);
|
||||
const currentAssistant = useConnectStore((state) => state.currentAssistant);
|
||||
const { connected } = useChatStore();
|
||||
const sourceDataIds = useSearchStore((state) => state.sourceDataIds);
|
||||
const MCPIds = useSearchStore((state) => state.MCPIds);
|
||||
|
||||
const [keyword, setKeyword] = useState("");
|
||||
|
||||
@@ -145,11 +148,10 @@ export function useChatActions(
|
||||
console.error("websocketSessionId", websocketSessionId, id);
|
||||
return;
|
||||
}
|
||||
console.log("sourceDataIds", sourceDataIds, websocketSessionId, id);
|
||||
console.log("sourceDataIds", sourceDataIds, MCPIds, websocketSessionId, id);
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
if (!currentServiceId) return;
|
||||
console.log("currentAssistant", currentAssistant);
|
||||
response = await platformAdapter.commands("new_chat", {
|
||||
serverId: currentServiceId,
|
||||
websocketId: websocketSessionId || id,
|
||||
@@ -157,7 +159,9 @@ export function useChatActions(
|
||||
queryParams: {
|
||||
search: isSearchActive,
|
||||
deep_thinking: isDeepThinkActive,
|
||||
mcp: isMCPActive,
|
||||
datasource: sourceDataIds?.join(",") || "",
|
||||
mcp_servers: MCPIds?.join(",") || "",
|
||||
assistant_id: currentAssistant?._id || '',
|
||||
},
|
||||
});
|
||||
@@ -171,7 +175,10 @@ export function useChatActions(
|
||||
{
|
||||
search: isSearchActive,
|
||||
deep_thinking: isDeepThinkActive,
|
||||
mcp: isMCPActive,
|
||||
datasource: sourceDataIds?.join(",") || "",
|
||||
mcp_servers: MCPIds?.join(",") || "",
|
||||
assistant_id: currentAssistant?._id || '',
|
||||
},
|
||||
{
|
||||
"WEBSOCKET-SESSION-ID": websocketSessionId || id,
|
||||
@@ -205,8 +212,10 @@ export function useChatActions(
|
||||
[
|
||||
currentServiceId,
|
||||
sourceDataIds,
|
||||
MCPIds,
|
||||
isSearchActive,
|
||||
isDeepThinkActive,
|
||||
isMCPActive,
|
||||
curIdRef,
|
||||
websocketSessionId,
|
||||
currentAssistant,
|
||||
@@ -234,7 +243,9 @@ export function useChatActions(
|
||||
queryParams: {
|
||||
search: isSearchActive,
|
||||
deep_thinking: isDeepThinkActive,
|
||||
mcp: isMCPActive,
|
||||
datasource: sourceDataIds?.join(",") || "",
|
||||
mcp_servers: MCPIds?.join(",") || "",
|
||||
assistant_id: currentAssistant?._id || '',
|
||||
},
|
||||
message: content,
|
||||
@@ -250,7 +261,10 @@ export function useChatActions(
|
||||
{
|
||||
search: isSearchActive,
|
||||
deep_thinking: isDeepThinkActive,
|
||||
mcp: isMCPActive,
|
||||
datasource: sourceDataIds?.join(",") || "",
|
||||
mcp_servers: MCPIds?.join(",") || "",
|
||||
assistant_id: currentAssistant?._id || '',
|
||||
},
|
||||
{
|
||||
"WEBSOCKET-SESSION-ID": websocketSessionId || id,
|
||||
@@ -281,8 +295,10 @@ export function useChatActions(
|
||||
[
|
||||
currentServiceId,
|
||||
sourceDataIds,
|
||||
MCPIds,
|
||||
isSearchActive,
|
||||
isDeepThinkActive,
|
||||
isMCPActive,
|
||||
curIdRef,
|
||||
setActiveChat,
|
||||
setCurChatEnd,
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { IChunkData } from "@/components/Assistant/types";
|
||||
|
||||
export default function useMessageChunkData() {
|
||||
const [query_intent, setQuery_intent] = useState<IChunkData>();
|
||||
const [tools, setTools] = useState<IChunkData>();
|
||||
const [fetch_source, setFetch_source] = useState<IChunkData>();
|
||||
const [pick_source, setPick_source] = useState<IChunkData>();
|
||||
const [deep_read, setDeep_read] = useState<IChunkData>();
|
||||
@@ -20,6 +21,15 @@ export default function useMessageChunkData() {
|
||||
};
|
||||
});
|
||||
}, []),
|
||||
deal_tools: useCallback((data: IChunkData) => {
|
||||
setTools((prev: IChunkData | undefined): IChunkData => {
|
||||
if (!prev) return data;
|
||||
return {
|
||||
...prev,
|
||||
message_chunk: prev.message_chunk + data.message_chunk,
|
||||
};
|
||||
});
|
||||
}, []),
|
||||
deal_fetch_source: useCallback((data: IChunkData) => {
|
||||
setFetch_source((prev: IChunkData | undefined): IChunkData => {
|
||||
if (!prev) return data;
|
||||
@@ -69,6 +79,7 @@ export default function useMessageChunkData() {
|
||||
|
||||
const clearAllChunkData = useCallback(() => {
|
||||
setQuery_intent(undefined);
|
||||
setTools(undefined);
|
||||
setFetch_source(undefined);
|
||||
setPick_source(undefined);
|
||||
setDeep_read(undefined);
|
||||
@@ -77,7 +88,7 @@ export default function useMessageChunkData() {
|
||||
}, []);
|
||||
|
||||
return {
|
||||
data: { query_intent, fetch_source, pick_source, deep_read, think, response },
|
||||
data: { query_intent, tools, fetch_source, pick_source, deep_read, think, response },
|
||||
handlers,
|
||||
clearAllChunkData,
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ export function useMessageHandler(
|
||||
) => void,
|
||||
handlers: {
|
||||
deal_query_intent: (data: IChunkData) => void;
|
||||
deal_tools: (data: IChunkData) => void;
|
||||
deal_fetch_source: (data: IChunkData) => void;
|
||||
deal_pick_source: (data: IChunkData) => void;
|
||||
deal_deep_read: (data: IChunkData) => void;
|
||||
@@ -47,6 +48,7 @@ export function useMessageHandler(
|
||||
|
||||
setLoadingStep(() => ({
|
||||
query_intent: false,
|
||||
tools: false,
|
||||
fetch_source: false,
|
||||
pick_source: false,
|
||||
deep_read: false,
|
||||
@@ -57,6 +59,9 @@ export function useMessageHandler(
|
||||
|
||||
if (chunkData.chunk_type === "query_intent") {
|
||||
handlers.deal_query_intent(chunkData);
|
||||
} else if (chunkData.chunk_type === "tools") {
|
||||
console.log("tools", chunkData);
|
||||
handlers.deal_tools(chunkData);
|
||||
} else if (chunkData.chunk_type === "fetch_source") {
|
||||
handlers.deal_fetch_source(chunkData);
|
||||
} else if (chunkData.chunk_type === "pick_source") {
|
||||
|
||||
@@ -220,6 +220,7 @@
|
||||
"connecting": "Connecting",
|
||||
"deepThink": "Deep Think",
|
||||
"search": "Search",
|
||||
"MCP": "MCP",
|
||||
"uploadFile": "Upload File",
|
||||
"screenshot": "Screenshot",
|
||||
"screenshotType": {
|
||||
@@ -285,6 +286,7 @@
|
||||
"thinkingButton": "View thinking process",
|
||||
"steps": {
|
||||
"query_intent": "Understand the query",
|
||||
"tools": "Call LLM Tools",
|
||||
"source_zero": "Searching for relevant documents",
|
||||
"fetch_source": "Retrieve {{count}} documents",
|
||||
"pick_source": "Intelligent pick {{count}} results",
|
||||
|
||||
@@ -222,6 +222,7 @@
|
||||
"connecting": "连接中",
|
||||
"deepThink": "深度思考",
|
||||
"search": "联网搜索",
|
||||
"MCP": "MCP",
|
||||
"uploadFile": "上传文件",
|
||||
"screenshot": "截取屏幕截图",
|
||||
"screenshotType": {
|
||||
@@ -287,6 +288,7 @@
|
||||
"thinkingButton": "查看思考过程",
|
||||
"steps": {
|
||||
"query_intent": "理解查询",
|
||||
"tools": "调用大模型工具",
|
||||
"source_zero": "正在搜索相关文档",
|
||||
"fetch_source": "检索 {{count}} 份文档",
|
||||
"pick_source": "智能预选 {{count}} 个结果",
|
||||
|
||||
@@ -23,18 +23,20 @@ import {
|
||||
session_chat_history,
|
||||
close_session_chat,
|
||||
open_session_chat,
|
||||
get_datasources_by_server,
|
||||
datasource_search,
|
||||
delete_session_chat,
|
||||
update_session_chat,
|
||||
} from "@/commands";
|
||||
import { DataSource } from "@/types/commands";
|
||||
import HistoryList from "@/components/Common/HistoryList";
|
||||
import { useSyncStore } from "@/hooks/useSyncStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
|
||||
interface ChatProps {}
|
||||
|
||||
export default function Chat({}: ChatProps) {
|
||||
const currentService = useConnectStore((state) => state.currentService);
|
||||
const currentAssistant = useConnectStore((state) => state.currentAssistant);
|
||||
|
||||
const chatAIRef = useRef<ChatAIRef>(null);
|
||||
|
||||
@@ -47,6 +49,7 @@ export default function Chat({}: ChatProps) {
|
||||
|
||||
const [isSearchActive, setIsSearchActive] = useState(false);
|
||||
const [isDeepThinkActive, setIsDeepThinkActive] = useState(false);
|
||||
const [isMCPActive, setIsMCPActive] = useState(false);
|
||||
const [keyword, setKeyword] = useState("");
|
||||
|
||||
const isChatPage = true;
|
||||
@@ -167,7 +170,7 @@ export default function Chat({}: ChatProps) {
|
||||
|
||||
const getDataSourcesByServer = useCallback(
|
||||
async (serverId: string): Promise<DataSource[]> => {
|
||||
return get_datasources_by_server(serverId);
|
||||
return datasource_search(serverId);
|
||||
},
|
||||
[]
|
||||
);
|
||||
@@ -261,6 +264,29 @@ export default function Chat({}: ChatProps) {
|
||||
await delete_session_chat(currentService.id, id);
|
||||
};
|
||||
|
||||
const getMCPByServer = useCallback(
|
||||
async (
|
||||
serverId: string,
|
||||
options?: {
|
||||
from?: number;
|
||||
size?: number;
|
||||
query?: string;
|
||||
}
|
||||
): Promise<DataSource[]> => {
|
||||
let response: any;
|
||||
response = platformAdapter.invokeBackend("mcp_server_search", {
|
||||
id: serverId,
|
||||
options,
|
||||
});
|
||||
let ids = currentAssistant?._source?.datasource?.ids;
|
||||
if (Array.isArray(ids) && ids.length > 0 && !ids.includes("*")) {
|
||||
response = response?.filter((item: any) => ids.includes(item.id));
|
||||
}
|
||||
return response || [];
|
||||
},
|
||||
[JSON.stringify(currentAssistant)]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-screen">
|
||||
<div className="h-full flex">
|
||||
@@ -318,6 +344,8 @@ export default function Chat({}: ChatProps) {
|
||||
setIsSearchActive={() => setIsSearchActive((prev) => !prev)}
|
||||
isDeepThinkActive={isDeepThinkActive}
|
||||
setIsDeepThinkActive={() => setIsDeepThinkActive((prev) => !prev)}
|
||||
isMCPActive={isMCPActive}
|
||||
setIsMCPActive={() => setIsMCPActive((prev) => !prev)}
|
||||
isChatPage={isChatPage}
|
||||
getDataSourcesByServer={getDataSourcesByServer}
|
||||
setupWindowFocusListener={setupWindowFocusListener}
|
||||
@@ -330,6 +358,7 @@ export default function Chat({}: ChatProps) {
|
||||
openFileDialog={openFileDialog}
|
||||
getFileMetadata={getFileMetadata}
|
||||
getFileIcon={getFileIcon}
|
||||
getMCPByServer={getMCPByServer}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ export type AppState = {
|
||||
input: string;
|
||||
isTransitioned: boolean;
|
||||
isSearchActive: boolean;
|
||||
isMCPActive: boolean;
|
||||
isDeepThinkActive: boolean;
|
||||
isTyping: boolean;
|
||||
isLoading: boolean;
|
||||
@@ -13,6 +14,7 @@ export type AppAction =
|
||||
| { type: 'SET_INPUT'; payload: string }
|
||||
| { type: 'TOGGLE_SEARCH_ACTIVE' }
|
||||
| { type: 'TOGGLE_DEEP_THINK_ACTIVE' }
|
||||
| { type: 'TOGGLE_MCP_ACTIVE' }
|
||||
| { type: 'SET_TYPING'; payload: boolean }
|
||||
| { type: 'SET_LOADING'; payload: boolean };
|
||||
|
||||
@@ -31,6 +33,7 @@ export const initialAppState: AppState = {
|
||||
isTransitioned: getCachedChatMode(),
|
||||
isSearchActive: false,
|
||||
isDeepThinkActive: false,
|
||||
isMCPActive: false,
|
||||
isTyping: false,
|
||||
isLoading: false
|
||||
};
|
||||
@@ -45,6 +48,8 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
||||
return { ...state, isSearchActive: !state.isSearchActive };
|
||||
case 'TOGGLE_DEEP_THINK_ACTIVE':
|
||||
return { ...state, isDeepThinkActive: !state.isDeepThinkActive };
|
||||
case 'TOGGLE_MCP_ACTIVE':
|
||||
return { ...state, isMCPActive: !state.isMCPActive };
|
||||
case 'SET_TYPING':
|
||||
return { ...state, isTyping: action.payload };
|
||||
case 'SET_LOADING':
|
||||
|
||||
@@ -6,6 +6,8 @@ export type ISearchStore = {
|
||||
setSourceData: (sourceData: any) => void;
|
||||
sourceDataIds: string[];
|
||||
setSourceDataIds: (prevSourceDataId: string[]) => void;
|
||||
MCPIds: string[];
|
||||
setMCPIds: (prevSourceDataId: string[]) => void;
|
||||
visibleContextMenu: boolean;
|
||||
setVisibleContextMenu: (visibleContextMenu: boolean) => void;
|
||||
selectedSearchContent?: Record<string, any>;
|
||||
@@ -21,6 +23,8 @@ export const useSearchStore = create<ISearchStore>()(
|
||||
setSourceData: (sourceData: any) => set({ sourceData }),
|
||||
sourceDataIds: [],
|
||||
setSourceDataIds: (sourceDataIds: string[]) => set({ sourceDataIds }),
|
||||
MCPIds: [],
|
||||
setMCPIds: (MCPIds: string[]) => set({ MCPIds }),
|
||||
visibleContextMenu: false,
|
||||
setVisibleContextMenu: (visibleContextMenu) => {
|
||||
return set({ visibleContextMenu });
|
||||
|
||||
@@ -67,7 +67,7 @@ export default defineConfig({
|
||||
|
||||
const packageJson = {
|
||||
name: "@infinilabs/search-chat",
|
||||
version: "1.1.6",
|
||||
version: "1.1.7",
|
||||
main: "index.js",
|
||||
module: "index.js",
|
||||
type: "module",
|
||||
|
||||
Reference in New Issue
Block a user