25 次代码提交 95e7a37355 ... f770bce114

作者 SHA1 备注 提交日期
  yuanfeijie f770bce114 Merge remote-tracking branch 'origin/main' 1 月之前
  ayangweb 80ac8baca5 feat: add chat mode launch page #424 1 月之前
  ayangweb bde658b981 feat: add chat mode launch page (#424) 1 月之前
  BiggerRain 4380b56a30 fix: query_coco_fusion params error (#425) 1 月之前
  ayangweb 54364565e2 feat: right-click menu support for search (#423) 1 月之前
  BiggerRain ee4a06b6de feat: web components assistant (#422) 1 月之前
  ayangweb 9715a92f36 refactor: change the selected background color of the item (#421) 1 月之前
  BiggerRain 2caeb4090a docs: update README (#418) 1 月之前
  ayangweb 983e65ee61 refactor: right-click menu returns to execute whichever one is selected (#417) 1 月之前
  BiggerRain ec37cfe68f fix: current conversation tip (#416) 1 月之前
  BiggerRain db66d81bd0 fix: fixed several search & chat bugs (#412) 1 月之前
  ayangweb 5b0fdbcb2c feat: support esc to exit right-click menu (#415) 1 月之前
  ayangweb 88955e0b95 feat: ai assistant supports shortcuts (#414) 1 月之前
  SteveLauC aee7df608f refactor: use timeout value specified in settings in query_coco_fusion() (#413) 1 月之前
  SteveLauC 6d8fa81141 revert: Document constructor changed in #399 (#410) 1 月之前
  ayangweb d67d6645fe feat: automatically selects the first entry after searching (#411) 1 月之前
  ayangweb 6329354243 feat: add keyboard event handling and double-click copying (#409) 1 月之前
  ayangweb 3ef5226e11 refactor: add empty data prompt to search scope (#406) 1 月之前
  ayangweb eebf49d7e0 refactor: optimize the style of the calculator (#405) 1 月之前
  BiggerRain 04903a09cd build: build web components and publish (#404) 1 月之前
  ayangweb 44b5f8400e feat: added support for the calculator function (#399) 1 月之前
  ayangweb 77e6b58381 refactor: show placeholder image when history is empty (#398) 1 月之前
  BiggerRain f6e5e826fd chore: update assistant icon & think mode (#397) 1 月之前
  SteveLauC 886400bcbc fix: correct datasource ID in returned documents (#396) 1 月之前
  BiggerRain 53258ee834 feat: add support for switching AI assistants (#395) 1 月之前
共有 75 个文件被更改,包括 2561 次插入1361 次删除
  1. 2 2
      .env
  2. 59 26
      README.md
  3. 10 1
      docs/content.en/docs/release-notes/_index.md
  4. 二进制
      public/assets/calculator.png
  5. 0 0
      public/assets/fonts/icons/iconfont.js
  6. 二进制
      public/assets/no_data_dark.png
  7. 二进制
      public/assets/no_data_light.png
  8. 73 1
      src-tauri/Cargo.lock
  9. 3 0
      src-tauri/Cargo.toml
  10. 22 10
      src-tauri/src/assistant/mod.rs
  11. 9 2
      src-tauri/src/common/document.rs
  12. 1 1
      src-tauri/src/common/search.rs
  13. 5 1
      src-tauri/src/lib.rs
  14. 15 12
      src-tauri/src/local/application.rs
  15. 163 0
      src-tauri/src/local/calculator.rs
  16. 2 1
      src-tauri/src/local/mod.rs
  17. 13 2
      src-tauri/src/search/mod.rs
  18. 1 0
      src-tauri/src/server/mod.rs
  19. 15 0
      src-tauri/src/server/system_settings.rs
  20. 9 8
      src/api/axiosRequest.ts
  21. 34 15
      src/commands/servers.ts
  22. 208 0
      src/components/Assistant/AssistantList.tsx
  23. 15 21
      src/components/Assistant/Chat.tsx
  24. 3 0
      src/components/Assistant/ChatContent.tsx
  25. 13 281
      src/components/Assistant/ChatHeader.tsx
  26. 5 1
      src/components/Assistant/Greetings.tsx
  27. 255 0
      src/components/Assistant/ServerList.tsx
  28. 159 0
      src/components/Assistant/Splash.tsx
  29. 1 1
      src/components/ChatMessage/DeepRead.tsx
  30. 1 1
      src/components/ChatMessage/PickSource.tsx
  31. 8 26
      src/components/ChatMessage/PrevSuggestion.tsx
  32. 1 1
      src/components/ChatMessage/QueryIntent.tsx
  33. 33 10
      src/components/ChatMessage/index.tsx
  34. 0 1
      src/components/Cloud/Cloud.tsx
  35. 3 3
      src/components/Cloud/Sidebar.tsx
  36. 188 179
      src/components/Common/HistoryList/index.tsx
  37. 10 0
      src/components/Common/Icons/TypeIcon.tsx
  38. 18 0
      src/components/Common/NoDataImage.tsx
  39. 56 0
      src/components/Search/Calculator.tsx
  40. 120 47
      src/components/Search/ContextMenu.tsx
  41. 65 3
      src/components/Search/DocumentDetail.tsx
  42. 82 42
      src/components/Search/DocumentList.tsx
  43. 119 68
      src/components/Search/DropdownList.tsx
  44. 31 24
      src/components/Search/InputBox.tsx
  45. 36 19
      src/components/Search/ListRight.tsx
  46. 75 33
      src/components/Search/Search.tsx
  47. 14 23
      src/components/Search/SearchListItem.tsx
  48. 145 174
      src/components/Search/SearchPopover.tsx
  49. 4 6
      src/components/Search/SearchResults.tsx
  50. 28 33
      src/components/SearchChat/index.tsx
  51. 12 0
      src/components/Settings/Advanced/components/Shortcuts/index.tsx
  52. 31 2
      src/components/Settings/Advanced/index.tsx
  53. 4 0
      src/constants/index.ts
  54. 32 24
      src/hooks/useChatActions.ts
  55. 23 0
      src/hooks/useClickAway.ts
  56. 24 10
      src/hooks/useEscape.ts
  57. 33 0
      src/hooks/useFeatureControl.ts
  58. 18 0
      src/hooks/useSyncStore.ts
  59. 18 19
      src/hooks/useWebSocket.ts
  60. 27 2
      src/locales/en/translation.json
  61. 27 2
      src/locales/zh/translation.json
  62. 0 1
      src/pages/chat/index.tsx
  63. 1 43
      src/pages/main/index.tsx
  64. 0 7
      src/pages/web/README.md
  65. 7 64
      src/pages/web/index.tsx
  66. 8 5
      src/stores/appStore.ts
  67. 101 69
      src/stores/connectStore.ts
  68. 5 0
      src/stores/shortcutsStore.ts
  69. 2 0
      src/types/platform.ts
  70. 7 1
      src/utils/index.ts
  71. 2 2
      src/utils/platformAdapter.ts
  72. 3 3
      src/utils/webAdapter.ts
  73. 2 1
      src/utils/wrappers/tauriWrappers.ts
  74. 1 1
      tsup.config.ts
  75. 41 26
      vite.config.ts

+ 2 - 2
.env

@@ -1,3 +1,3 @@
-COCO_SERVER_URL=https://coco.infini.cloud  #http://localhost:9000
+COCO_SERVER_URL=http://localhost:9000 #https://coco.infini.cloud  #http://localhost:9000
 
 
-COCO_WEBSOCKET_URL=wss://coco.infini.cloud/ws #ws://localhost:9000/ws
+COCO_WEBSOCKET_URL=ws://localhost:9000/ws #wss://coco.infini.cloud/ws #ws://localhost:9000/ws

+ 59 - 26
README.md

@@ -1,7 +1,15 @@
 # Coco AI - Connect & Collaborate
 # Coco AI - Connect & Collaborate
 
 
+<div align="center">
+
 **Tagline**: _"Coco AI - search, connect, collaborate – all in one place."_
 **Tagline**: _"Coco AI - search, connect, collaborate – all in one place."_
 
 
+Visit our website: [https://coco.rs](https://coco.rs)
+
+[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Tauri 2.0](https://img.shields.io/badge/Tauri-2.0-blue)](https://tauri.app/) [![React](https://img.shields.io/badge/React-18-blue)](https://react.dev/) [![TypeScript](https://img.shields.io/badge/TypeScript-5-blue)](https://www.typescriptlang.org/) [![Rust](https://img.shields.io/badge/Rust-latest-orange)](https://www.rust-lang.org/) [![Node](https://img.shields.io/badge/Node-%3E%3D18.12-green)](https://nodejs.org/) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/infinilabs/coco-app/pulls) [![Version](https://img.shields.io/github/v/release/infinilabs/coco-app)](https://github.com/infinilabs/coco-app/releases) [![Build Status](https://img.shields.io/github/actions/workflow/status/infinilabs/coco-app/ci.yml)](https://github.com/infinilabs/coco-app/actions) [![Discord](https://img.shields.io/discord/1122384609359966313)](https://discord.com/invite/4tKTMkkvVX)
+
+</div>
+
 Coco AI is a unified search platform that connects all your enterprise applications and data—Google Workspace, Dropbox,
 Coco AI is a unified search platform that connects all your enterprise applications and data—Google Workspace, Dropbox,
 Confluent Wiki, GitHub, and more—into a single, powerful search interface. This repository contains the **Coco App**,
 Confluent Wiki, GitHub, and more—into a single, powerful search interface. This repository contains the **Coco App**,
 built for both **desktop and mobile**. The app allows users to search and interact with their enterprise data across
 built for both **desktop and mobile**. The app allows users to search and interact with their enterprise data across
@@ -12,20 +20,15 @@ and internal resources. Coco enhances collaboration by making information instan
 insights based on your enterprise's specific data.
 insights based on your enterprise's specific data.
 
 
 > **Note**: Backend services, including data indexing and search functionality, are handled in a
 > **Note**: Backend services, including data indexing and search functionality, are handled in a
-> separate [repository](https://github.com/infinilabs/coco-server).
-
+separate [repository](https://github.com/infinilabs/coco-server).
 
 
-![](./docs/static/img/coco-preview.gif)
+![Coco AI](./docs/static/img/coco-preview.gif)
 
 
+## 🚀 Vision
 
 
-## Vision
+At Coco AI, we aim to streamline workplace collaboration by centralizing access to enterprise data. The Coco App provides a seamless, cross-platform experience, enabling teams to easily search, connect, and collaborate within their workspace.
 
 
-At Coco AI, we aim to streamline workplace collaboration by centralizing access to enterprise data. The Coco
-App
-provides a seamless, cross-platform experience, enabling teams to easily search, connect, and collaborate within their
-workspace.
-
-## Use Cases
+## 💡 Use Cases
 
 
 - **Unified Search Across Platforms**: Coco integrates with all your enterprise apps, letting you search documents,
 - **Unified Search Across Platforms**: Coco integrates with all your enterprise apps, letting you search documents,
   conversations, and files across Google Workspace, Dropbox, GitHub, etc.
   conversations, and files across Google Workspace, Dropbox, GitHub, etc.
@@ -36,37 +39,67 @@ workspace.
 - **Simplified Data Access**: By removing the friction between various tools, Coco enhances your workflow and increases
 - **Simplified Data Access**: By removing the friction between various tools, Coco enhances your workflow and increases
   productivity.
   productivity.
 
 
-## Getting Started
+## ✨ Key Features
+
+- 🔍 **Unified Search**: One-stop enterprise search with multi-platform integration
+  - Supports major collaboration platforms: Google Workspace, Dropbox, Confluence Wiki, GitHub, etc.
+  - Real-time search across documents, conversations, and files
+  - Smart search intent understanding with relevance ranking
+  - Cross-platform data correlation and context display
+- 🤖 **AI-Powered Chat**: Team-specific ChatGPT-like assistant trained on your enterprise data
+- 🌐 **Cross-Platform**: Available for Windows, macOS, Linux and Web
+- 🔒 **Security-First**: Support for private deployment and data sovereignty
+- ⚡ **High Performance**: Built with Rust and Tauri 2.0
+- 🎨 **Modern UI**: Sleek interface designed for productivity
+
+## 🛠️ Technology Stack
+
+- **Frontend**: React + TypeScript
+- **Desktop Framework**: Tauri 2.0
+- **Styling**: Tailwind CSS
+- **State Management**: Zustand
+- **Build Tool**: Vite
 
 
-### Initial Setup
+## 🚀 Getting Started
 
 
-**This version of pnpm requires at least Node.js v18.12**
+### Prerequisites
 
 
-To set up the Coco App for development:
+- Node.js >= 18.12
+- Rust (latest stable)
+- pnpm (package manager)
+
+### Development Setup
 
 
 ```bash
 ```bash
-cd coco-app
+# Install pnpm
 npm install -g pnpm
 npm install -g pnpm
+
+# Install dependencies
 pnpm install
 pnpm install
+
+# Start development server
 pnpm tauri dev
 pnpm tauri dev
 ```
 ```
 
 
-#### Desktop Development:
-
-To start desktop development, run:
+### Production Build
 
 
 ```bash
 ```bash
-pnpm tauri dev
+pnpm tauri build
 ```
 ```
 
 
-## Documentation
-
-For full documentation on Coco AI, please visit the [Coco AI Documentation](https://docs.infinilabs.com/coco-app/main/).
+## 📚 Documentation
 
 
-## License
+- [Coco App Documentation](https://docs.infinilabs.com/coco-app/main/)
+- [Coco Server Documentation](https://docs.infinilabs.com/coco-server/main/)
+- [Tauri Documentation](https://tauri.app/)
 
 
-Coco AI is an open-source project licensed under
-the [MIT License](https://github.com/infinilabs/coco-app/blob/main/LICENSE).
+## 📄 License
 
 
-This means that you can freely use, modify, and
+Coco AI is an open-source project licensed under the [MIT License](LICENSE). You can freely use, modify, and
 distribute the software for both personal and commercial purposes, including hosting it on your own servers.
 distribute the software for both personal and commercial purposes, including hosting it on your own servers.
+
+---
+
+<div align="center">
+Built with ❤️ by <a href="https://infinilabs.com">INFINI Labs</a>
+</div>

+ 10 - 1
docs/content.en/docs/release-notes/_index.md

@@ -24,14 +24,21 @@ Information about release notes of Coco Server is provided here.
 - feat: add application management to the plugin #374
 - feat: add application management to the plugin #374
 - feat: add keyboard-only operation to history list #385
 - feat: add keyboard-only operation to history list #385
 - feat: add error notification #386
 - feat: add error notification #386
+- feat: add support for AI assistant #394
+- feat: add support for calculator function #399
+- feat: auto selects the first item after searching #411
+- feat: web components assistant #422
+- feat: right-click menu support for search #423
+- feat: add chat mode launch page #424
 
 
 ### Bug fix
 ### Bug fix
 
 
 - fix: fixed the problem of not being able to search in secondary directories #338
 - fix: fixed the problem of not being able to search in secondary directories #338
 - fix: active shadow setting #354
 - fix: active shadow setting #354
 - fix: chat history was not show up #377
 - fix: chat history was not show up #377
-- fix: get attachments in chat sessioins
+- fix: get attachments in chat sessions
 - fix: filter http query_args and convert only supported values
 - fix: filter http query_args and convert only supported values
+- fix:fixed several search & chat bugs #412
 
 
 ### Improvements
 ### Improvements
 
 
@@ -41,6 +48,8 @@ Information about release notes of Coco Server is provided here.
 - style: modify the style #370
 - style: modify the style #370
 - style: search list details display #378
 - style: search list details display #378
 - refactor: refactoring api error handling #382
 - refactor: refactoring api error handling #382
+- chore: update assistant icon & think mode #397
+- build: build web components and publish #404
 
 
 ## 0.3.0 (2025-03-31)
 ## 0.3.0 (2025-03-31)
 
 

二进制
public/assets/calculator.png


文件差异内容过多而无法显示
+ 0 - 0
public/assets/fonts/icons/iconfont.js


二进制
public/assets/no_data_dark.png


二进制
public/assets/no_data_light.png


+ 73 - 1
src-tauri/Cargo.lock

@@ -416,7 +416,7 @@ dependencies = [
  "anyhow",
  "anyhow",
  "arrayvec",
  "arrayvec",
  "log",
  "log",
- "nom",
+ "nom 7.1.3",
  "num-rational",
  "num-rational",
  "v_frame",
  "v_frame",
 ]
 ]
@@ -723,6 +723,24 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
 checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
 
 
+[[package]]
+name = "chinese-number"
+version = "0.7.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49fccaef6346f6d6a741908d3b79fe97c2debe2fbb5eb3a7d00ff5981b52bb6c"
+dependencies = [
+ "chinese-variant",
+ "enum-ordinalize",
+ "num-bigint",
+ "num-traits",
+]
+
+[[package]]
+name = "chinese-variant"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7588475145507237ded760e52bf2f1085495245502033756d28ea72ade0e498b"
+
 [[package]]
 [[package]]
 name = "chrono"
 name = "chrono"
 version = "0.4.40"
 version = "0.4.40"
@@ -770,6 +788,7 @@ dependencies = [
  "applications",
  "applications",
  "async-trait",
  "async-trait",
  "base64 0.13.1",
  "base64 0.13.1",
+ "chinese-number",
  "dirs 5.0.1",
  "dirs 5.0.1",
  "env_logger",
  "env_logger",
  "futures",
  "futures",
@@ -781,7 +800,9 @@ dependencies = [
  "hyper 0.14.32",
  "hyper 0.14.32",
  "lazy_static",
  "lazy_static",
  "log",
  "log",
+ "meval",
  "notify",
  "notify",
+ "num2words",
  "once_cell",
  "once_cell",
  "ordered-float",
  "ordered-float",
  "pizza-common",
  "pizza-common",
@@ -1478,6 +1499,26 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
 checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
 
 
+[[package]]
+name = "enum-ordinalize"
+version = "4.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5"
+dependencies = [
+ "enum-ordinalize-derive",
+]
+
+[[package]]
+name = "enum-ordinalize-derive"
+version = "4.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+]
+
 [[package]]
 [[package]]
 name = "enumflags2"
 name = "enumflags2"
 version = "0.7.11"
 version = "0.7.11"
@@ -3283,6 +3324,16 @@ dependencies = [
  "autocfg",
  "autocfg",
 ]
 ]
 
 
+[[package]]
+name = "meval"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f79496a5651c8d57cd033c5add8ca7ee4e3d5f7587a4777484640d9cb60392d9"
+dependencies = [
+ "fnv",
+ "nom 1.2.4",
+]
+
 [[package]]
 [[package]]
 name = "mime"
 name = "mime"
 version = "0.3.17"
 version = "0.3.17"
@@ -3437,6 +3488,12 @@ version = "0.1.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
 checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
 
 
+[[package]]
+name = "nom"
+version = "1.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce"
+
 [[package]]
 [[package]]
 name = "nom"
 name = "nom"
 version = "7.1.3"
 version = "7.1.3"
@@ -3471,6 +3528,12 @@ dependencies = [
  "windows-sys 0.45.0",
  "windows-sys 0.45.0",
 ]
 ]
 
 
+[[package]]
+name = "num-bigfloat"
+version = "1.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793a9a2afbf2b4fc1b9a47de731031b06ed362a5ede3681fbfbaeb2ad4faaa13"
+
 [[package]]
 [[package]]
 name = "num-bigint"
 name = "num-bigint"
 version = "0.4.6"
 version = "0.4.6"
@@ -3538,6 +3601,15 @@ dependencies = [
  "autocfg",
  "autocfg",
 ]
 ]
 
 
+[[package]]
+name = "num2words"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ab78b987a0e1e6cf869a443f7f1c9dc696117d47780227e961e742d0b45d706"
+dependencies = [
+ "num-bigfloat",
+]
+
 [[package]]
 [[package]]
 name = "num_enum"
 name = "num_enum"
 version = "0.7.3"
 version = "0.7.3"

+ 3 - 0
src-tauri/Cargo.toml

@@ -74,6 +74,9 @@ tungstenite = "0.24.0"
 env_logger = "0.11.5"
 env_logger = "0.11.5"
 tokio-util = "0.7.14"
 tokio-util = "0.7.14"
 tauri-plugin-windows-version = "2"
 tauri-plugin-windows-version = "2"
+meval = "0.2"
+chinese-number = "0.7"
+num2words = "1"
 
 
 [target."cfg(target_os = \"macos\")".dependencies]
 [target."cfg(target_os = \"macos\")".dependencies]
 tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }
 tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }

+ 22 - 10
src-tauri/src/assistant/mod.rs

@@ -35,7 +35,6 @@ pub async fn chat_history<R: Runtime>(
             format!("Error get history: {}", e)
             format!("Error get history: {}", e)
         })?;
         })?;
 
 
-
     common::http::get_response_body_text(response).await
     common::http::get_response_body_text(response).await
 }
 }
 
 
@@ -135,17 +134,18 @@ pub async fn new_chat<R: Runtime>(
     let mut headers = HashMap::new();
     let mut headers = HashMap::new();
     headers.insert("WEBSOCKET-SESSION-ID".to_string(), websocket_id.into());
     headers.insert("WEBSOCKET-SESSION-ID".to_string(), websocket_id.into());
 
 
-    let response = HttpClient::advanced_post(&server_id, "/chat/_new", Some(headers), query_params, body)
-        .await
-        .map_err(|e| format!("Error sending message: {}", e))?;
+    let response =
+        HttpClient::advanced_post(&server_id, "/chat/_new", Some(headers), query_params, body)
+            .await
+            .map_err(|e| format!("Error sending message: {}", e))?;
 
 
     let text = response
     let text = response
         .text()
         .text()
         .await
         .await
         .map_err(|e| format!("Failed to read response body: {}", e))?;
         .map_err(|e| format!("Failed to read response body: {}", e))?;
 
 
-    let chat_response: GetResponse = serde_json::from_str(&text)
-        .map_err(|e| format!("Failed to parse response JSON: {}", e))?;
+    let chat_response: GetResponse =
+        serde_json::from_str(&text).map_err(|e| format!("Failed to parse response JSON: {}", e))?;
 
 
     if chat_response.result != "created" {
     if chat_response.result != "created" {
         return Err(format!("Unexpected result: {}", chat_response.result));
         return Err(format!("Unexpected result: {}", chat_response.result));
@@ -179,8 +179,8 @@ pub async fn send_message<R: Runtime>(
         query_params,
         query_params,
         Some(body),
         Some(body),
     )
     )
-        .await
-        .map_err(|e| format!("Error cancel session: {}", e))?;
+    .await
+    .map_err(|e| format!("Error cancel session: {}", e))?;
 
 
     common::http::get_response_body_text(response).await
     common::http::get_response_body_text(response).await
 }
 }
@@ -222,8 +222,20 @@ pub async fn update_session_chat(
         None,
         None,
         Some(reqwest::Body::from(serde_json::to_string(&body).unwrap())),
         Some(reqwest::Body::from(serde_json::to_string(&body).unwrap())),
     )
     )
-        .await
-        .map_err(|e| format!("Error updating session: {}", e))?;
+    .await
+    .map_err(|e| format!("Error updating session: {}", e))?;
 
 
     Ok(response.status().is_success())
     Ok(response.status().is_success())
 }
 }
+
+#[tauri::command]
+pub async fn assistant_search<R: Runtime>(
+    _app_handle: AppHandle<R>,
+    server_id: String,
+) -> Result<String, String> {
+    let response = HttpClient::get(&server_id, "/assistant/_search", None)
+        .await
+        .map_err(|e| format!("Error searching assistants: {}", e))?;
+
+    common::http::get_response_body_text(response).await
+}

+ 9 - 2
src-tauri/src/common/document.rs

@@ -29,7 +29,7 @@ pub struct EditorInfo {
     pub timestamp: Option<String>,
     pub timestamp: Option<String>,
 }
 }
 
 
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
 pub struct Document {
 pub struct Document {
     pub id: String,
     pub id: String,
     pub created: Option<String>,
     pub created: Option<String>,
@@ -55,8 +55,15 @@ pub struct Document {
     pub owner: Option<UserInfo>,
     pub owner: Option<UserInfo>,
     pub last_updated_by: Option<EditorInfo>,
     pub last_updated_by: Option<EditorInfo>,
 }
 }
+
 impl Document {
 impl Document {
-    pub fn new(source: Option<DataSourceReference>, id: String, category: String, name: String, url: String) -> Self {
+    pub fn new(
+        source: Option<DataSourceReference>,
+        id: String,
+        category: String,
+        name: String,
+        url: String,
+    ) -> Self {
         Self {
         Self {
             id,
             id,
             created: None,
             created: None,

+ 1 - 1
src-tauri/src/common/search.rs

@@ -50,7 +50,7 @@ where
 {
 {
     let body_text = get_response_body_text(response).await?;
     let body_text = get_response_body_text(response).await?;
 
 
-    dbg!(&body_text);
+    // dbg!(&body_text);
 
 
     let search_response: SearchResponse<T> = serde_json::from_str(&body_text)
     let search_response: SearchResponse<T> = serde_json::from_str(&body_text)
         .map_err(|e| format!("Failed to deserialize search response: {}", e))?;
         .map_err(|e| format!("Failed to deserialize search response: {}", e))?;

+ 5 - 1
src-tauri/src/lib.rs

@@ -125,6 +125,7 @@ pub fn run() {
             assistant::cancel_session_chat,
             assistant::cancel_session_chat,
             assistant::delete_session_chat,
             assistant::delete_session_chat,
             assistant::update_session_chat,
             assistant::update_session_chat,
+            assistant::assistant_search,
             // server::get_coco_server_datasources,
             // server::get_coco_server_datasources,
             // server::get_coco_server_connectors,
             // server::get_coco_server_connectors,
             server::websocket::connect_to_server,
             server::websocket::connect_to_server,
@@ -136,7 +137,8 @@ pub fn run() {
             server::transcription::transcription,
             server::transcription::transcription,
             local::application::get_default_search_paths,
             local::application::get_default_search_paths,
             local::application::list_app_with_metadata_in,
             local::application::list_app_with_metadata_in,
-            util::open
+            util::open,
+            server::system_settings::get_system_settings
         ])
         ])
         .setup(|app| {
         .setup(|app| {
             let registry = SearchSourceRegistry::default();
             let registry = SearchSourceRegistry::default();
@@ -245,10 +247,12 @@ pub async fn init<R: Runtime>(app_handle: &AppHandle<R>) {
 async fn init_app_search_source<R: Runtime>(app_handle: &AppHandle<R>) -> Result<(), String> {
 async fn init_app_search_source<R: Runtime>(app_handle: &AppHandle<R>) -> Result<(), String> {
     let application_search =
     let application_search =
         local::application::ApplicationSearchSource::new(app_handle.clone(), 1000f64).await?;
         local::application::ApplicationSearchSource::new(app_handle.clone(), 1000f64).await?;
+    let calculator_search = local::calculator::CalculatorSource::new(2000f64);
 
 
     // Register the application search source
     // Register the application search source
     let registry = app_handle.state::<SearchSourceRegistry>();
     let registry = app_handle.state::<SearchSourceRegistry>();
     registry.register_source(application_search).await;
     registry.register_source(application_search).await;
+    registry.register_source(calculator_search).await;
 
 
     Ok(())
     Ok(())
 }
 }

+ 15 - 12
src-tauri/src/local/application.rs

@@ -11,6 +11,8 @@ use std::path::PathBuf;
 use tauri::{AppHandle, Runtime};
 use tauri::{AppHandle, Runtime};
 use tauri_plugin_fs_pro::{icon, metadata, name, IconOptions};
 use tauri_plugin_fs_pro::{icon, metadata, name, IconOptions};
 
 
+const DATA_SOURCE_ID: &str = "Applications";
+
 #[tauri::command]
 #[tauri::command]
 pub fn get_default_search_paths() -> Vec<String> {
 pub fn get_default_search_paths() -> Vec<String> {
     #[cfg(target_os = "macos")]
     #[cfg(target_os = "macos")]
@@ -232,7 +234,7 @@ impl SearchSource for ApplicationSearchSource {
                 .unwrap_or("My Computer".into())
                 .unwrap_or("My Computer".into())
                 .to_string_lossy()
                 .to_string_lossy()
                 .into(),
                 .into(),
-            id: "local_applications".into(),
+            id: DATA_SOURCE_ID.into(),
         }
         }
     }
     }
 
 
@@ -279,18 +281,19 @@ impl SearchSource for ApplicationSearchSource {
                 let app_path_string = app_path.to_string_lossy().into_owned();
                 let app_path_string = app_path.to_string_lossy().into_owned();
 
 
                 total_hits += 1;
                 total_hits += 1;
+
                 let mut doc = Document::new(
                 let mut doc = Document::new(
-                    Some(DataSourceReference {
-                        r#type: Some(LOCAL_QUERY_SOURCE_TYPE.into()),
-                        name: Some("Applications".into()),
-                        id: Some(app_name.clone()),
-                        icon: None,
-                    }),
-                    app_path_string.clone(),
-                    "Application".to_string(),
-                    app_name.clone(),
-                    app_path_string.clone(),
-                );
+                  Some(DataSourceReference {
+                      r#type: Some(LOCAL_QUERY_SOURCE_TYPE.into()),
+                      name: Some(DATA_SOURCE_ID.into()),
+                      id: Some(DATA_SOURCE_ID.into()),
+                      icon: None,
+                  }),
+                  app_path_string.clone(),
+                  "Application".to_string(),
+                  app_name.clone(),
+                  app_path_string.clone(),
+              );
 
 
                 // Attach icon if available
                 // Attach icon if available
                 if let Some(icon_path) = self.icons.get(app_name.as_str()) {
                 if let Some(icon_path) = self.icons.get(app_name.as_str()) {

+ 163 - 0
src-tauri/src/local/calculator.rs

@@ -0,0 +1,163 @@
+use super::LOCAL_QUERY_SOURCE_TYPE;
+use crate::common::{
+    document::{DataSourceReference, Document},
+    error::SearchError,
+    search::{QueryResponse, QuerySource, SearchQuery},
+    traits::SearchSource,
+};
+use async_trait::async_trait;
+use chinese_number::{ChineseCase, ChineseCountMethod, ChineseVariant, NumberToChinese};
+use num2words::Num2Words;
+use serde_json::Value;
+use std::collections::HashMap;
+
+const DATA_SOURCE_ID: &str = "Calculator";
+
+pub struct CalculatorSource {
+    base_score: f64,
+}
+
+impl CalculatorSource {
+    pub fn new(base_score: f64) -> Self {
+        CalculatorSource { base_score }
+    }
+}
+
+fn parse_query(query: String) -> Value {
+    let mut query_json = serde_json::Map::new();
+
+    let operators = ["+", "-", "*", "/", "%"];
+
+    let found_operators: Vec<_> = query
+        .chars()
+        .filter(|c| operators.contains(&c.to_string().as_str()))
+        .collect();
+
+    if found_operators.len() == 1 {
+        let operation = match found_operators[0] {
+            '+' => "sum",
+            '-' => "subtract",
+            '*' => "multiply",
+            '/' => "divide",
+            '%' => "remainder",
+            _ => "expression",
+        };
+
+        query_json.insert("type".to_string(), Value::String(operation.to_string()));
+    } else {
+        query_json.insert("type".to_string(), Value::String("expression".to_string()));
+    }
+
+    query_json.insert("value".to_string(), Value::String(query));
+
+    Value::Object(query_json)
+}
+
+fn parse_result(num: f64) -> Value {
+    let mut result_json = serde_json::Map::new();
+
+    let to_zh = num
+        .to_chinese(
+            ChineseVariant::Simple,
+            ChineseCase::Upper,
+            ChineseCountMethod::TenThousand,
+        )
+        .unwrap_or(num.to_string());
+
+    let to_en = Num2Words::new(num)
+        .to_words()
+        .map(|s| {
+            let mut chars = s.chars();
+            let mut result = String::new();
+            let mut capitalize = true;
+
+            while let Some(c) = chars.next() {
+                if c == ' ' || c == '-' {
+                    result.push(c);
+                    capitalize = true;
+                } else if capitalize {
+                    result.extend(c.to_uppercase());
+                    capitalize = false;
+                } else {
+                    result.push(c);
+                }
+            }
+
+            result
+        })
+        .unwrap_or(num.to_string());
+
+    result_json.insert("value".to_string(), Value::String(num.to_string()));
+    result_json.insert("toZh".to_string(), Value::String(to_zh));
+    result_json.insert("toEn".to_string(), Value::String(to_en));
+
+    Value::Object(result_json)
+}
+
+#[async_trait]
+impl SearchSource for CalculatorSource {
+    fn get_type(&self) -> QuerySource {
+        QuerySource {
+            r#type: LOCAL_QUERY_SOURCE_TYPE.into(),
+            name: hostname::get()
+                .unwrap_or(DATA_SOURCE_ID.into())
+                .to_string_lossy()
+                .into(),
+            id: DATA_SOURCE_ID.into(),
+        }
+    }
+
+    async fn search(&self, query: SearchQuery) -> Result<QueryResponse, SearchError> {
+        let query_string = query
+            .query_strings
+            .get("query")
+            .unwrap_or(&"".to_string())
+            .to_string();
+
+        if query_string.is_empty() || query_string.len() == 1 {
+            return Ok(QueryResponse {
+                source: self.get_type(),
+                hits: Vec::new(),
+                total_hits: 0,
+            });
+        }
+
+        match meval::eval_str(&query_string) {
+            Ok(num) => {
+                let mut payload: HashMap<String, Value> = HashMap::new();
+
+                let payload_query = parse_query(query_string);
+                let payload_result = parse_result(num);
+
+                payload.insert("query".to_string(), payload_query);
+                payload.insert("result".to_string(), payload_result);
+
+                let doc = Document {
+                    id: DATA_SOURCE_ID.to_string(),
+                    category: Some(DATA_SOURCE_ID.to_string()),
+                    payload: Some(payload),
+                    source: Some(DataSourceReference {
+                        r#type: Some(LOCAL_QUERY_SOURCE_TYPE.into()),
+                        name: Some(DATA_SOURCE_ID.into()),
+                        id: Some(DATA_SOURCE_ID.into()),
+                        icon: None,
+                    }),
+                    ..Default::default()
+                };
+
+                return Ok(QueryResponse {
+                    source: self.get_type(),
+                    hits: vec![(doc, self.base_score)],
+                    total_hits: 1,
+                });
+            }
+            Err(_) => {
+                return Ok(QueryResponse {
+                    source: self.get_type(),
+                    hits: Vec::new(),
+                    total_hits: 0,
+                });
+            }
+        };
+    }
+}

+ 2 - 1
src-tauri/src/local/mod.rs

@@ -1,4 +1,5 @@
 pub mod application;
 pub mod application;
+pub mod calculator;
 pub mod file_system;
 pub mod file_system;
 
 
-pub const LOCAL_QUERY_SOURCE_TYPE: &str = "local";
+pub const LOCAL_QUERY_SOURCE_TYPE: &str = "local";

+ 13 - 2
src-tauri/src/search/mod.rs

@@ -64,7 +64,10 @@ pub async fn query_coco_fusion<R: Runtime>(
     from: u64,
     from: u64,
     size: u64,
     size: u64,
     query_strings: HashMap<String, String>,
     query_strings: HashMap<String, String>,
+    query_timeout: u64,
 ) -> Result<MultiSourceQueryResponse, SearchError> {
 ) -> Result<MultiSourceQueryResponse, SearchError> {
+    let query_source_to_search = query_strings.get("querysource");
+
     let search_sources = app_handle.state::<SearchSourceRegistry>();
     let search_sources = app_handle.state::<SearchSourceRegistry>();
 
 
     let sources_future = search_sources.get_sources();
     let sources_future = search_sources.get_sources();
@@ -74,11 +77,19 @@ pub async fn query_coco_fusion<R: Runtime>(
     let sources_list = sources_future.await;
     let sources_list = sources_future.await;
 
 
     // Time limit for each query
     // Time limit for each query
-    let timeout_duration = Duration::from_millis(500); //TODO, settings
+    let timeout_duration = Duration::from_secs(query_timeout);
 
 
     // Push all queries into futures
     // Push all queries into futures
     for query_source in sources_list {
     for query_source in sources_list {
         let query_source_type = query_source.get_type().clone();
         let query_source_type = query_source.get_type().clone();
+
+        if let Some(query_source_to_search) = query_source_to_search {
+            // We should not search this data source
+            if &query_source_type.id != query_source_to_search {
+                continue;
+            }
+        }
+
         sources.insert(query_source_type.id.clone(), query_source_type);
         sources.insert(query_source_type.id.clone(), query_source_type);
 
 
         let query = SearchQuery::new(from, size, query_strings.clone());
         let query = SearchQuery::new(from, size, query_strings.clone());
@@ -89,7 +100,7 @@ pub async fn query_coco_fusion<R: Runtime>(
             timeout(timeout_duration, async {
             timeout(timeout_duration, async {
                 query_source_clone.search(query).await
                 query_source_clone.search(query).await
             })
             })
-                .await
+            .await
         }));
         }));
     }
     }
 
 

+ 1 - 0
src-tauri/src/server/mod.rs

@@ -8,5 +8,6 @@ pub mod http_client;
 pub mod profile;
 pub mod profile;
 pub mod search;
 pub mod search;
 pub mod servers;
 pub mod servers;
+pub mod system_settings;
 pub mod transcription;
 pub mod transcription;
 pub mod websocket;
 pub mod websocket;

+ 15 - 0
src-tauri/src/server/system_settings.rs

@@ -0,0 +1,15 @@
+use crate::server::http_client::HttpClient;
+use serde_json::Value;
+use tauri::command;
+
+#[command]
+pub async fn get_system_settings(server_id: String) -> Result<Value, String> {
+    let response = HttpClient::get(&server_id, "/settings", None)
+        .await
+        .map_err(|err| err.to_string())?;
+
+    response
+        .json::<Value>()
+        .await
+        .map_err(|err| err.to_string())
+}

+ 9 - 8
src/api/axiosRequest.ts

@@ -1,6 +1,6 @@
 import axios from "axios";
 import axios from "axios";
 
 
-import { useAppStore } from '@/stores/appStore';
+import { useAppStore } from "@/stores/appStore";
 
 
 import {
 import {
   handleChangeRequestHeader,
   handleChangeRequestHeader,
@@ -44,21 +44,22 @@ axios.interceptors.response.use(
 
 
 export const handleApiError = (error: any) => {
 export const handleApiError = (error: any) => {
   const addError = useAppStore.getState().addError;
   const addError = useAppStore.getState().addError;
-  
-  let message = 'Request failed';
-  
+
+  let message = "Request failed";
+
   if (error.response) {
   if (error.response) {
     // Server error response
     // Server error response
-    message = error.response.data?.message || `Error (${error.response.status})`;
+    message =
+      error.response.data?.message || `Error (${error.response.status})`;
   } else if (error.request) {
   } else if (error.request) {
     // Request failed to send
     // Request failed to send
-    message = 'Network connection failed';
+    message = "Network connection failed";
   } else {
   } else {
     // Other errors
     // Other errors
     message = error.message;
     message = error.message;
   }
   }
-  
-  addError(message, 'error');
+
+  addError(message, "error");
   return error;
   return error;
 };
 };
 
 

+ 34 - 15
src/commands/servers.ts

@@ -15,7 +15,7 @@ import {
   TranscriptionResponse,
   TranscriptionResponse,
   MultiSourceQueryResponse,
   MultiSourceQueryResponse,
 } from "@/types/commands";
 } from "@/types/commands";
-import { useAppStore } from '@/stores/appStore';
+import { useAppStore } from "@/stores/appStore";
 
 
 async function invokeWithErrorHandler<T>(
 async function invokeWithErrorHandler<T>(
   command: string,
   command: string,
@@ -26,27 +26,27 @@ async function invokeWithErrorHandler<T>(
     const result = await invoke<T>(command, args);
     const result = await invoke<T>(command, args);
     // console.log(command, result);
     // console.log(command, result);
 
 
-    if (result && typeof result === 'object' && 'failed' in result) {
+    if (result && typeof result === "object" && "failed" in result) {
       const failedResult = result as any;
       const failedResult = result as any;
       if (failedResult.failed?.length > 0) {
       if (failedResult.failed?.length > 0) {
         failedResult.failed.forEach((error: any) => {
         failedResult.failed.forEach((error: any) => {
           // addError(error.error, 'error');
           // addError(error.error, 'error');
-          console.log(error.error);
+          console.error(error.error);
         });
         });
       }
       }
     }
     }
 
 
-    if (typeof result === 'string') {
+    if (typeof result === "string") {
       const res = JSON.parse(result);
       const res = JSON.parse(result);
-      if (typeof res === 'string') {
+      if (typeof res === "string") {
         throw new Error(result);
         throw new Error(result);
       }
       }
     }
     }
 
 
     return result;
     return result;
   } catch (error: any) {
   } catch (error: any) {
-    const errorMessage = error || 'Command execution failed';
-    addError(errorMessage, 'error');
+    const errorMessage = error || "Command execution failed";
+    addError(command + ":" + errorMessage, "error");
     throw error;
     throw error;
   }
   }
 }
 }
@@ -234,7 +234,10 @@ export function send_message({
 }
 }
 
 
 export const delete_session_chat = (serverId: string, sessionId: string) => {
 export const delete_session_chat = (serverId: string, sessionId: string) => {
-  return invokeWithErrorHandler<boolean>(`delete_session_chat`, { serverId, sessionId });
+  return invokeWithErrorHandler<boolean>(`delete_session_chat`, {
+    serverId,
+    sessionId,
+  });
 };
 };
 
 
 export const update_session_chat = (payload: {
 export const update_session_chat = (payload: {
@@ -248,10 +251,19 @@ export const update_session_chat = (payload: {
   return invokeWithErrorHandler<boolean>("update_session_chat", payload);
   return invokeWithErrorHandler<boolean>("update_session_chat", payload);
 };
 };
 
 
+export const assistant_search = (payload: {
+  serverId: string;
+}): Promise<boolean> => {
+  return invokeWithErrorHandler<boolean>("assistant_search", payload);
+};
+
 export const upload_attachment = async (payload: UploadAttachmentPayload) => {
 export const upload_attachment = async (payload: UploadAttachmentPayload) => {
-  const response = await invokeWithErrorHandler<UploadAttachmentResponse>("upload_attachment", {
-    ...payload,
-  });
+  const response = await invokeWithErrorHandler<UploadAttachmentResponse>(
+    "upload_attachment",
+    {
+      ...payload,
+    }
+  );
 
 
   if (response?.acknowledged) {
   if (response?.acknowledged) {
     return response.attachments;
     return response.attachments;
@@ -259,7 +271,9 @@ export const upload_attachment = async (payload: UploadAttachmentPayload) => {
 };
 };
 
 
 export const get_attachment = (payload: GetAttachmentPayload) => {
 export const get_attachment = (payload: GetAttachmentPayload) => {
-  return invokeWithErrorHandler<GetAttachmentResponse>("get_attachment", { ...payload });
+  return invokeWithErrorHandler<GetAttachmentResponse>("get_attachment", {
+    ...payload,
+  });
 };
 };
 
 
 export const delete_attachment = (payload: DeleteAttachmentPayload) => {
 export const delete_attachment = (payload: DeleteAttachmentPayload) => {
@@ -267,13 +281,18 @@ export const delete_attachment = (payload: DeleteAttachmentPayload) => {
 };
 };
 
 
 export const transcription = (payload: TranscriptionPayload) => {
 export const transcription = (payload: TranscriptionPayload) => {
-  return invokeWithErrorHandler<TranscriptionResponse>("transcription", { ...payload });
+  return invokeWithErrorHandler<TranscriptionResponse>("transcription", {
+    ...payload,
+  });
 };
 };
 
 
 export const query_coco_fusion = (payload: {
 export const query_coco_fusion = (payload: {
   from: number;
   from: number;
   size: number;
   size: number;
-  query_strings: Record<string, string>;
+  queryStrings: Record<string, string>;
+  queryTimeout: number;
 }) => {
 }) => {
-  return invokeWithErrorHandler<MultiSourceQueryResponse>("query_coco_fusion", { ...payload });
+  return invokeWithErrorHandler<MultiSourceQueryResponse>("query_coco_fusion", {
+    ...payload,
+  });
 };
 };

+ 208 - 0
src/components/Assistant/AssistantList.tsx

@@ -0,0 +1,208 @@
+import { useEffect, useState, useRef, useCallback } from "react";
+import { ChevronDownIcon, RefreshCw, Check } from "lucide-react";
+import { useTranslation } from "react-i18next";
+
+import { useAppStore } from "@/stores/appStore";
+import logoImg from "@/assets/icon.svg";
+import platformAdapter from "@/utils/platformAdapter";
+import { useClickAway } from "@/hooks/useClickAway";
+import VisibleKey from "@/components/Common/VisibleKey";
+import { useConnectStore } from "@/stores/connectStore";
+import FontIcon from "@/components/Common/Icons/FontIcon";
+import { useChatStore } from "@/stores/chatStore";
+import { AI_ASSISTANT_PANEL_ID } from "@/constants";
+import { useShortcutsStore } from "@/stores/shortcutsStore";
+import { Get } from "@/api/axiosRequest";
+
+interface AssistantListProps {
+  assistantIDs?: string[];
+}
+
+export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
+  const { t } = useTranslation();
+  const { connected } = useChatStore();
+  const isTauri = useAppStore((state) => state.isTauri);
+  const assistantList = useConnectStore((state) => state.assistantList);
+  const setAssistantList = useConnectStore((state) => state.setAssistantList);
+  const currentService = useConnectStore((state) => state.currentService);
+  const currentAssistant = useConnectStore((state) => state.currentAssistant);
+  const setCurrentAssistant = useConnectStore(
+    (state) => state.setCurrentAssistant
+  );
+  const aiAssistant = useShortcutsStore((state) => state.aiAssistant);
+
+  const [isOpen, setIsOpen] = useState(false);
+  const [isRefreshing, setIsRefreshing] = useState(false);
+  const menuRef = useRef<HTMLDivElement>(null);
+
+  useClickAway(menuRef, () => setIsOpen(false));
+
+  const fetchAssistant = useCallback(async (serverId?: string) => {
+    let response: any;
+    if (isTauri) {
+      if (!serverId) return;
+      try {
+        response = await platformAdapter.commands("assistant_search", {
+          serverId,
+        });
+        response = response ? JSON.parse(response) : null;
+      } catch (err) {
+        setAssistantList([]);
+        setCurrentAssistant(null);
+        console.error("assistant_search", err);
+      }
+    } else {
+      const [error, res] = await Get(`/assistant/_search`);
+      if (error) {
+        setAssistantList([]);
+        setCurrentAssistant(null);
+        console.error("assistant_search", error);
+        return;
+      }
+      console.log("/assistant/_search", res);
+      response = res;
+    }
+    console.log("assistant_search", response);
+    let assistantList = response?.hits?.hits || [];
+
+    assistantList =
+      assistantIDs.length > 0
+        ? assistantList.filter((item: any) => assistantIDs.includes(item._id))
+        : assistantList;
+
+    setAssistantList(assistantList);
+    if (assistantList.length > 0) {
+      const assistant = assistantList.find(
+        (item: any) => item._id === currentAssistant?._id
+      );
+      if (assistant) {
+        setCurrentAssistant(assistant);
+      } else {
+        setCurrentAssistant(assistantList[0]);
+      }
+    }
+  }, []);
+
+  useEffect(() => {
+    connected && fetchAssistant(currentService?.id);
+  }, [connected, currentService?.id]);
+
+  const handleRefresh = useCallback(async () => {
+    setIsRefreshing(true);
+    await fetchAssistant(currentService?.id);
+    setTimeout(() => setIsRefreshing(false), 1000);
+  }, [currentService?.id]);
+
+  return (
+    <div className="relative" ref={menuRef}>
+      <button
+        onClick={() => setIsOpen(!isOpen)}
+        className="h-6  p-1 px-1.5 flex items-center gap-1 rounded-full bg-white dark:bg-[#202126] text-sm/6 font-semibold text-gray-800 dark:text-[#d8d8d8] border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none"
+      >
+        <div className="w-4 h-4 flex justify-center items-center rounded-full bg-white">
+          {currentAssistant?._source?.icon?.startsWith("font_") ? (
+            <FontIcon
+              name={currentAssistant._source.icon}
+              className="w-3 h-3"
+            />
+          ) : (
+            <img
+              src={logoImg}
+              className="w-3 h-3"
+              alt={t("assistant.message.logo")}
+            />
+          )}
+        </div>
+        <div className="max-w-[100px] truncate">
+          {currentAssistant?._source?.name || "Coco AI"}
+        </div>
+        <VisibleKey
+          aria-controls={isOpen ? AI_ASSISTANT_PANEL_ID : ""}
+          shortcut={aiAssistant}
+          onKeyPress={() => {
+            setIsOpen(!isOpen);
+          }}
+        >
+          <ChevronDownIcon
+            className={`size-4 text-gray-500 dark:text-gray-400 transition-transform ${
+              isOpen ? "rotate-180" : ""
+            }`}
+          />
+        </VisibleKey>
+      </button>
+
+      {isOpen && (
+        <div
+          id={isOpen ? AI_ASSISTANT_PANEL_ID : ""}
+          className="absolute z-50 top-full mt-1 left-0 w-64 rounded-xl bg-white dark:bg-[#202126] p-2 text-sm/6 text-gray-800 dark:text-white shadow-lg border border-gray-200 dark:border-gray-700 focus:outline-none max-h-[calc(100vh-80px)] overflow-y-auto"
+        >
+          <div className="sticky top-0 mb-2 px-2 py-1 text-sm font-medium text-gray-900 dark:text-white bg-white dark:bg-[#202126] flex justify-between">
+            <div>AI Assistant</div>
+            <button
+              onClick={handleRefresh}
+              className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400"
+              disabled={isRefreshing}
+            >
+              <VisibleKey shortcut="R" onKeyPress={handleRefresh}>
+                <RefreshCw
+                  className={`h-4 w-4 text-[#0287FF] transition-transform duration-1000 ${
+                    isRefreshing ? "animate-spin" : ""
+                  }`}
+                />
+              </VisibleKey>
+            </button>
+          </div>
+          {assistantList.map((assistant) => (
+            <button
+              key={assistant._id}
+              onClick={() => {
+                console.log("assistant", assistant);
+                setCurrentAssistant(assistant);
+                setIsOpen(false);
+              }}
+              className={`w-full flex items-center gap-2 rounded-lg p-1 py-1.5 mb-1 ${
+                currentAssistant?._id === assistant._id
+                  ? "bg-[#F3F4F6] dark:bg-[#1F2937]"
+                  : "hover:bg-[#F3F4F6] dark:hover:bg-[#1F2937]"
+              }
+              }`}
+            >
+              {assistant._source?.icon?.startsWith("font_") ? (
+                <div className="w-7 h-7 flex items-center justify-center rounded-full bg-white">
+                  <FontIcon
+                    name={assistant._source?.icon}
+                    className="w-5 h-5"
+                  />
+                </div>
+              ) : (
+                <img
+                  src={logoImg}
+                  className="w-5 h-5 rounded-full"
+                  alt={assistant.name}
+                />
+              )}
+              <div className="text-left flex-1 min-w-0">
+                <div className="font-medium text-gray-900 dark:text-white truncate">
+                  {assistant._source?.name || "-"}
+                </div>
+                <div className="text-xs text-gray-500 dark:text-gray-400 truncate">
+                  {assistant._source?.description || ""}
+                </div>
+              </div>
+              {currentAssistant?._id === assistant._id && (
+                <div className="flex items-center">
+                  <VisibleKey
+                    shortcut="↓↑"
+                    shortcutClassName="w-6 -translate-x-4"
+                  >
+                    <Check className="w-4 h-4 text-gray-500 dark:text-gray-400" />
+                  </VisibleKey>
+                </div>
+              )}
+            </button>
+          ))}
+        </div>
+      )}
+    </div>
+  );
+}

+ 15 - 21
src/components/Assistant/Chat.tsx

@@ -25,7 +25,6 @@ import PrevSuggestion from "@/components/ChatMessage/PrevSuggestion";
 import { useAppStore } from "@/stores/appStore";
 import { useAppStore } from "@/stores/appStore";
 
 
 interface ChatAIProps {
 interface ChatAIProps {
-  isTransitioned: boolean;
   isSearchActive?: boolean;
   isSearchActive?: boolean;
   isDeepThinkActive?: boolean;
   isDeepThinkActive?: boolean;
   activeChatProp?: Chat;
   activeChatProp?: Chat;
@@ -36,6 +35,7 @@ interface ChatAIProps {
   isChatPage?: boolean;
   isChatPage?: boolean;
   getFileUrl: (path: string) => string;
   getFileUrl: (path: string) => string;
   showChatHistory?: boolean;
   showChatHistory?: boolean;
+  assistantIDs?: string[];
 }
 }
 
 
 export interface ChatAIRef {
 export interface ChatAIRef {
@@ -49,7 +49,6 @@ const ChatAI = memo(
   forwardRef<ChatAIRef, ChatAIProps>(
   forwardRef<ChatAIRef, ChatAIProps>(
     (
     (
       {
       {
-        isTransitioned,
         changeInput,
         changeInput,
         isSearchActive,
         isSearchActive,
         isDeepThinkActive,
         isDeepThinkActive,
@@ -60,11 +59,10 @@ const ChatAI = memo(
         isChatPage = false,
         isChatPage = false,
         getFileUrl,
         getFileUrl,
         showChatHistory,
         showChatHistory,
+        assistantIDs,
       },
       },
       ref
       ref
     ) => {
     ) => {
-      if (!isTransitioned) return null;
-
       useImperativeHandle(ref, () => ({
       useImperativeHandle(ref, () => ({
         init: init,
         init: init,
         cancelChat: () => cancelChat(activeChat),
         cancelChat: () => cancelChat(activeChat),
@@ -76,6 +74,9 @@ const ChatAI = memo(
         useChatStore();
         useChatStore();
 
 
       const currentService = useConnectStore((state) => state.currentService);
       const currentService = useConnectStore((state) => state.currentService);
+      const visibleStartPage = useConnectStore((state) => {
+        return state.visibleStartPage;
+      });
 
 
       const addError = useAppStore.getState().addError;
       const addError = useAppStore.getState().addError;
 
 
@@ -201,10 +202,14 @@ const ChatAI = memo(
         async (value: string) => {
         async (value: string) => {
           try {
           try {
             console.log("init", isLogin, curChatEnd, activeChat?._id);
             console.log("init", isLogin, curChatEnd, activeChat?._id);
-            if (!isLogin || !curChatEnd) {
+            if (!isLogin) {
               addError("Please login to continue chatting");
               addError("Please login to continue chatting");
               return;
               return;
             }
             }
+            if (!curChatEnd) {
+              addError("Please wait for the current conversation to complete");
+              return;
+            }
             setShowPrevSuggestion(false);
             setShowPrevSuggestion(false);
             if (!activeChat?._id) {
             if (!activeChat?._id) {
               await createNewChat(value, activeChat, websocketSessionId);
               await createNewChat(value, activeChat, websocketSessionId);
@@ -307,20 +312,6 @@ const ChatAI = memo(
         };
         };
       }, [isSidebarOpenChat, handleOutsideClick]);
       }, [isSidebarOpenChat, handleOutsideClick]);
 
 
-      // const fetchChatHistory = useCallback(async () => {
-      //   const hits = await getChatHistory();
-      //   setChats(hits);
-      // }, [getChatHistory]);
-
-      const setIsLoginChat = useCallback(
-        (value: boolean) => {
-          setIsLogin(value);
-          value && currentService && !setIsSidebarOpen && getChatHistory();
-          !value && setChats([]);
-        },
-        [currentService, setIsSidebarOpen, getChatHistory]
-      );
-
       const toggleSidebar = useCallback(() => {
       const toggleSidebar = useCallback(() => {
         setIsSidebarOpenChat(!isSidebarOpenChat);
         setIsSidebarOpenChat(!isSidebarOpenChat);
         setIsSidebarOpen && setIsSidebarOpen(!isSidebarOpenChat);
         setIsSidebarOpen && setIsSidebarOpen(!isSidebarOpenChat);
@@ -388,8 +379,9 @@ const ChatAI = memo(
             reconnect={reconnect}
             reconnect={reconnect}
             isChatPage={isChatPage}
             isChatPage={isChatPage}
             isLogin={isLogin}
             isLogin={isLogin}
-            setIsLogin={setIsLoginChat}
+            setIsLogin={setIsLogin}
             showChatHistory={showChatHistory}
             showChatHistory={showChatHistory}
+            assistantIDs={assistantIDs}
           />
           />
           {isLogin ? (
           {isLogin ? (
             <ChatContent
             <ChatContent
@@ -413,7 +405,9 @@ const ChatAI = memo(
             <ConnectPrompt />
             <ConnectPrompt />
           )}
           )}
 
 
-          {showPrevSuggestion ? <PrevSuggestion sendMessage={init} /> : null}
+          {showPrevSuggestion && !visibleStartPage && (
+            <PrevSuggestion sendMessage={init} />
+          )}
         </div>
         </div>
       );
       );
     }
     }

+ 3 - 0
src/components/Assistant/ChatContent.tsx

@@ -10,6 +10,7 @@ import type { Chat, IChunkData } from "./types";
 // import SessionFile from "./SessionFile";
 // import SessionFile from "./SessionFile";
 import { useConnectStore } from "@/stores/connectStore";
 import { useConnectStore } from "@/stores/connectStore";
 import SessionFile from "./SessionFile";
 import SessionFile from "./SessionFile";
+import Splash from "./Splash";
 
 
 interface ChatContentProps {
 interface ChatContentProps {
   activeChat?: Chat;
   activeChat?: Chat;
@@ -145,6 +146,8 @@ export const ChatContent = ({
       )}
       )}
 
 
       {sessionId && <SessionFile sessionId={sessionId} />}
       {sessionId && <SessionFile sessionId={sessionId} />}
+
+      <Splash />
     </div>
     </div>
   );
   );
 };
 };

+ 13 - 281
src/components/Assistant/ChatHeader.tsx

@@ -1,39 +1,18 @@
-import {
-  MessageSquarePlus,
-  ChevronDownIcon,
-  Settings,
-  RefreshCw,
-  Check,
-  Server,
-} from "lucide-react";
-import { useState, useEffect, useCallback, useRef } from "react";
-import {
-  Menu,
-  MenuButton,
-  MenuItem,
-  MenuItems,
-  Popover,
-  PopoverButton,
-  PopoverPanel,
-} from "@headlessui/react";
-import { useTranslation } from "react-i18next";
+import { MessageSquarePlus } from "lucide-react";
 import clsx from "clsx";
 import clsx from "clsx";
-import { useKeyPress } from "ahooks";
 
 
-import logoImg from "@/assets/icon.svg";
 import HistoryIcon from "@/icons/History";
 import HistoryIcon from "@/icons/History";
 import PinOffIcon from "@/icons/PinOff";
 import PinOffIcon from "@/icons/PinOff";
 import PinIcon from "@/icons/Pin";
 import PinIcon from "@/icons/Pin";
-import ServerIcon from "@/icons/Server";
 import WindowsFullIcon from "@/icons/WindowsFull";
 import WindowsFullIcon from "@/icons/WindowsFull";
 import { useAppStore, IServer } from "@/stores/appStore";
 import { useAppStore, IServer } from "@/stores/appStore";
-import { useChatStore } from "@/stores/chatStore";
 import type { Chat } from "./types";
 import type { Chat } from "./types";
-import { useConnectStore } from "@/stores/connectStore";
 import platformAdapter from "@/utils/platformAdapter";
 import platformAdapter from "@/utils/platformAdapter";
 import VisibleKey from "../Common/VisibleKey";
 import VisibleKey from "../Common/VisibleKey";
 import { useShortcutsStore } from "@/stores/shortcutsStore";
 import { useShortcutsStore } from "@/stores/shortcutsStore";
 import { HISTORY_PANEL_ID } from "@/constants";
 import { HISTORY_PANEL_ID } from "@/constants";
+import { AssistantList } from "./AssistantList";
+import { ServerList } from "./ServerList";
 
 
 interface ChatHeaderProps {
 interface ChatHeaderProps {
   onCreateNewChat: () => void;
   onCreateNewChat: () => void;
@@ -46,6 +25,7 @@ interface ChatHeaderProps {
   setIsLogin: (isLogin: boolean) => void;
   setIsLogin: (isLogin: boolean) => void;
   isChatPage?: boolean;
   isChatPage?: boolean;
   showChatHistory?: boolean;
   showChatHistory?: boolean;
+  assistantIDs?: string[];
 }
 }
 
 
 export function ChatHeader({
 export function ChatHeader({
@@ -59,21 +39,11 @@ export function ChatHeader({
   setIsLogin,
   setIsLogin,
   isChatPage = false,
   isChatPage = false,
   showChatHistory = true,
   showChatHistory = true,
+  assistantIDs,
 }: ChatHeaderProps) {
 }: ChatHeaderProps) {
-  const { t } = useTranslation();
-
-  const setEndpoint = useAppStore((state) => state.setEndpoint);
   const isPinned = useAppStore((state) => state.isPinned);
   const isPinned = useAppStore((state) => state.isPinned);
   const setIsPinned = useAppStore((state) => state.setIsPinned);
   const setIsPinned = useAppStore((state) => state.setIsPinned);
 
 
-  const { setMessages } = useChatStore();
-
-  const [serverList, setServerList] = useState<IServer[]>([]);
-  const [isRefreshing, setIsRefreshing] = useState(false);
-
-  const currentService = useConnectStore((state) => state.currentService);
-  const setCurrentService = useConnectStore((state) => state.setCurrentService);
-
   const isTauri = useAppStore((state) => state.isTauri);
   const isTauri = useAppStore((state) => state.isTauri);
   const historicalRecords = useShortcutsStore((state) => {
   const historicalRecords = useShortcutsStore((state) => {
     return state.historicalRecords;
     return state.historicalRecords;
@@ -84,79 +54,8 @@ export function ChatHeader({
   const fixedWindow = useShortcutsStore((state) => {
   const fixedWindow = useShortcutsStore((state) => {
     return state.fixedWindow;
     return state.fixedWindow;
   });
   });
-  const serviceList = useShortcutsStore((state) => state.serviceList);
-  const external = useShortcutsStore((state) => state.external);
-  const serverListButtonRef = useRef<HTMLButtonElement>(null);
-
-  const fetchServers = useCallback(
-    async (resetSelection: boolean) => {
-      platformAdapter
-        .commands("list_coco_servers")
-        .then((res: any) => {
-          const enabledServers = (res as IServer[]).filter(
-            (server) => server.enabled !== false
-          );
-          //console.log("list_coco_servers", enabledServers);
-          setServerList(enabledServers);
-
-          if (resetSelection && enabledServers.length > 0) {
-            const currentServiceExists = enabledServers.find(
-              (server) => server.id === currentService?.id
-            );
-
-            if (currentServiceExists) {
-              switchServer(currentServiceExists);
-            } else {
-              switchServer(enabledServers[enabledServers.length - 1]);
-            }
-          }
-        })
-        .catch((err: any) => {
-          console.error(err);
-        });
-    },
-    [currentService?.id]
-  );
 
 
-  useEffect(() => {
-    isTauri && fetchServers(true);
-
-    const unlisten = platformAdapter.listenEvent("login_or_logout", (event) => {
-      console.log("Login or Logout:", currentService, event.payload);
-      if (event.payload !== isLogin) {
-        setIsLogin(!!event.payload);
-      }
-      fetchServers(true);
-    });
-
-    return () => {
-      // Cleanup logic if needed
-      unlisten.then((fn) => fn());
-    };
-  }, []);
-
-  const switchServer = async (server: IServer) => {
-    if (!server) return;
-    try {
-      // Switch UI first, then switch server connection
-      setCurrentService(server);
-      setEndpoint(server.endpoint);
-      setMessages(""); // Clear previous messages
-      onCreateNewChat();
-      //
-      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
-      // src-tauri/src/server/websocket.rs
-      reconnect && reconnect(server);
-    } catch (error) {
-      console.error("switchServer:", error);
-    }
-  };
+  const external = useShortcutsStore((state) => state.external);
 
 
   const togglePin = async () => {
   const togglePin = async () => {
     try {
     try {
@@ -169,37 +68,6 @@ export function ChatHeader({
     }
     }
   };
   };
 
 
-  const openSettings = async () => {
-    platformAdapter.emitEvent("open_settings", "connect");
-  };
-
-  useKeyPress(["uparrow", "downarrow"], (_, key) => {
-    const isOpen = serverListButtonRef.current?.dataset["open"] != null;
-    const length = serverList.length;
-
-    if (!isOpen || length <= 1) return;
-
-    const currentIndex = serverList.findIndex((server) => {
-      return server.id === currentService?.id;
-    });
-
-    let nextIndex = currentIndex;
-
-    if (key === "uparrow") {
-      nextIndex = currentIndex > 0 ? currentIndex - 1 : length - 1;
-    } else if (key === "downarrow") {
-      nextIndex = currentIndex < serverList.length - 1 ? currentIndex + 1 : 0;
-    }
-
-    switchServer(serverList[nextIndex]);
-  });
-
-  const handleRefresh = async () => {
-    setIsRefreshing(true);
-    await fetchServers(false);
-    setTimeout(() => setIsRefreshing(false), 1000);
-  };
-
   return (
   return (
     <header
     <header
       className="flex items-center justify-between py-2 px-3"
       className="flex items-center justify-between py-2 px-3"
@@ -225,38 +93,7 @@ export function ChatHeader({
           </button>
           </button>
         )}
         )}
 
 
-        <Menu>
-          <MenuButton className="px-2 flex items-center gap-1 rounded-full bg-white dark:bg-[#202126] p-1 text-sm/6 font-semibold text-gray-800 dark:text-white border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none">
-            <img
-              src={logoImg}
-              className="w-4 h-4"
-              alt={t("assistant.message.logo")}
-            />
-            Coco AI
-            {showChatHistory && isTauri ? (
-              <ChevronDownIcon className="size-4 text-gray-500 dark:text-gray-400" />
-            ) : null}
-          </MenuButton>
-
-          {showChatHistory && isTauri ? (
-            <MenuItems
-              transition
-              anchor="bottom end"
-              className="w-28 origin-top-right rounded-xl bg-white dark:bg-[#202126] p-1 text-sm/6 text-gray-800 dark:text-white shadow-lg border border-gray-200 dark:border-gray-700 focus:outline-none data-[closed]:scale-95 data-[closed]:opacity-0"
-            >
-              <MenuItem>
-                <button className="group flex w-full items-center gap-2 rounded-lg py-1.5 px-3 hover:bg-gray-100 dark:hover:bg-gray-700">
-                  <img
-                    src={logoImg}
-                    className="w-4 h-4"
-                    alt={t("assistant.message.logo")}
-                  />
-                  Coco AI
-                </button>
-              </MenuItem>
-            </MenuItems>
-          ) : null}
-        </Menu>
+        <AssistantList assistantIDs={assistantIDs} />
 
 
         {showChatHistory ? (
         {showChatHistory ? (
           <button
           <button
@@ -291,117 +128,12 @@ export function ChatHeader({
             </VisibleKey>
             </VisibleKey>
           </button>
           </button>
 
 
-          <Popover className="relative">
-            <PopoverButton
-              ref={serverListButtonRef}
-              className="flex items-center"
-            >
-              <VisibleKey
-                shortcut={serviceList}
-                onKeyPress={() => {
-                  serverListButtonRef.current?.click();
-                }}
-              >
-                <ServerIcon />
-              </VisibleKey>
-            </PopoverButton>
-
-            <PopoverPanel className="absolute right-0 z-10 mt-2 min-w-[240px] bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700">
-              <div className="p-3">
-                <div className="flex items-center justify-between mb-3 whitespace-nowrap">
-                  <h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
-                    Servers
-                  </h3>
-                  <div className="flex items-center gap-2">
-                    <button
-                      onClick={openSettings}
-                      className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400"
-                    >
-                      <VisibleKey shortcut=",">
-                        <Settings className="h-4 w-4 text-[#0287FF]" />
-                      </VisibleKey>
-                    </button>
-                    <button
-                      onClick={handleRefresh}
-                      className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400"
-                      disabled={isRefreshing}
-                    >
-                      <VisibleKey shortcut="R" onKeyPress={handleRefresh}>
-                        <RefreshCw
-                          className={`h-4 w-4 text-[#0287FF] transition-transform duration-1000 ${
-                            isRefreshing ? "animate-spin" : ""
-                          }`}
-                        />
-                      </VisibleKey>
-                    </button>
-                  </div>
-                </div>
-                <div className="space-y-1">
-                  {serverList.length > 0 ? (
-                    serverList.map((server) => (
-                      <div
-                        key={server.id}
-                        onClick={() => switchServer(server)}
-                        className={`w-full flex items-center justify-between gap-1 p-2 rounded-lg transition-colors whitespace-nowrap ${
-                          currentService?.id === server.id
-                            ? "bg-gray-100 dark:bg-gray-800"
-                            : "hover:bg-gray-50 dark:hover:bg-gray-800/50"
-                        }`}
-                      >
-                        <div className="flex items-center gap-2 overflow-hidden min-w-0">
-                          <img
-                            src={server?.provider?.icon || logoImg}
-                            alt={server.name}
-                            className="w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800"
-                          />
-                          <div className="text-left flex-1 min-w-0">
-                            <div className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate max-w-[200px]">
-                              {server.name}
-                            </div>
-                            <div className="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[200px]">
-                              AI Assistant: {server.assistantCount || 1}
-                            </div>
-                          </div>
-                        </div>
-                        <div className="flex flex-col items-center gap-2">
-                          <span
-                            className={`w-3 h-3 rounded-full ${
-                              server.health?.status
-                                ? `bg-[${server.health?.status}]`
-                                : "bg-gray-400 dark:bg-gray-600"
-                            }`}
-                          />
-                          <div className="size-4 flex justify-end">
-                            {currentService?.id === server.id && (
-                              <VisibleKey
-                                shortcut="↓↑"
-                                shortcutClassName="w-6 -translate-x-4"
-                              >
-                                <Check className="w-full h-full text-gray-500 dark:text-gray-400" />
-                              </VisibleKey>
-                            )}
-                          </div>
-                        </div>
-                      </div>
-                    ))
-                  ) : (
-                    <div className="flex flex-col items-center justify-center py-6 text-center">
-                      <Server className="w-8 h-8 text-gray-400 dark:text-gray-600 mb-2" />
-                      <p className="text-sm text-gray-500 dark:text-gray-400">
-                        {t("assistant.chat.noServers")}
-                      </p>
-                      <button
-                        onClick={openSettings}
-                        className="mt-2 text-xs text-[#0287FF] hover:underline"
-                      >
-                        {t("assistant.chat.addServer")}
-                      </button>
-                    </div>
-                  )}
-                </div>
-              </div>
-            </PopoverPanel>
-          </Popover>
+          <ServerList
+            isLogin={isLogin}
+            setIsLogin={setIsLogin}
+            reconnect={reconnect}
+            onCreateNewChat={onCreateNewChat}
+          />
 
 
           {isChatPage ? null : (
           {isChatPage ? null : (
             <button className="inline-flex" onClick={onOpenChatAI}>
             <button className="inline-flex" onClick={onOpenChatAI}>

+ 5 - 1
src/components/Assistant/Greetings.tsx

@@ -1,9 +1,11 @@
 import { useTranslation } from "react-i18next";
 import { useTranslation } from "react-i18next";
 
 
 import { ChatMessage } from "@/components/ChatMessage";
 import { ChatMessage } from "@/components/ChatMessage";
+import { useConnectStore } from "@/stores/connectStore";
 
 
 export const Greetings = () => {
 export const Greetings = () => {
   const { t } = useTranslation();
   const { t } = useTranslation();
+  const currentAssistant = useConnectStore((state) => state.currentAssistant);
 
 
   return (
   return (
     <ChatMessage
     <ChatMessage
@@ -12,7 +14,9 @@ export const Greetings = () => {
         _id: "greetings",
         _id: "greetings",
         _source: {
         _source: {
           type: "assistant",
           type: "assistant",
-          message: t("assistant.chat.greetings"),
+          message:
+            currentAssistant?._source?.chat_settings?.greeting_message ||
+            t("assistant.chat.greetings"),
         },
         },
       }}
       }}
     />
     />

+ 255 - 0
src/components/Assistant/ServerList.tsx

@@ -0,0 +1,255 @@
+import { useState, useCallback, useEffect, useRef } from "react";
+import { Settings, RefreshCw, Check, Server } from "lucide-react";
+import { useTranslation } from "react-i18next";
+import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
+import { useKeyPress } from "ahooks";
+
+import logoImg from "@/assets/icon.svg";
+import ServerIcon from "@/icons/Server";
+import VisibleKey from "../Common/VisibleKey";
+import { useShortcutsStore } from "@/stores/shortcutsStore";
+import platformAdapter from "@/utils/platformAdapter";
+import { useAppStore, IServer } from "@/stores/appStore";
+import { useChatStore } from "@/stores/chatStore";
+import { useConnectStore } from "@/stores/connectStore";
+
+interface ServerListProps {
+  isLogin: boolean;
+  setIsLogin: (isLogin: boolean) => void;
+  reconnect: (server?: IServer) => void;
+  onCreateNewChat: () => void;
+}
+
+export function ServerList({
+  isLogin,
+  setIsLogin,
+  reconnect,
+  onCreateNewChat,
+}: ServerListProps) {
+  const { t } = useTranslation();
+
+  const serviceList = useShortcutsStore((state) => state.serviceList);
+  const setEndpoint = useAppStore((state) => state.setEndpoint);
+  const setCurrentService = useConnectStore((state) => state.setCurrentService);
+  const isTauri = useAppStore((state) => state.isTauri);
+  const currentService = useConnectStore((state) => state.currentService);
+
+  const { setMessages } = useChatStore();
+
+  const [serverList, setServerList] = useState<IServer[]>([]);
+  const [isRefreshing, setIsRefreshing] = useState(false);
+
+  const serverListButtonRef = useRef<HTMLButtonElement>(null);
+
+  const fetchServers = useCallback(
+    async (resetSelection: boolean) => {
+      platformAdapter
+        .commands("list_coco_servers")
+        .then((res: any) => {
+          const enabledServers = (res as IServer[]).filter(
+            (server) => server.enabled !== false
+          );
+          //console.log("list_coco_servers", enabledServers);
+          setServerList(enabledServers);
+
+          if (resetSelection && enabledServers.length > 0) {
+            const currentServiceExists = enabledServers.find(
+              (server) => server.id === currentService?.id
+            );
+
+            if (currentServiceExists) {
+              switchServer(currentServiceExists);
+            } else {
+              switchServer(enabledServers[enabledServers.length - 1]);
+            }
+          }
+        })
+        .catch((err: any) => {
+          console.error(err);
+        });
+    },
+    [currentService?.id]
+  );
+
+  useEffect(() => {
+    isTauri && fetchServers(true);
+
+    const unlisten = platformAdapter.listenEvent("login_or_logout", (event) => {
+      console.log("Login or Logout:", currentService, event.payload);
+      if (event.payload !== isLogin) {
+        setIsLogin(!!event.payload);
+      }
+      fetchServers(true);
+    });
+
+    return () => {
+      // Cleanup logic if needed
+      unlisten.then((fn) => fn());
+    };
+  }, []);
+
+  const handleRefresh = async () => {
+    setIsRefreshing(true);
+    await fetchServers(false);
+    setTimeout(() => setIsRefreshing(false), 1000);
+  };
+
+  const openSettings = async () => {
+    platformAdapter.emitEvent("open_settings", "connect");
+  };
+
+  const switchServer = async (server: IServer) => {
+    if (!server) return;
+    try {
+      // Switch UI first, then switch server connection
+      setCurrentService(server);
+      setEndpoint(server.endpoint);
+      setMessages(""); // Clear previous messages
+      onCreateNewChat();
+      //
+      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
+      // src-tauri/src/server/websocket.rs
+      reconnect && reconnect(server);
+    } catch (error) {
+      console.error("switchServer:", error);
+    }
+  };
+
+  useKeyPress(["uparrow", "downarrow"], (_, key) => {
+    const isOpen = serverListButtonRef.current?.dataset["open"] != null;
+    const length = serverList.length;
+
+    if (!isOpen || length <= 1) return;
+
+    const currentIndex = serverList.findIndex((server) => {
+      return server.id === currentService?.id;
+    });
+
+    let nextIndex = currentIndex;
+
+    if (key === "uparrow") {
+      nextIndex = currentIndex > 0 ? currentIndex - 1 : length - 1;
+    } else if (key === "downarrow") {
+      nextIndex = currentIndex < serverList.length - 1 ? currentIndex + 1 : 0;
+    }
+
+    switchServer(serverList[nextIndex]);
+  });
+
+  return (
+    <Popover className="relative">
+      <PopoverButton ref={serverListButtonRef} className="flex items-center">
+        <VisibleKey
+          shortcut={serviceList}
+          onKeyPress={() => {
+            serverListButtonRef.current?.click();
+          }}
+        >
+          <ServerIcon />
+        </VisibleKey>
+      </PopoverButton>
+
+      <PopoverPanel className="absolute right-0 z-10 mt-2 min-w-[240px] bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700">
+        <div className="p-3">
+          <div className="flex items-center justify-between mb-3 whitespace-nowrap">
+            <h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
+              Servers
+            </h3>
+            <div className="flex items-center gap-2">
+              <button
+                onClick={openSettings}
+                className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400"
+              >
+                <VisibleKey shortcut=",">
+                  <Settings className="h-4 w-4 text-[#0287FF]" />
+                </VisibleKey>
+              </button>
+              <button
+                onClick={handleRefresh}
+                className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400"
+                disabled={isRefreshing}
+              >
+                <VisibleKey shortcut="R" onKeyPress={handleRefresh}>
+                  <RefreshCw
+                    className={`h-4 w-4 text-[#0287FF] transition-transform duration-1000 ${
+                      isRefreshing ? "animate-spin" : ""
+                    }`}
+                  />
+                </VisibleKey>
+              </button>
+            </div>
+          </div>
+          <div className="space-y-1">
+            {serverList.length > 0 ? (
+              serverList.map((server) => (
+                <div
+                  key={server.id}
+                  onClick={() => switchServer(server)}
+                  className={`w-full flex items-center justify-between gap-1 p-2 rounded-lg transition-colors whitespace-nowrap ${
+                    currentService?.id === server.id
+                      ? "bg-gray-100 dark:bg-gray-800"
+                      : "hover:bg-gray-50 dark:hover:bg-gray-800/50"
+                  }`}
+                >
+                  <div className="flex items-center gap-2 overflow-hidden min-w-0">
+                    <img
+                      src={server?.provider?.icon || logoImg}
+                      alt={server.name}
+                      className="w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800"
+                    />
+                    <div className="text-left flex-1 min-w-0">
+                      <div className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate max-w-[200px]">
+                        {server.name}
+                      </div>
+                      <div className="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[200px]">
+                        AI Assistant: {server.assistantCount || 1}
+                      </div>
+                    </div>
+                  </div>
+                  <div className="flex flex-col items-center gap-2">
+                    <span
+                      className={`w-3 h-3 rounded-full ${
+                        server.health?.status
+                          ? `bg-[${server.health?.status}]`
+                          : "bg-gray-400 dark:bg-gray-600"
+                      }`}
+                    />
+                    <div className="size-4 flex justify-end">
+                      {currentService?.id === server.id && (
+                        <VisibleKey
+                          shortcut="↓↑"
+                          shortcutClassName="w-6 -translate-x-4"
+                        >
+                          <Check className="w-full h-full text-gray-500 dark:text-gray-400" />
+                        </VisibleKey>
+                      )}
+                    </div>
+                  </div>
+                </div>
+              ))
+            ) : (
+              <div className="flex flex-col items-center justify-center py-6 text-center">
+                <Server className="w-8 h-8 text-gray-400 dark:text-gray-600 mb-2" />
+                <p className="text-sm text-gray-500 dark:text-gray-400">
+                  {t("assistant.chat.noServers")}
+                </p>
+                <button
+                  onClick={openSettings}
+                  className="mt-2 text-xs text-[#0287FF] hover:underline"
+                >
+                  {t("assistant.chat.addServer")}
+                </button>
+              </div>
+            )}
+          </div>
+        </div>
+      </PopoverPanel>
+    </Popover>
+  );
+}

+ 159 - 0
src/components/Assistant/Splash.tsx

@@ -0,0 +1,159 @@
+import { CircleX, MoveRight } from "lucide-react";
+import { useMount } from "ahooks";
+import { useAppStore } from "@/stores/appStore";
+import { useMemo, useState } from "react";
+import platformAdapter from "@/utils/platformAdapter";
+import { useConnectStore } from "@/stores/connectStore";
+import { useThemeStore } from "@/stores/themeStore";
+import FontIcon from "../Common/Icons/FontIcon";
+import logoImg from "@/assets/icon.svg";
+import { Get } from "@/api/axiosRequest";
+
+interface StartPage {
+  enabled?: boolean;
+  logo?: {
+    light?: string;
+    dark?: string;
+  };
+  introduction?: string;
+  display_assistants?: string[];
+}
+
+export interface Response {
+  app_settings?: {
+    chat?: {
+      start_page?: StartPage;
+    };
+  };
+}
+
+const Splash = () => {
+  const isTauri = useAppStore((state) => state.isTauri);
+  const currentService = useConnectStore((state) => state.currentService);
+  const [settings, setSettings] = useState<StartPage>();
+  const visibleStartPage = useConnectStore((state) => state.visibleStartPage);
+  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 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 settingsAssistantList = useMemo(() => {
+    console.log("assistantList", assistantList);
+
+    return assistantList.filter((item) => {
+      return settings?.display_assistants?.includes(item?._source?.id);
+    });
+  }, [settings, assistantList]);
+
+  const logo = useMemo(() => {
+    const { light, dark } = settings?.logo || {};
+
+    if (isDark) {
+      return dark || light;
+    }
+
+    return light || dark;
+  }, [settings, isDark]);
+
+  return (
+    visibleStartPage && (
+      <div className="absolute inset-0 flex flex-col items-center px-6 pt-6 text-[#333] dark:text-white">
+        <CircleX
+          className="absolute top-3 right-3 size-4 text-[#999] cursor-pointer"
+          onClick={() => {
+            setVisibleStartPage(false);
+          }}
+        />
+
+        <img src={logo} className="h-8" />
+
+        <div className="mt-3 mb-6 text-lg font-medium">
+          {settings?.introduction}
+        </div>
+
+        <ul className="flex flex-wrap -m-1 w-full">
+          {settingsAssistantList?.map((item) => {
+            const { id, name, description, icon } = item._source;
+
+            return (
+              <li key={id} className="w-1/2 p-1">
+                <div
+                  className="group h-[74px] px-3 py-2 text-sm rounded-xl border dark:border-[#262626] bg-white dark:bg-black cursor-pointer transition hover:!border-[#0087FF]"
+                  onClick={() => {
+                    setCurrentAssistant(item);
+
+                    setVisibleStartPage(false);
+                  }}
+                >
+                  <div className="flex items-center justify-between">
+                    <div className="flex items-center gap-1">
+                      {icon?.startsWith("font_") ? (
+                        <div className="size-4 flex items-center justify-center rounded-full bg-white">
+                          <FontIcon name={icon} className="w-5 h-5" />
+                        </div>
+                      ) : (
+                        <img
+                          src={logoImg}
+                          className="size-4 rounded-full"
+                          alt={name}
+                        />
+                      )}
+
+                      <span>{name}</span>
+                    </div>
+
+                    <MoveRight className="size-4 transition group-hover:text-[#0087FF]" />
+                  </div>
+
+                  <div className="mt-1 text-xs text-[#999] line-clamp-2">
+                    {description}
+                  </div>
+                </div>
+              </li>
+            );
+          })}
+        </ul>
+      </div>
+    )
+  );
+};
+
+export default Splash;

+ 1 - 1
src/components/ChatMessage/DeepRead.tsx

@@ -84,7 +84,7 @@ export const DeepRead = ({
         )}
         )}
       </button>
       </button>
       {isThinkingExpanded && (
       {isThinkingExpanded && (
-        <div className="pl-2 border-l-2 border-[e5e5e5]">
+        <div className="pl-2 border-l-2 border-[#e5e5e5] dark:border-[#4e4e56]">
           <div className="text-[#8b8b8b] dark:text-[#a6a6a6] space-y-2">
           <div className="text-[#8b8b8b] dark:text-[#a6a6a6] space-y-2">
             <div className="mb-4 space-y-3 text-xs">
             <div className="mb-4 space-y-3 text-xs">
               {Data?.map((item) => (
               {Data?.map((item) => (

+ 1 - 1
src/components/ChatMessage/PickSource.tsx

@@ -103,7 +103,7 @@ export const PickSource = ({
         )}
         )}
       </button>
       </button>
       {isThinkingExpanded && (
       {isThinkingExpanded && (
-        <div className="pl-2 border-l-2 border-[e5e5e5]">
+        <div className="pl-2 border-l-2 border-[#e5e5e5] dark:border-[#4e4e56]">
           <div className="text-[#8b8b8b] dark:text-[#a6a6a6] space-y-2">
           <div className="text-[#8b8b8b] dark:text-[#a6a6a6] space-y-2">
             <div className="mb-4 space-y-3 text-xs">
             <div className="mb-4 space-y-3 text-xs">
               {Data?.map((item) => (
               {Data?.map((item) => (

+ 8 - 26
src/components/ChatMessage/PrevSuggestion.tsx

@@ -1,8 +1,7 @@
 import { MoveRight } from "lucide-react";
 import { MoveRight } from "lucide-react";
 import { FC, useEffect, useState } from "react";
 import { FC, useEffect, useState } from "react";
 
 
-import { Get } from "@/api/axiosRequest";
-import { useAppStore } from "@/stores/appStore";
+import { useConnectStore } from "@/stores/connectStore";
 
 
 interface PrevSuggestionProps {
 interface PrevSuggestionProps {
   sendMessage: (message: string) => void;
   sendMessage: (message: string) => void;
@@ -11,35 +10,18 @@ interface PrevSuggestionProps {
 const PrevSuggestion: FC<PrevSuggestionProps> = (props) => {
 const PrevSuggestion: FC<PrevSuggestionProps> = (props) => {
   const { sendMessage } = props;
   const { sendMessage } = props;
 
 
-  const isTauri = useAppStore((state) => state.isTauri);
-
-  const headersStr = localStorage.getItem("headers") || "{}";
-  const headers = JSON.parse(headersStr);
-  const id = headers["APP-INTEGRATION-ID"] || "cvkm9hmhpcemufsg3vug";
-  // console.log("id", id);
+  const currentAssistant = useConnectStore((state) => state.currentAssistant);
 
 
   const [list, setList] = useState<string[]>([]);
   const [list, setList] = useState<string[]>([]);
 
 
   useEffect(() => {
   useEffect(() => {
-    if (!isTauri) getList();
-  }, [id]);
-
-  const getList = async () => {
-    if (!id) return;
-
-    const url = `/integration/${id}/chat/_suggest`;
-
-    const [error, res] = await Get(`/integration/${id}/chat/_suggest`);
-
-    if (error) {
-      console.error(url, error);
-      return setList([]);
+    const suggested = currentAssistant?._source?.chat_settings?.suggested || {};
+    if (suggested.enabled) {
+      setList(suggested.questions || []);
+    } else {
+      setList([]);
     }
     }
-
-    console.log("chat/_suggest", res);
-
-    setList(Array.isArray(res) ? res : []);
-  };
+  }, [JSON.stringify(currentAssistant)]);
 
 
   return (
   return (
     <ul className="absolute left-2 bottom-2 flex flex-col gap-2">
     <ul className="absolute left-2 bottom-2 flex flex-col gap-2">

+ 1 - 1
src/components/ChatMessage/QueryIntent.tsx

@@ -97,7 +97,7 @@ export const QueryIntent = ({
         )}
         )}
       </button>
       </button>
       {isThinkingExpanded && (
       {isThinkingExpanded && (
-        <div className="pl-2 border-l-2 border-[e5e5e5]">
+        <div className="pl-2 border-l-2 border-[#e5e5e5] dark:border-[#4e4e56]">
           <div className="text-[#8b8b8b] dark:text-[#a6a6a6] space-y-2">
           <div className="text-[#8b8b8b] dark:text-[#a6a6a6] space-y-2">
             <div className="mb-4 space-y-2 text-xs">
             <div className="mb-4 space-y-2 text-xs">
               {Data?.keyword ? (
               {Data?.keyword ? (

+ 33 - 10
src/components/ChatMessage/index.tsx

@@ -12,6 +12,9 @@ import { MessageActions } from "./MessageActions";
 import Markdown from "./Markdown";
 import Markdown from "./Markdown";
 import { SuggestionList } from "./SuggestionList";
 import { SuggestionList } from "./SuggestionList";
 import { UserMessage } from "./UserMessage";
 import { UserMessage } from "./UserMessage";
+import { useConnectStore } from "@/stores/connectStore";
+import FontIcon from "@/components/Common/Icons/FontIcon";
+import clsx from "clsx";
 
 
 interface ChatMessageProps {
 interface ChatMessageProps {
   message: Message;
   message: Message;
@@ -40,6 +43,8 @@ export const ChatMessage = memo(function ChatMessage({
 }: ChatMessageProps) {
 }: ChatMessageProps) {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
+  const currentAssistant = useConnectStore((state) => state.currentAssistant);
+
   const isAssistant = message?._source?.type === "assistant";
   const isAssistant = message?._source?.type === "assistant";
   const messageContent = message?._source?.message || "";
   const messageContent = message?._source?.message || "";
   const details = message?._source?.details || [];
   const details = message?._source?.details || [];
@@ -49,6 +54,7 @@ export const ChatMessage = memo(function ChatMessage({
     isTyping === false && (messageContent || response?.message_chunk);
     isTyping === false && (messageContent || response?.message_chunk);
 
 
   const [suggestion, setSuggestion] = useState<string[]>([]);
   const [suggestion, setSuggestion] = useState<string[]>([]);
+  const visibleStartPage = useConnectStore((state) => state.visibleStartPage);
 
 
   const getSuggestion = (suggestion: string[]) => {
   const getSuggestion = (suggestion: string[]) => {
     setSuggestion(suggestion);
     setSuggestion(suggestion);
@@ -117,7 +123,13 @@ export const ChatMessage = memo(function ChatMessage({
 
 
   return (
   return (
     <div
     <div
-      className={`py-8 flex ${isAssistant ? "justify-start" : "justify-end"}`}
+      className={clsx(
+        "py-8 flex",
+        [isAssistant ? "justify-start" : "justify-end"],
+        {
+          hidden: visibleStartPage,
+        }
+      )}
     >
     >
       <div
       <div
         className={`px-4 flex gap-4 ${
         className={`px-4 flex gap-4 ${
@@ -125,18 +137,29 @@ export const ChatMessage = memo(function ChatMessage({
         }`}
         }`}
       >
       >
         <div
         <div
-          className={`w-full space-y-2 ${isAssistant ? "text-left" : "text-right"}`}
+          className={`w-full space-y-2 ${
+            isAssistant ? "text-left" : "text-right"
+          }`}
         >
         >
-          <p className="w-full flex items-center gap-1 font-semibold text-sm text-[#333] dark:text-[#d8d8d8]">
+          <div className="w-full flex items-center gap-1 font-semibold text-sm text-[#333] dark:text-[#d8d8d8]">
             {isAssistant ? (
             {isAssistant ? (
-              <img
-                src={logoImg}
-                className="w-6 h-6"
-                alt={t("assistant.message.logo")}
-              />
+              <div className="w-6 h-6 flex justify-center items-center rounded-full bg-white">
+                {currentAssistant?._source?.icon?.startsWith("font_") ? (
+                  <FontIcon
+                    name={currentAssistant._source.icon}
+                    className="w-4 h-4"
+                  />
+                ) : (
+                  <img
+                    src={logoImg}
+                    className="w-4 h-4"
+                    alt={t("assistant.message.logo")}
+                  />
+                )}
+              </div>
             ) : null}
             ) : null}
-            {isAssistant ? t("assistant.message.aiName") : ""}
-          </p>
+            {isAssistant ? currentAssistant?._source?.name || "Coco AI" : ""}
+          </div>
           <div className="w-full prose dark:prose-invert prose-sm max-w-none">
           <div className="w-full prose dark:prose-invert prose-sm max-w-none">
             <div className="w-full pl-7 text-[#333] dark:text-[#d8d8d8] leading-relaxed">
             <div className="w-full pl-7 text-[#333] dark:text-[#d8d8d8] leading-relaxed">
               {renderContent()}
               {renderContent()}

+ 0 - 1
src/components/Cloud/Cloud.tsx

@@ -153,7 +153,6 @@ export default function Cloud() {
         getCurrentWindow().setFocus();
         getCurrentWindow().setFocus();
       } catch (e) {
       } catch (e) {
         console.error("Sign in failed:", e);
         console.error("Sign in failed:", e);
-        addError("SSO login failed: " + e);
       } finally {
       } finally {
         setLoading(false);
         setLoading(false);
       }
       }

+ 3 - 3
src/components/Cloud/Sidebar.tsx

@@ -27,7 +27,7 @@ export const Sidebar = forwardRef<{ refreshData: () => void }, SidebarProps>(
       return (
       return (
         <div
         <div
           key={item?.id}
           key={item?.id}
-          className={`flex cursor-pointer items-center space-x-2 px-3 py-2 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 rounded-lg mb-2 ${
+          className={`flex cursor-pointer items-center space-x-2 px-3 py-2 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 rounded-lg mb-2 whitespace-nowrap ${
             currentService?.id === item?.id
             currentService?.id === item?.id
               ? "dark:bg-blue-900/20 dark:bg-blue-900 border border-[#0087ff]"
               ? "dark:bg-blue-900/20 dark:bg-blue-900 border border-[#0087ff]"
               : "bg-gray-50 dark:bg-gray-900 border border-[#e6e6e6] dark:border-gray-700"
               : "bg-gray-50 dark:bg-gray-900 border border-[#e6e6e6] dark:border-gray-700"
@@ -37,9 +37,9 @@ export const Sidebar = forwardRef<{ refreshData: () => void }, SidebarProps>(
           <img
           <img
             src={item?.provider?.icon || cocoLogoImg}
             src={item?.provider?.icon || cocoLogoImg}
             alt="LogoImg"
             alt="LogoImg"
-            className="w-5 h-5"
+            className="w-5 h-5 flex-shrink-0"
           />
           />
-          <span className="font-medium">{item?.name}</span>
+          <span className="font-medium truncate max-w-[140px]">{item?.name}</span>
           <div className="flex-1" />
           <div className="flex-1" />
           <button className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300">
           <button className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300">
             {item.health?.status ? (
             {item.health?.status ? (

+ 188 - 179
src/components/Common/HistoryList/index.tsx

@@ -19,6 +19,7 @@ import { useTranslation } from "react-i18next";
 
 
 import VisibleKey from "../VisibleKey";
 import VisibleKey from "../VisibleKey";
 import { Chat } from "@/components/Assistant/types";
 import { Chat } from "@/components/Assistant/types";
+import NoDataImage from "../NoDataImage";
 
 
 dayjs.extend(isSameOrAfter);
 dayjs.extend(isSameOrAfter);
 
 
@@ -106,7 +107,7 @@ const HistoryList: FC<HistoryListProps> = (props) => {
   ];
   ];
 
 
   const debouncedSearch = useMemo(() => {
   const debouncedSearch = useMemo(() => {
-    return debounce((value: string) => onSearch(value), 500);
+    return debounce((value: string) => onSearch(value), 300);
   }, [onSearch]);
   }, [onSearch]);
 
 
   useKeyPress(["uparrow", "downarrow"], (_, key) => {
   useKeyPress(["uparrow", "downarrow"], (_, key) => {
@@ -158,7 +159,7 @@ const HistoryList: FC<HistoryListProps> = (props) => {
       ref={listRef}
       ref={listRef}
       id={id}
       id={id}
       className={clsx(
       className={clsx(
-        "h-full overflow-auto px-3 py-2 text-sm bg-[#F3F4F6] dark:bg-[#1F2937]"
+        "flex flex-col h-full overflow-auto px-3 py-2 text-sm bg-[#F3F4F6] dark:bg-[#1F2937] custom-scrollbar"
       )}
       )}
     >
     >
       <div className="flex gap-1 children:h-8">
       <div className="flex gap-1 children:h-8">
@@ -197,194 +198,202 @@ const HistoryList: FC<HistoryListProps> = (props) => {
         </div>
         </div>
       </div>
       </div>
 
 
-      <div className="mt-6">
-        {Object.entries(sortedList).map(([label, list]) => {
-          return (
-            <div key={label}>
-              <span className="text-xs text-[#999] px-3">{t(label)}</span>
-
-              <ul>
-                {list.map((item) => {
-                  const { _id, _source } = item;
-
-                  const isActive = _id === active?._id;
-                  const title = _source?.title ?? _id;
-
-                  return (
-                    <li
-                      key={_id}
-                      id={_id}
-                      className={clsx(
-                        "flex items-center mt-1 h-10 rounded-lg cursor-pointer hover:bg-[#EDEDED] dark:hover:bg-[#353F4D] transition",
-                        {
-                          "!bg-[#E5E7EB] dark:!bg-[#2B3444]": isActive,
-                        }
-                      )}
-                      onClick={() => {
-                        if (!isActive) {
-                          setIsEdit(false);
-                        }
-
-                        onSelect(item);
-                      }}
-                    >
-                      <div
-                        className={clsx("w-1 h-6 rounded-sm bg-[#0072FF]", {
-                          "opacity-0": _id !== active?._id,
-                        })}
-                      />
-
-                      <div className="flex-1 flex items-center justify-between gap-2 px-2 overflow-hidden">
-                        {isEdit && isActive ? (
-                          <Input
-                            autoFocus
-                            defaultValue={title}
-                            className="flex-1 -mx-px outline-none bg-transparent border border-[#0061FF] rounded-[4px]"
-                            onKeyDown={(event) => {
-                              if (event.key !== "Enter") return;
-
-                              onRename(item._id, event.currentTarget.value);
-
-                              setIsEdit(false);
-                            }}
-                            onBlur={(event) => {
-                              onRename(item._id, event.target.value);
-
+      {list.length > 0 ? (
+        <>
+          <div className="mt-6">
+            {Object.entries(sortedList).map(([label, list]) => {
+              return (
+                <div key={label}>
+                  <span className="text-xs text-[#999] px-3">{t(label)}</span>
+
+                  <ul>
+                    {list.map((item) => {
+                      const { _id, _source } = item;
+
+                      const isActive = _id === active?._id;
+                      const title = _source?.title ?? _id;
+
+                      return (
+                        <li
+                          key={_id}
+                          id={_id}
+                          className={clsx(
+                            "flex items-center mt-1 h-10 rounded-lg cursor-pointer hover:bg-[#EDEDED] dark:hover:bg-[#353F4D] transition",
+                            {
+                              "!bg-[#E5E7EB] dark:!bg-[#2B3444]": isActive,
+                            }
+                          )}
+                          onClick={() => {
+                            if (!isActive) {
                               setIsEdit(false);
                               setIsEdit(false);
-                            }}
+                            }
+
+                            onSelect(item);
+                          }}
+                        >
+                          <div
+                            className={clsx("w-1 h-6 rounded-sm bg-[#0072FF]", {
+                              "opacity-0": _id !== active?._id,
+                            })}
                           />
                           />
-                        ) : (
-                          <span className="truncate">{title}</span>
-                        )}
-
-                        <div className="flex items-center gap-2">
-                          {isActive && !isEdit && (
-                            <VisibleKey
-                              shortcut="↑↓"
-                              rootClassName="w-6"
-                              shortcutClassName="w-6"
-                            />
-                          )}
 
 
-                          <Popover>
-                            {isActive && !isEdit && (
-                              <PopoverButton
-                                ref={moreButtonRef}
-                                className="flex gap-2"
-                              >
-                                <VisibleKey
-                                  shortcut="O"
-                                  onKeyPress={() => {
-                                    moreButtonRef.current?.click();
-                                  }}
-                                >
-                                  <Ellipsis className="size-4 text-[#979797]" />
-                                </VisibleKey>
-                              </PopoverButton>
+                          <div className="flex-1 flex items-center justify-between gap-2 px-2 overflow-hidden">
+                            {isEdit && isActive ? (
+                              <Input
+                                autoFocus
+                                defaultValue={title}
+                                className="flex-1 -mx-px outline-none bg-transparent border border-[#0061FF] rounded-[4px]"
+                                onKeyDown={(event) => {
+                                  if (event.key !== "Enter") return;
+
+                                  onRename(item._id, event.currentTarget.value);
+
+                                  setIsEdit(false);
+                                }}
+                                onBlur={(event) => {
+                                  onRename(item._id, event.target.value);
+
+                                  setIsEdit(false);
+                                }}
+                              />
+                            ) : (
+                              <span className="truncate">{title}</span>
                             )}
                             )}
 
 
-                            <PopoverPanel
-                              anchor="bottom"
-                              className="flex flex-col rounded-lg shadow-md z-100 bg-white dark:bg-[#202126] p-1 border border-black/2 dark:border-white/10"
-                              onClick={(event) => {
-                                event.stopPropagation();
-                              }}
-                            >
-                              {menuItems.map((menuItem) => {
-                                const {
-                                  label,
-                                  icon: Icon,
-                                  shortcut,
-                                  iconColor,
-                                  onClick,
-                                } = menuItem;
-
-                                return (
-                                  <button
-                                    key={label}
-                                    className="flex items-center gap-2 px-3 py-2 text-sm rounded-md hover:bg-[#EDEDED] dark:hover:bg-[#2B2C31] transition"
-                                    onClick={onClick}
+                            <div className="flex items-center gap-2">
+                              {isActive && !isEdit && (
+                                <VisibleKey
+                                  shortcut="↑↓"
+                                  rootClassName="w-6"
+                                  shortcutClassName="w-6"
+                                />
+                              )}
+
+                              <Popover>
+                                {isActive && !isEdit && (
+                                  <PopoverButton
+                                    ref={moreButtonRef}
+                                    className="flex gap-2"
                                   >
                                   >
                                     <VisibleKey
                                     <VisibleKey
-                                      shortcut={shortcut}
-                                      onKeyPress={onClick}
+                                      shortcut="O"
+                                      onKeyPress={() => {
+                                        moreButtonRef.current?.click();
+                                      }}
                                     >
                                     >
-                                      <Icon
-                                        className="size-4"
-                                        style={{
-                                          color: iconColor,
-                                        }}
-                                      />
+                                      <Ellipsis className="size-4 text-[#979797]" />
                                     </VisibleKey>
                                     </VisibleKey>
-
-                                    <span>{t(label)}</span>
-                                  </button>
-                                );
-                              })}
-                            </PopoverPanel>
-                          </Popover>
-                        </div>
-                      </div>
-                    </li>
-                  );
-                })}
-              </ul>
-            </div>
-          );
-        })}
-      </div>
-
-      <Dialog
-        open={isOpen}
-        onClose={() => setIsOpen(false)}
-        className="relative z-1000"
-      >
-        <div
-          id="headlessui-popover-panel:delete-history"
-          className="fixed inset-0 flex items-center justify-center w-screen"
-        >
-          <DialogPanel className="flex flex-col justify-between w-[360px] h-[160px] p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-xl rounded-lg">
-            <div className="flex flex-col gap-3">
-              <DialogTitle className="text-base font-bold">
-                {t("history_list.delete_modal.title")}
-              </DialogTitle>
-              <Description className="text-sm">
-                {t("history_list.delete_modal.description", {
-                  replace: [active?._source?.title || active?._id],
-                })}
-              </Description>
-            </div>
-
-            <div className="flex gap-4 self-end">
-              <VisibleKey
-                shortcut="N"
-                shortcutClassName="left-[unset] right-0"
-                onKeyPress={() => setIsOpen(false)}
-              >
-                <button
-                  className="h-8 px-4 text-sm text-[#666666] bg-[#F8F9FA] dark:text-white dark:bg-[#202126] border border-[#E6E6E6] dark:border-white/10 rounded-lg"
-                  onClick={() => setIsOpen(false)}
-                >
-                  {t("history_list.delete_modal.button.cancel")}
-                </button>
-              </VisibleKey>
-
-              <VisibleKey
-                shortcut="Y"
-                shortcutClassName="left-[unset] right-0"
-                onKeyPress={handleRemove}
-              >
-                <button
-                  className="h-8 px-4 text-sm text-white bg-[#EF4444] rounded-lg"
-                  onClick={handleRemove}
-                >
-                  {t("history_list.delete_modal.button.delete")}
-                </button>
-              </VisibleKey>
+                                  </PopoverButton>
+                                )}
+
+                                <PopoverPanel
+                                  anchor="bottom"
+                                  className="flex flex-col rounded-lg shadow-md z-100 bg-white dark:bg-[#202126] p-1 border border-black/2 dark:border-white/10"
+                                  onClick={(event) => {
+                                    event.stopPropagation();
+                                  }}
+                                >
+                                  {menuItems.map((menuItem) => {
+                                    const {
+                                      label,
+                                      icon: Icon,
+                                      shortcut,
+                                      iconColor,
+                                      onClick,
+                                    } = menuItem;
+
+                                    return (
+                                      <button
+                                        key={label}
+                                        className="flex items-center gap-2 px-3 py-2 text-sm rounded-md hover:bg-[#EDEDED] dark:hover:bg-[#2B2C31] transition"
+                                        onClick={onClick}
+                                      >
+                                        <VisibleKey
+                                          shortcut={shortcut}
+                                          onKeyPress={onClick}
+                                        >
+                                          <Icon
+                                            className="size-4"
+                                            style={{
+                                              color: iconColor,
+                                            }}
+                                          />
+                                        </VisibleKey>
+
+                                        <span>{t(label)}</span>
+                                      </button>
+                                    );
+                                  })}
+                                </PopoverPanel>
+                              </Popover>
+                            </div>
+                          </div>
+                        </li>
+                      );
+                    })}
+                  </ul>
+                </div>
+              );
+            })}
+          </div>
+
+          <Dialog
+            open={isOpen}
+            onClose={() => setIsOpen(false)}
+            className="relative z-1000"
+          >
+            <div
+              id="headlessui-popover-panel:delete-history"
+              className="fixed inset-0 flex items-center justify-center w-screen"
+            >
+              <DialogPanel className="flex flex-col justify-between w-[360px] h-[160px] p-3 text-[#333] dark:text-white/90 border border-[#e6e6e6] bg-white dark:bg-[#202126] dark:border-white/10 shadow-xl rounded-lg">
+                <div className="flex flex-col gap-3">
+                  <DialogTitle className="text-base font-bold">
+                    {t("history_list.delete_modal.title")}
+                  </DialogTitle>
+                  <Description className="text-sm">
+                    {t("history_list.delete_modal.description", {
+                      replace: [active?._source?.title || active?._id],
+                    })}
+                  </Description>
+                </div>
+
+                <div className="flex gap-4 self-end">
+                  <VisibleKey
+                    shortcut="N"
+                    shortcutClassName="left-[unset] right-0"
+                    onKeyPress={() => setIsOpen(false)}
+                  >
+                    <button
+                      className="h-8 px-4 text-sm text-[#666666] bg-[#F8F9FA] dark:text-white dark:bg-[#202126] border border-[#E6E6E6] dark:border-white/10 rounded-lg"
+                      onClick={() => setIsOpen(false)}
+                    >
+                      {t("history_list.delete_modal.button.cancel")}
+                    </button>
+                  </VisibleKey>
+
+                  <VisibleKey
+                    shortcut="Y"
+                    shortcutClassName="left-[unset] right-0"
+                    onKeyPress={handleRemove}
+                  >
+                    <button
+                      className="h-8 px-4 text-sm text-white bg-[#EF4444] rounded-lg"
+                      onClick={handleRemove}
+                    >
+                      {t("history_list.delete_modal.button.delete")}
+                    </button>
+                  </VisibleKey>
+                </div>
+              </DialogPanel>
             </div>
             </div>
-          </DialogPanel>
+          </Dialog>
+        </>
+      ) : (
+        <div className="flex items-center justify-center flex-1">
+          <NoDataImage />
         </div>
         </div>
-      </Dialog>
+      )}
     </div>
     </div>
   );
   );
 };
 };

+ 10 - 0
src/components/Common/Icons/TypeIcon.tsx

@@ -20,6 +20,16 @@ function TypeIcon({
   const endpoint_http = useAppStore((state) => state.endpoint_http);
   const endpoint_http = useAppStore((state) => state.endpoint_http);
   const connectorSource = useFindConnectorIcon(item);
   const connectorSource = useFindConnectorIcon(item);
 
 
+  const isCalculator = item.id === "Calculator";
+
+  if (isCalculator) {
+    return (
+      <IconWrapper className={className} onClick={onClick}>
+        <img className={className} src="/assets/calculator.png" alt="icon" />
+      </IconWrapper>
+    );
+  }
+
   if (item?.source?.icon) {
   if (item?.source?.icon) {
     if (
     if (
       item?.source?.icon.startsWith("http://") ||
       item?.source?.icon.startsWith("http://") ||

+ 18 - 0
src/components/Common/NoDataImage.tsx

@@ -0,0 +1,18 @@
+import { useThemeStore } from "@/stores/themeStore";
+import clsx from "clsx";
+import { FC, HTMLAttributes } from "react";
+
+const NoDataImage: FC<HTMLAttributes<HTMLImageElement>> = (props) => {
+  const { className } = props;
+  const isDark = useThemeStore((state) => state.isDark);
+
+  return (
+    <img
+      {...props}
+      src={isDark ? "/assets/no_data_dark.png" : "/assets/no_data_light.png"}
+      className={clsx("size-16", className)}
+    />
+  );
+};
+
+export default NoDataImage;

+ 56 - 0
src/components/Search/Calculator.tsx

@@ -0,0 +1,56 @@
+import { ChevronsRight } from "lucide-react";
+import { FC } from "react";
+import clsx from "clsx";
+import { useTranslation } from "react-i18next";
+import { copyToClipboard } from "@/utils";
+
+interface CalculatorProps {
+  item: any;
+  isSelected: boolean;
+}
+
+const Calculator: FC<CalculatorProps> = (props) => {
+  const { item, isSelected } = props;
+  const {
+    payload: { query, result },
+  } = item;
+  const { t, i18n } = useTranslation();
+
+  const renderItem = (result: string, description: string) => {
+    return (
+      <div className="flex-1 flex flex-col gap-1 items-center justify-center h-[90px] overflow-hidden rounded-[4px] border border-transparent transition bg-[#F8F8F8] dark:bg-[#141414]">
+        <div className="w-[90%] text-xl text-[#333] dark:text-[#d8d8d8] truncate text-center">
+          {result}
+        </div>
+        <div className="w-[90%] text-xs text-[#999] dark:text-[#666] truncate text-center">
+          {description}
+        </div>
+      </div>
+    );
+  };
+
+  return (
+    <div
+      className={clsx(
+        "flex items-center gap-1 p-2 w-full rounded-lg transition",
+        {
+          "bg-[#EDEDED] dark:bg-[#202126]": isSelected,
+        }
+      )}
+      onDoubleClick={() => {
+        copyToClipboard(result.value);
+      }}
+    >
+      {renderItem(query.value, t(`calculator.${query.type}`))}
+
+      <ChevronsRight className="text-[#999999] size-5" />
+
+      {renderItem(
+        result.value,
+        i18n.language === "zh" ? result.toZh : result.toEn
+      )}
+    </div>
+  );
+};
+
+export default Calculator;

+ 120 - 47
src/components/Search/ContextMenu.tsx

@@ -1,19 +1,17 @@
-import {
-  useClickAway,
-  useCreation,
-  useEventListener,
-  useReactive,
-} from "ahooks";
+import { useClickAway, useCreation, useReactive } from "ahooks";
 import clsx from "clsx";
 import clsx from "clsx";
-import { isNil } from "lodash-es";
-import { Link, SquareArrowOutUpRight } from "lucide-react";
-import { cloneElement, useEffect, useRef } from "react";
+import { isNil, lowerCase, noop } from "lodash-es";
+import { Copy, Link, SquareArrowOutUpRight } from "lucide-react";
+import { cloneElement, useEffect, useRef, useState } from "react";
 import { useTranslation } from "react-i18next";
 import { useTranslation } from "react-i18next";
 
 
 import { useOSKeyPress } from "@/hooks/useOSKeyPress";
 import { useOSKeyPress } from "@/hooks/useOSKeyPress";
 import { useSearchStore } from "@/stores/searchStore";
 import { useSearchStore } from "@/stores/searchStore";
 import { copyToClipboard, OpenURLWithBrowser } from "@/utils";
 import { copyToClipboard, OpenURLWithBrowser } from "@/utils";
 import { isMac } from "@/utils/platform";
 import { isMac } from "@/utils/platform";
+import { CONTEXT_MENU_PANEL_ID } from "@/constants";
+import { useShortcutsStore } from "@/stores/shortcutsStore";
+import { Input } from "@headlessui/react";
 
 
 interface State {
 interface State {
   activeMenuIndex: number;
   activeMenuIndex: number;
@@ -25,55 +23,101 @@ interface ContextMenuProps {
 
 
 const ContextMenu = ({ hideCoco }: ContextMenuProps) => {
 const ContextMenu = ({ hideCoco }: ContextMenuProps) => {
   const containerRef = useRef<HTMLDivElement>(null);
   const containerRef = useRef<HTMLDivElement>(null);
-
-  const { t } = useTranslation();
-
+  const { t, i18n } = useTranslation();
+  const state = useReactive<State>({
+    activeMenuIndex: 0,
+  });
   const visibleContextMenu = useSearchStore((state) => {
   const visibleContextMenu = useSearchStore((state) => {
     return state.visibleContextMenu;
     return state.visibleContextMenu;
   });
   });
-
   const setVisibleContextMenu = useSearchStore((state) => {
   const setVisibleContextMenu = useSearchStore((state) => {
     return state.setVisibleContextMenu;
     return state.setVisibleContextMenu;
   });
   });
-
+  const setOpenPopover = useShortcutsStore((state) => state.setOpenPopover);
   const selectedSearchContent = useSearchStore((state) => {
   const selectedSearchContent = useSearchStore((state) => {
     return state.selectedSearchContent;
     return state.selectedSearchContent;
   });
   });
+  const [searchMenus, setSearchMenus] = useState<typeof menus>([]);
+
+  const title = useCreation(() => {
+    if (selectedSearchContent?.id === "Calculator") {
+      return t("search.contextMenu.title.calculator");
+    }
+
+    return selectedSearchContent?.title;
+  }, [selectedSearchContent]);
 
 
   const menus = useCreation(() => {
   const menus = useCreation(() => {
     if (isNil(selectedSearchContent)) return [];
     if (isNil(selectedSearchContent)) return [];
 
 
-    return [
+    const { url, category, payload } = selectedSearchContent;
+    const { query, result } = payload ?? {};
+
+    const menus = [
       {
       {
-        name: "search.contextMenu.open",
+        name: t("search.contextMenu.open"),
         icon: <SquareArrowOutUpRight />,
         icon: <SquareArrowOutUpRight />,
         keys: isMac ? ["↩︎"] : ["Enter"],
         keys: isMac ? ["↩︎"] : ["Enter"],
         shortcut: "enter",
         shortcut: "enter",
+        hide: category === "Calculator",
         clickEvent: () => {
         clickEvent: () => {
-          OpenURLWithBrowser(selectedSearchContent?.url);
-
-          setVisibleContextMenu(false);
+          OpenURLWithBrowser(url);
 
 
           hideCoco && hideCoco();
           hideCoco && hideCoco();
         },
         },
       },
       },
       {
       {
-        name: "search.contextMenu.copyLink",
+        name: t("search.contextMenu.copyLink"),
         icon: <Link />,
         icon: <Link />,
         keys: isMac ? ["⌘", "L"] : ["Ctrl", "L"],
         keys: isMac ? ["⌘", "L"] : ["Ctrl", "L"],
         shortcut: isMac ? "meta.l" : "ctrl.l",
         shortcut: isMac ? "meta.l" : "ctrl.l",
-        clickEvent: () => {
-          copyToClipboard(selectedSearchContent?.url);
-
-          setVisibleContextMenu(false);
+        hide: category === "Calculator",
+        clickEvent() {
+          copyToClipboard(url);
+        },
+      },
+      {
+        name: t("search.contextMenu.copyAnswer"),
+        icon: <Copy />,
+        keys: isMac ? ["↩︎"] : ["Enter"],
+        shortcut: "enter",
+        hide: category !== "Calculator",
+        clickEvent() {
+          copyToClipboard(result.value);
+        },
+      },
+      {
+        name: t("search.contextMenu.copyUppercaseAnswer"),
+        icon: <Copy />,
+        keys: isMac ? ["⌘", "↩︎"] : ["Ctrl", "Enter"],
+        shortcut: "meta.enter",
+        hide: category !== "Calculator",
+        clickEvent() {
+          copyToClipboard(i18n.language === "zh" ? result.toZh : result.toEn);
+        },
+      },
+      {
+        name: t("search.contextMenu.copyQuestionAndAnswer"),
+        icon: <Copy />,
+        keys: isMac ? ["⌘", "L"] : ["Ctrl", "L"],
+        shortcut: "meta.l",
+        hide: category !== "Calculator",
+        clickEvent() {
+          copyToClipboard(`${query.value} = ${result.value}`);
         },
         },
       },
       },
     ];
     ];
+
+    const filterMenus = menus.filter((item) => !item.hide);
+
+    setSearchMenus(filterMenus);
+
+    return filterMenus;
   }, [selectedSearchContent]);
   }, [selectedSearchContent]);
 
 
-  const state = useReactive<State>({
-    activeMenuIndex: 0,
-  });
+  const shortcuts = useCreation(() => {
+    return menus.map((item) => item.shortcut);
+  }, [menus]);
 
 
   useEffect(() => {
   useEffect(() => {
     state.activeMenuIndex = 0;
     state.activeMenuIndex = 0;
@@ -111,23 +155,30 @@ const ContextMenu = ({ hideCoco }: ContextMenuProps) => {
     }
     }
   });
   });
 
 
-  useOSKeyPress(
-    menus.map((item) => item.shortcut),
-    (_, key) => {
-      if (!visibleContextMenu) return;
+  useOSKeyPress(shortcuts, (_, key) => {
+    if (!visibleContextMenu) return;
 
 
-      const item = menus.find((item) => item.shortcut === key);
+    let matched;
 
 
-      item?.clickEvent();
+    if (key === "enter") {
+      matched = menus.find((_, index) => index === state.activeMenuIndex);
+    } else {
+      matched = menus.find((item) => item.shortcut === key);
     }
     }
-  );
-
-  useEventListener("keydown", (event) => {
-    if (!visibleContextMenu) return;
 
 
-    event.stopImmediatePropagation();
+    handleClick(matched?.clickEvent);
   });
   });
 
 
+  useEffect(() => {
+    setOpenPopover(visibleContextMenu);
+  }, [visibleContextMenu]);
+
+  const handleClick = (click = noop) => {
+    click?.();
+
+    setVisibleContextMenu(false);
+  };
+
   return (
   return (
     <>
     <>
       {visibleContextMenu && (
       {visibleContextMenu && (
@@ -138,41 +189,44 @@ const ContextMenu = ({ hideCoco }: ContextMenuProps) => {
 
 
             setVisibleContextMenu(false);
             setVisibleContextMenu(false);
           }}
           }}
-        ></div>
+        />
       )}
       )}
 
 
       <div
       <div
         ref={containerRef}
         ref={containerRef}
+        id={visibleContextMenu ? CONTEXT_MENU_PANEL_ID : ""}
         className={clsx(
         className={clsx(
-          "absolute bottom-[40px] right-[8px] min-w-[280px] scale-0 transition origin-bottom-right text-sm p-1 bg-white dark:bg-[#202126] rounded-lg shadow-xs border border-gray-200 dark:border-gray-700",
+          "absolute bottom-[50px] right-[18px] w-[300px] flex flex-col gap-2 scale-0 transition origin-bottom-right text-sm p-3 pb-0 bg-white dark:bg-black rounded-lg shadow-xs border border-[#EDEDED] dark:border-[#272828] shadow-lg dark:shadow-white/15",
           {
           {
             "!scale-100": visibleContextMenu,
             "!scale-100": visibleContextMenu,
           }
           }
         )}
         )}
       >
       >
-        <ul className="flex flex-col">
-          {menus.map((item, index) => {
+        <div className="text-[#999] dark:text-[#666] truncate">{title}</div>
+
+        <ul className="flex flex-col -mx-2">
+          {searchMenus.map((item, index) => {
             const { name, icon, keys, clickEvent } = item;
             const { name, icon, keys, clickEvent } = item;
 
 
             return (
             return (
               <li
               <li
                 key={name}
                 key={name}
                 className={clsx(
                 className={clsx(
-                  "flex justify-between items-center px-3 py-2 rounded-lg cursor-pointer",
+                  "flex justify-between items-center gap-2 px-2 py-2 rounded-lg cursor-pointer",
                   {
                   {
-                    "bg-black/5 dark:bg-white/5":
+                    "bg-[#EDEDED] dark:bg-[#202126]":
                       index === state.activeMenuIndex,
                       index === state.activeMenuIndex,
                   }
                   }
                 )}
                 )}
                 onMouseEnter={() => {
                 onMouseEnter={() => {
                   state.activeMenuIndex = index;
                   state.activeMenuIndex = index;
                 }}
                 }}
-                onClick={clickEvent}
+                onClick={() => handleClick(clickEvent)}
               >
               >
                 <div className="flex items-center gap-2 text-black/80 dark:text-white/80">
                 <div className="flex items-center gap-2 text-black/80 dark:text-white/80">
                   {cloneElement(icon, { className: "size-4" })}
                   {cloneElement(icon, { className: "size-4" })}
 
 
-                  <span>{t(name)}</span>
+                  <span>{name}</span>
                 </div>
                 </div>
 
 
                 <div className="flex gap-[4px] text-black/60 dark:text-white/60">
                 <div className="flex gap-[4px] text-black/60 dark:text-white/60">
@@ -180,7 +234,7 @@ const ContextMenu = ({ hideCoco }: ContextMenuProps) => {
                     <kbd
                     <kbd
                       key={key}
                       key={key}
                       className={clsx(
                       className={clsx(
-                        "flex justify-center items-center font-sans h-[20px] min-w-[20px] text-[10px] rounded-md border border-black/10 dark:border-white/10",
+                        "flex justify-center items-center font-sans h-[20px] min-w-[20px] text-[10px] rounded-md border border-[#EDEDED] dark:border-white/10 bg-white dark:bg-[#202126]",
                         {
                         {
                           "px-1": key.length > 1,
                           "px-1": key.length > 1,
                         }
                         }
@@ -194,6 +248,25 @@ const ContextMenu = ({ hideCoco }: ContextMenuProps) => {
             );
             );
           })}
           })}
         </ul>
         </ul>
+
+        <div className="-mx-3 p-2 border-t border-[#E6E6E6] dark:border-[#262626]">
+          {visibleContextMenu && (
+            <Input
+              autoFocus
+              placeholder={t("search.contextMenu.search")}
+              className="w-full bg-transparent"
+              onChange={(event) => {
+                const value = event.target.value;
+
+                const searchMenus = menus.filter((item) => {
+                  return lowerCase(item.name).includes(lowerCase(value));
+                });
+
+                setSearchMenus(searchMenus);
+              }}
+            />
+          )}
+        </div>
       </div>
       </div>
     </>
     </>
   );
   );

+ 65 - 3
src/components/Search/DocumentDetail.tsx

@@ -5,6 +5,7 @@ import { formatter } from "@/utils/index";
 import TypeIcon from "@/components/Common/Icons/TypeIcon";
 import TypeIcon from "@/components/Common/Icons/TypeIcon";
 import defaultThumbnail from "@/assets/coconut-tree.png";
 import defaultThumbnail from "@/assets/coconut-tree.png";
 import ItemIcon from "@/components/Common/Icons/ItemIcon";
 import ItemIcon from "@/components/Common/Icons/ItemIcon";
+import { RichCategories } from "./ListRight";
 
 
 interface DocumentDetailProps {
 interface DocumentDetailProps {
   document: any;
   document: any;
@@ -19,10 +20,13 @@ interface DetailItemProps {
 
 
 const DetailItem: React.FC<DetailItemProps> = ({ label, value, icon }) => (
 const DetailItem: React.FC<DetailItemProps> = ({ label, value, icon }) => (
   <div className="flex justify-between flex-wrap font-normal text-xs mb-2.5 border-t border-[rgba(238,240,243,1)] dark:border-[#272626] pt-2.5">
   <div className="flex justify-between flex-wrap font-normal text-xs mb-2.5 border-t border-[rgba(238,240,243,1)] dark:border-[#272626] pt-2.5">
-    <div className="text-[rgba(153,153,153,1)] dark:text-[#666]">{label}</div>
-    <div className="text-[rgba(51,51,51,1);] dark:text-[#D8D8D8] flex justify-end text-right w-56 break-words">
+    <div className="text-[rgba(153,153,153,1)] dark:text-[#666] min-w-[80px]">{label}</div>
+    <div
+      className="text-[rgba(51,51,51,1);] dark:text-[#D8D8D8] flex justify-end text-right flex-1 truncate group relative"
+      title={typeof value === "string" ? value : undefined}
+    >
       {icon}
       {icon}
-      {value}
+      <div className="truncate">{value}</div>
     </div>
     </div>
   </div>
   </div>
 );
 );
@@ -36,6 +40,52 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => {
     return `${url.slice(0, 20)}...${url.slice(-20)}`;
     return `${url.slice(0, 20)}...${url.slice(-20)}`;
   };
   };
 
 
+  //   categories: null
+
+  // category: null
+
+  // content: null
+
+  // cover: null
+
+  // created: null
+
+  // icon: "http://localhost:9000/assets/icons/connector/hugo_site/web.png"
+
+  // id: "31a8db836fe503d8f1d3ce2ea7c2fe6d"
+
+  // lang: null
+
+  // last_updated_by: null
+
+  // metadata: null
+
+  // owner: null
+
+  // payload: null
+
+  // rich_categories: null
+
+  // size: null
+
+  // source: {type: "connector", name: "INFINI Labs 官网", id: "cu4vj5o2sdb34a5pcbfg", icon: "http://localhost:9000/assets/icons/connector/hugo_site/icon.png"}
+
+  // subcategory: null
+
+  // summary: null
+
+  // tags: null
+
+  // thumbnail: null
+
+  // title: "dump_hash"
+
+  // type: "web_page"
+
+  // updated: null
+
+  // url: "https://infi
+
   return (
   return (
     <div className="p-3">
     <div className="p-3">
       {/* <div className="font-normal text-xs text-[#666] dark:text-[#999] mb-2">
       {/* <div className="font-normal text-xs text-[#666] dark:text-[#999] mb-2">
@@ -90,6 +140,18 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => {
           icon={<TypeIcon item={document} className="w-4 h-4 mr-1" />}
           icon={<TypeIcon item={document} className="w-4 h-4 mr-1" />}
         />
         />
 
 
+        {/* Rich Categories */}
+        {document?.rich_categories && (
+          <DetailItem
+            label={t("search.document.richCategories")}
+            value={
+              <div className="min-w-[160px] flex items-center justify-end w-full text-[12px] relative">
+                <RichCategories item={document} isSelected={false} />
+              </div>
+            }
+          />
+        )}
+
         {/* Document URL */}
         {/* Document URL */}
         {document?.url && (
         {document?.url && (
           <DetailItem
           <DetailItem

+ 82 - 42
src/components/Search/DocumentList.tsx

@@ -8,20 +8,19 @@ import noDataImg from "@/assets/coconut-tree.png";
 import { metaOrCtrlKey } from "@/utils/keyboardUtils";
 import { metaOrCtrlKey } from "@/utils/keyboardUtils";
 import SearchListItem from "./SearchListItem";
 import SearchListItem from "./SearchListItem";
 import { OpenURLWithBrowser } from "@/utils/index";
 import { OpenURLWithBrowser } from "@/utils/index";
+import platformAdapter from "@/utils/platformAdapter";
+import { Get } from "@/api/axiosRequest";
+import { useAppStore } from "@/stores/appStore";
+import { useConnectStore } from "@/stores/connectStore";
 
 
 interface DocumentListProps {
 interface DocumentListProps {
   onSelectDocument: (id: string) => void;
   onSelectDocument: (id: string) => void;
-  getDocDetail: (detail: any) => void;
+  getDocDetail: (detail: Record<string, any>) => void;
   input: string;
   input: string;
   isChatMode: boolean;
   isChatMode: boolean;
   selectedId?: string;
   selectedId?: string;
   viewMode: "detail" | "list";
   viewMode: "detail" | "list";
   setViewMode: (mode: "detail" | "list") => void;
   setViewMode: (mode: "detail" | "list") => void;
-  queryDocuments: (
-    from: number,
-    size: number,
-    queryStrings: any
-  ) => Promise<any>;
 }
 }
 
 
 const PAGE_SIZE = 20;
 const PAGE_SIZE = 20;
@@ -32,10 +31,11 @@ export const DocumentList: React.FC<DocumentListProps> = ({
   isChatMode,
   isChatMode,
   viewMode,
   viewMode,
   setViewMode,
   setViewMode,
-  queryDocuments,
 }) => {
 }) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const sourceData = useSearchStore((state) => state.sourceData);
   const sourceData = useSearchStore((state) => state.sourceData);
+  const isTauri = useAppStore((state) => state.isTauri);
+  const queryTimeout = useConnectStore((state) => state.queryTimeout);
 
 
   const [selectedItem, setSelectedItem] = useState<number | null>(null);
   const [selectedItem, setSelectedItem] = useState<number | null>(null);
   const [total, setTotal] = useState(0);
   const [total, setTotal] = useState(0);
@@ -49,6 +49,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
       let queryStrings: any = {
       let queryStrings: any = {
         query: input,
         query: input,
         datasource: sourceData?.source?.id,
         datasource: sourceData?.source?.id,
+        querysource: sourceData?.querySource?.id,
       };
       };
 
 
       if (sourceData?.rich_categories) {
       if (sourceData?.rich_categories) {
@@ -58,23 +59,50 @@ export const DocumentList: React.FC<DocumentListProps> = ({
         };
         };
       }
       }
 
 
-      try {
-        const response = await queryDocuments(from, PAGE_SIZE, queryStrings);
-        const list = response?.hits || [];
-        const total = response?.total_hits || 0;
-        setTotal(total);
+      let response: any;
+      if (isTauri) {
+        response = await platformAdapter.commands("query_coco_fusion", {
+          from: from,
+          size: PAGE_SIZE,
+          queryStrings: queryStrings,
+          queryTimeout: queryTimeout,
+        });
+      } else {
+        let url = `/query/_search?query=${queryStrings.query}&datasource=${queryStrings.datasource}&from=${from}&size=${PAGE_SIZE}`;
+        if (queryStrings?.rich_categories) {
+          url = `/query/_search?query=${queryStrings.query}&rich_category=${queryStrings.rich_category}&from=${from}&size=${PAGE_SIZE}`;
+        }
+        const [error, res]: any = await Get(url);
 
 
-        return {
-          list: list,
-          hasMore: list.length === PAGE_SIZE && from + list.length < total,
-        };
-      } catch (error) {
-        console.error("Failed to fetch documents:", error);
-        return {
-          list: d?.list || [],
-          hasMore: false,
-        };
+        if (error) {
+          console.error("_search", error);
+          response = { hits: [], total: 0 };
+        } else {
+          const hits =
+            res?.hits?.hits?.map((hit: any) => ({
+              document: {
+                ...hit._source,
+              },
+              score: hit._score || 0,
+              source: hit._source.source || null,
+            })) || [];
+          const total = res?.hits?.total?.value || 0;
+
+          response = {
+            hits: hits,
+            total_hits: total,
+          };
+        }
       }
       }
+      console.log("_docs", from, queryStrings, response);
+      const list = response?.hits || [];
+      const total = response?.total_hits || 0;
+      setTotal(total);
+
+      return {
+        list: list,
+        hasMore: list.length === PAGE_SIZE && from + list.length < total,
+      };
     },
     },
     {
     {
       target: containerRef,
       target: containerRef,
@@ -110,23 +138,18 @@ export const DocumentList: React.FC<DocumentListProps> = ({
     (e: KeyboardEvent) => {
     (e: KeyboardEvent) => {
       if (!data?.list?.length) return;
       if (!data?.list?.length) return;
 
 
-      if (e.key === "ArrowUp" || e.key === "ArrowDown") {
+      const handleArrowKeys = () => {
         e.preventDefault();
         e.preventDefault();
         setIsKeyboardMode(true);
         setIsKeyboardMode(true);
-
-        const newIndex =
-          e.key === "ArrowUp"
-            ? (prev: number | null) =>
-                prev === null || prev === 0 ? 0 : prev - 1
-            : (prev: number | null) =>
-                prev === null
-                  ? 0
-                  : prev === data.list.length - 1
-                  ? prev
-                  : prev + 1;
-
+  
         setSelectedItem((prev) => {
         setSelectedItem((prev) => {
-          const nextIndex = newIndex(prev);
+          const isArrowUp = e.key === "ArrowUp";
+          const nextIndex = prev === null 
+            ? 0 
+            : isArrowUp
+              ? Math.max(0, prev - 1)
+              : Math.min(data.list.length - 1, prev + 1);
+  
           getDocDetail(data.list[nextIndex]?.document);
           getDocDetail(data.list[nextIndex]?.document);
           itemRefs.current[nextIndex]?.scrollIntoView({
           itemRefs.current[nextIndex]?.scrollIntoView({
             behavior: "smooth",
             behavior: "smooth",
@@ -134,11 +157,25 @@ export const DocumentList: React.FC<DocumentListProps> = ({
           });
           });
           return nextIndex;
           return nextIndex;
         });
         });
-      } else if (e.key === metaOrCtrlKey()) {
-        e.preventDefault();
-      } else if (e.key === "Enter" && selectedItem !== null) {
-        const item = data?.list?.[selectedItem];
-        item?.document?.url && OpenURLWithBrowser(item.document.url);
+      };
+
+      const handleEnter = () => {
+        if (selectedItem === null) return;
+        const item = data.list[selectedItem]?.document;
+        item?.url && OpenURLWithBrowser(item.url);
+      };
+
+      switch (e.key) {
+        case "ArrowUp":
+        case "ArrowDown":
+          handleArrowKeys();
+          break;
+        case metaOrCtrlKey():
+          e.preventDefault();
+          break;
+        case "Enter":
+          handleEnter();
+          break;
       }
       }
     },
     },
     [data, selectedItem, getDocDetail]
     [data, selectedItem, getDocDetail]
@@ -174,7 +211,10 @@ export const DocumentList: React.FC<DocumentListProps> = ({
         />
         />
       </div>
       </div>
 
 
-      <div className="flex-1 overflow-auto custom-scrollbar pr-0.5" ref={containerRef}>
+      <div
+        className="flex-1 overflow-auto custom-scrollbar pr-0.5"
+        ref={containerRef}
+      >
         {data?.list && data.list.length > 0 && (
         {data?.list && data.list.length > 0 && (
           <div>
           <div>
             {data.list.map((hit, index) => (
             {data.list.map((hit, index) => (

+ 119 - 68
src/components/Search/DropdownList.tsx

@@ -1,7 +1,8 @@
-import { useEffect, useRef, useState, useCallback } from "react";
+import { useEffect, useRef, useState, useCallback, MouseEvent } from "react";
 import { CircleAlert, Bolt, X, ArrowBigRight } from "lucide-react";
 import { CircleAlert, Bolt, X, ArrowBigRight } from "lucide-react";
 import { isNil } from "lodash-es";
 import { isNil } from "lodash-es";
-import { useUnmount } from "ahooks";
+import { useDebounceFn, useUnmount } from "ahooks";
+import { useTranslation } from "react-i18next";
 
 
 import { useSearchStore } from "@/stores/searchStore";
 import { useSearchStore } from "@/stores/searchStore";
 import ThemedIcon from "@/components/Common/Icons/ThemedIcon";
 import ThemedIcon from "@/components/Common/Icons/ThemedIcon";
@@ -9,8 +10,10 @@ import IconWrapper from "@/components/Common/Icons/IconWrapper";
 import TypeIcon from "@/components/Common/Icons/TypeIcon";
 import TypeIcon from "@/components/Common/Icons/TypeIcon";
 import SearchListItem from "./SearchListItem";
 import SearchListItem from "./SearchListItem";
 import { metaOrCtrlKey, isMetaOrCtrlKey } from "@/utils/keyboardUtils";
 import { metaOrCtrlKey, isMetaOrCtrlKey } from "@/utils/keyboardUtils";
-import { OpenURLWithBrowser } from "@/utils/index";
+import { copyToClipboard, OpenURLWithBrowser } from "@/utils/index";
 import VisibleKey from "@/components/Common/VisibleKey";
 import VisibleKey from "@/components/Common/VisibleKey";
+import Calculator from "./Calculator";
+import { useShortcutsStore } from "@/stores/shortcutsStore";
 
 
 type ISearchData = Record<string, any[]>;
 type ISearchData = Record<string, any[]>;
 
 
@@ -28,12 +31,12 @@ function DropdownList({
   IsError,
   IsError,
   isChatMode,
   isChatMode,
 }: DropdownListProps) {
 }: DropdownListProps) {
+  const { t } = useTranslation();
+
   let globalIndex = 0;
   let globalIndex = 0;
   const globalItemIndexMap: any[] = [];
   const globalItemIndexMap: any[] = [];
 
 
-  const setSourceData = useSearchStore(
-    (state: { setSourceData: any }) => state.setSourceData
-  );
+  const setSourceData = useSearchStore((state) => state.setSourceData);
 
 
   const [showError, setShowError] = useState<boolean>(IsError);
   const [showError, setShowError] = useState<boolean>(IsError);
   const [selectedItem, setSelectedItem] = useState<number | null>(null);
   const [selectedItem, setSelectedItem] = useState<number | null>(null);
@@ -42,11 +45,18 @@ function DropdownList({
   const containerRef = useRef<HTMLDivElement>(null);
   const containerRef = useRef<HTMLDivElement>(null);
   const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
   const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
 
 
-  const setSelectedSearchContent = useSearchStore((state) => {
-    return state.setSelectedSearchContent;
-  });
+  const setSelectedSearchContent = useSearchStore(
+    (state) => state.setSelectedSearchContent
+  );
+
+  const hideArrowRight = (item: any) => {
+    const categories = ["Calculator"];
+
+    return categories.includes(item.category);
+  };
 
 
   useUnmount(() => {
   useUnmount(() => {
+    setSelectedItem(null);
     setSelectedSearchContent(void 0);
     setSelectedSearchContent(void 0);
   });
   });
 
 
@@ -66,15 +76,19 @@ function DropdownList({
     }
     }
   }, [isChatMode]);
   }, [isChatMode]);
 
 
+  const { run } = useDebounceFn(() => setSelectedItem(0), { wait: 200 });
+
+  useEffect(() => {
+    setSelectedItem(null);
+
+    run();
+  }, [SearchData]);
+
+  const openPopover = useShortcutsStore((state) => state.openPopover);
+
   const handleKeyDown = useCallback(
   const handleKeyDown = useCallback(
     (e: KeyboardEvent) => {
     (e: KeyboardEvent) => {
-      // console.log(
-      //   "handleKeyDown",
-      //   e.key,
-      //   showIndex,
-      //   e.key >= "0" && e.key <= "9" && showIndex
-      // );
-      if (!suggests.length) return;
+      if (!suggests.length || openPopover) return;
 
 
       if (e.key === "ArrowUp") {
       if (e.key === "ArrowUp") {
         e.preventDefault();
         e.preventDefault();
@@ -100,7 +114,11 @@ function DropdownList({
 
 
       if (e.key === "ArrowRight" && selectedItem !== null) {
       if (e.key === "ArrowRight" && selectedItem !== null) {
         e.preventDefault();
         e.preventDefault();
+
         const item = globalItemIndexMap[selectedItem];
         const item = globalItemIndexMap[selectedItem];
+
+        if (hideArrowRight(item)) return;
+
         goToTwoPage(item);
         goToTwoPage(item);
       }
       }
 
 
@@ -109,6 +127,8 @@ function DropdownList({
         const item = globalItemIndexMap[selectedItem];
         const item = globalItemIndexMap[selectedItem];
         if (item?.url) {
         if (item?.url) {
           OpenURLWithBrowser(item?.url);
           OpenURLWithBrowser(item?.url);
+        } else {
+          copyToClipboard(item?.payload?.result?.value);
         }
         }
       }
       }
 
 
@@ -124,7 +144,7 @@ function DropdownList({
         }
         }
       }
       }
     },
     },
-    [suggests, selectedItem, showIndex, globalItemIndexMap]
+    [suggests, selectedItem, showIndex, globalItemIndexMap, openPopover]
   );
   );
 
 
   const handleKeyUp = useCallback((e: KeyboardEvent) => {
   const handleKeyUp = useCallback((e: KeyboardEvent) => {
@@ -161,6 +181,16 @@ function DropdownList({
     setSourceData(item);
     setSourceData(item);
   }
   }
 
 
+  const setVisibleContextMenu = useSearchStore(
+    (state) => state.setVisibleContextMenu
+  );
+
+  const handleContextMenu = (event: MouseEvent) => {
+    event.preventDefault();
+
+    setVisibleContextMenu(true);
+  };
+
   return (
   return (
     <div
     <div
       ref={containerRef}
       ref={containerRef}
@@ -171,8 +201,7 @@ function DropdownList({
       {showError ? (
       {showError ? (
         <div className="flex items-center gap-2 text-sm text-[#333] p-2">
         <div className="flex items-center gap-2 text-sm text-[#333] p-2">
           <CircleAlert className="text-[#FF0000] w-[14px] h-[14px]" />
           <CircleAlert className="text-[#FF0000] w-[14px] h-[14px]" />
-          Coco server is unavailable, only local results and available services
-          are displayed.
+          {t("search.list.failures")}
           <Bolt className="text-[#000] w-[14px] h-[14px] cursor-pointer" />
           <Bolt className="text-[#000] w-[14px] h-[14px] cursor-pointer" />
           <X
           <X
             className="text-[#666] w-[16px] h-[16px] cursor-pointer"
             className="text-[#666] w-[16px] h-[16px] cursor-pointer"
@@ -180,56 +209,78 @@ function DropdownList({
           />
           />
         </div>
         </div>
       ) : null}
       ) : null}
-      {Object.entries(SearchData).map(([sourceName, items]) => (
-        <div key={sourceName}>
-          {Object.entries(SearchData).length < 5 ? (
-            <div className="p-2 text-xs text-[#999] dark:text-[#666] flex items-center gap-2.5 relative">
-              <TypeIcon item={items[0]?.document} className="w-4 h-4" />
-              {sourceName} - {items[0]?.source.name}
-              <div className="flex-1 border-b border-b-[#e6e6e6] dark:border-b-[#272626]"></div>
-              <IconWrapper
-                className="w-4 h-4 cursor-pointer"
-                onClick={(e: React.MouseEvent) => {
-                  e.stopPropagation();
-                  goToTwoPage(items[0]?.document);
-                }}
-              >
-                <ThemedIcon component={ArrowBigRight} className="w-4 h-4" />
-              </IconWrapper>
-              {showIndex && sourceName === selectedName ? (
-                <div className="absolute top-1 right-4">
-                  <VisibleKey shortcut="→" />
+      {Object.entries(SearchData).map(([sourceName, items]) => {
+        const showHeader = Object.entries(SearchData).length < 5;
+
+        return (
+          <div key={sourceName}>
+            {showHeader && (
+              <div className="p-2 text-xs text-[#999] dark:text-[#666] flex items-center gap-2.5 relative">
+                <TypeIcon item={items[0]?.document} className="w-4 h-4" />
+                {sourceName} - {items[0]?.source.name}
+                <div className="flex-1 border-b border-b-[#e6e6e6] dark:border-b-[#272626]"></div>
+                {!hideArrowRight({ category: sourceName }) && (
+                  <>
+                    <IconWrapper
+                      className="w-4 h-4 cursor-pointer"
+                      onClick={(e: React.MouseEvent) => {
+                        e.stopPropagation();
+                        goToTwoPage(items[0]?.document);
+                      }}
+                    >
+                      <ThemedIcon
+                        component={ArrowBigRight}
+                        className="w-4 h-4"
+                      />
+                    </IconWrapper>
+                    {showIndex && sourceName === selectedName && (
+                      <div className="absolute top-1 right-4">
+                        <VisibleKey shortcut="→" />
+                      </div>
+                    )}
+                  </>
+                )}
+              </div>
+            )}
+
+            {items.map((hit: any) => {
+              const isSelected = selectedItem === globalIndex;
+              const currentIndex = globalIndex;
+              const item = hit.document;
+              globalItemIndexMap.push(item);
+              globalIndex++;
+
+              return (
+                <div key={item.id} onContextMenu={handleContextMenu}>
+                  {hideArrowRight(item) ? (
+                    <div
+                      ref={(el) => (itemRefs.current[currentIndex] = el)}
+                      onMouseEnter={() => setSelectedItem(currentIndex)}
+                    >
+                      <Calculator item={item} isSelected={isSelected} />
+                    </div>
+                  ) : (
+                    <SearchListItem
+                      item={item}
+                      isSelected={isSelected}
+                      currentIndex={currentIndex}
+                      showIndex={showIndex}
+                      onMouseEnter={() => setSelectedItem(currentIndex)}
+                      onItemClick={() => {
+                        if (item?.url) {
+                          OpenURLWithBrowser(item?.url);
+                        }
+                      }}
+                      goToTwoPage={() => goToTwoPage(item)}
+                      itemRef={(el) => (itemRefs.current[currentIndex] = el)}
+                    />
+                  )}
                 </div>
                 </div>
-              ) : null}
-            </div>
-          ) : null}
-
-          {items.map((hit: any, index: number) => {
-            const isSelected = selectedItem === globalIndex;
-            const currentIndex = globalIndex;
-            const item = hit.document;
-            globalItemIndexMap.push(item);
-            globalIndex++;
-            return (
-              <SearchListItem
-                key={item.id + index}
-                item={item}
-                isSelected={isSelected}
-                currentIndex={currentIndex}
-                showIndex={showIndex}
-                onMouseEnter={() => setSelectedItem(currentIndex)}
-                onItemClick={() => {
-                  if (item?.url) {
-                    OpenURLWithBrowser(item?.url);
-                  }
-                }}
-                goToTwoPage={goToTwoPage}
-                itemRef={(el) => (itemRefs.current[currentIndex] = el)}
-              />
-            );
-          })}
-        </div>
-      ))}
+              );
+            })}
+          </div>
+        );
+      })}
     </div>
     </div>
   );
   );
 }
 }

+ 31 - 24
src/components/Search/InputBox.tsx

@@ -15,7 +15,7 @@ import SearchPopover from "./SearchPopover";
 // import AudioRecording from "../AudioRecording";
 // import AudioRecording from "../AudioRecording";
 import { DataSource } from "@/types/commands";
 import { DataSource } from "@/types/commands";
 // import InputExtra from "./InputExtra";
 // import InputExtra from "./InputExtra";
-// import { useConnectStore } from "@/stores/connectStore";
+import { useConnectStore } from "@/stores/connectStore";
 import { useShortcutsStore } from "@/stores/shortcutsStore";
 import { useShortcutsStore } from "@/stores/shortcutsStore";
 import Copyright from "@/components/Common/Copyright";
 import Copyright from "@/components/Common/Copyright";
 import VisibleKey from "@/components/Common/VisibleKey";
 import VisibleKey from "@/components/Common/VisibleKey";
@@ -55,7 +55,6 @@ interface ChatInputProps {
   getFileMetadata: (path: string) => Promise<any>;
   getFileMetadata: (path: string) => Promise<any>;
   getFileIcon: (path: string, size: number) => Promise<string>;
   getFileIcon: (path: string, size: number) => Promise<string>;
   hideCoco?: () => void;
   hideCoco?: () => void;
-  hasFeature?: string[];
   hasModules?: string[];
   hasModules?: string[];
   searchPlaceholder?: string;
   searchPlaceholder?: string;
   chatPlaceholder?: string;
   chatPlaceholder?: string;
@@ -77,7 +76,6 @@ export default function ChatInput({
   isChatPage = false,
   isChatPage = false,
   getDataSourcesByServer,
   getDataSourcesByServer,
   setupWindowFocusListener,
   setupWindowFocusListener,
-  hasFeature = ["think", "search", "think_icon", "search_icon"],
   hideCoco,
   hideCoco,
   hasModules = [],
   hasModules = [],
   searchPlaceholder,
   searchPlaceholder,
@@ -85,24 +83,17 @@ export default function ChatInput({
 }: ChatInputProps) {
 }: ChatInputProps) {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
-  const showTooltip = useAppStore(
-    (state: { showTooltip: boolean }) => state.showTooltip
-  );
+  const currentAssistant = useConnectStore((state) => state.currentAssistant);
+  console.log("currentAssistant", currentAssistant);
 
 
+  const showTooltip = useAppStore((state) => state.showTooltip);
   const isPinned = useAppStore((state) => state.isPinned);
   const isPinned = useAppStore((state) => state.isPinned);
 
 
-  const sourceData = useSearchStore(
-    (state: { sourceData: any }) => state.sourceData
-  );
-  const setSourceData = useSearchStore(
-    (state: { setSourceData: any }) => state.setSourceData
-  );
+  const sourceData = useSearchStore((state) => state.sourceData);
+  const setSourceData = useSearchStore((state) => state.setSourceData);
 
 
   // const sessionId = useConnectStore((state) => state.currentSessionId);
   // const sessionId = useConnectStore((state) => state.currentSessionId);
-  const modifierKey = useShortcutsStore((state) => {
-    return state.modifierKey;
-  });
-
+  const modifierKey = useShortcutsStore((state) => state.modifierKey);
   const modeSwitch = useShortcutsStore((state) => state.modeSwitch);
   const modeSwitch = useShortcutsStore((state) => state.modeSwitch);
   const returnToInput = useShortcutsStore((state) => state.returnToInput);
   const returnToInput = useShortcutsStore((state) => state.returnToInput);
   const deepThinking = useShortcutsStore((state) => state.deepThinking);
   const deepThinking = useShortcutsStore((state) => state.deepThinking);
@@ -140,6 +131,9 @@ export default function ChatInput({
   const setModifierKeyPressed = useShortcutsStore((state) => {
   const setModifierKeyPressed = useShortcutsStore((state) => {
     return state.setModifierKeyPressed;
     return state.setModifierKeyPressed;
   });
   });
+  const setVisibleStartPage = useConnectStore((state) => {
+    return state.setVisibleStartPage;
+  });
 
 
   useEffect(() => {
   useEffect(() => {
     const handleFocus = () => {
     const handleFocus = () => {
@@ -163,6 +157,8 @@ export default function ChatInput({
   }, [isChatMode, textareaRef, inputRef]);
   }, [isChatMode, textareaRef, inputRef]);
 
 
   const handleSubmit = useCallback(() => {
   const handleSubmit = useCallback(() => {
+    setVisibleStartPage(false);
+
     const trimmedValue = inputValue.trim();
     const trimmedValue = inputValue.trim();
     console.log("handleSubmit", trimmedValue, disabled);
     console.log("handleSubmit", trimmedValue, disabled);
     if (trimmedValue && !disabled) {
     if (trimmedValue && !disabled) {
@@ -183,13 +179,23 @@ export default function ChatInput({
 
 
   useKeyPress(`${modifierKey}.${returnToInput}`, handleToggleFocus);
   useKeyPress(`${modifierKey}.${returnToInput}`, handleToggleFocus);
 
 
+  const visibleContextMenu = useSearchStore((state) => {
+    return state.visibleContextMenu;
+  });
+  const setVisibleContextMenu = useSearchStore((state) => {
+    return state.setVisibleContextMenu;
+  });
+
   const handleKeyDown = useCallback(
   const handleKeyDown = useCallback(
     (e: KeyboardEvent) => {
     (e: KeyboardEvent) => {
       // console.log("handleKeyDown", e.code, e.key);
       // console.log("handleKeyDown", e.code, e.key);
 
 
       if (e.key === "Escape") {
       if (e.key === "Escape") {
-        handleEscapeKey();
-        return;
+        if (visibleContextMenu) {
+          return setVisibleContextMenu(false);
+        }
+
+        return handleEscapeKey();
       }
       }
 
 
       pressedKeys.add(e.key);
       pressedKeys.add(e.key);
@@ -239,6 +245,7 @@ export default function ChatInput({
       setIsCommandPressed,
       setIsCommandPressed,
       disabledChange,
       disabledChange,
       curChatEnd,
       curChatEnd,
+      visibleContextMenu,
     ]
     ]
   );
   );
 
 
@@ -399,7 +406,7 @@ export default function ChatInput({
         )}
         )}
 
 
         {!connected && isChatMode ? (
         {!connected && isChatMode ? (
-          <div className="absolute top-0 right-0 bottom-0 left-0 px-2 py-4 bg-[rgba(255,255,255,0.9)] dark:bg-[rgba(32,33,38,0.9)] backdrop-blur-[2px] rounded-md font-normal text-xs text-gray-400 flex items-center gap-4 z-10">
+          <div className="absolute top-0 right-0 bottom-0 left-0 px-2 py-4 bg-[rgba(238,238,238,0.98)] dark:bg-[rgba(32,33,38,0.9)] backdrop-blur-[2px] rounded-md font-normal text-xs text-gray-400 flex items-center gap-4 z-10">
             {t("search.input.connectionError")}
             {t("search.input.connectionError")}
             <div
             <div
               className="px-1 h-[24px] text-[#0061FF] font-normal text-xs flex items-center justify-center cursor-pointer underline"
               className="px-1 h-[24px] text-[#0061FF] font-normal text-xs flex items-center justify-center cursor-pointer underline"
@@ -436,7 +443,7 @@ export default function ChatInput({
               />
               />
             )} */}
             )} */}
 
 
-            {hasFeature.includes("think") && (
+            {currentAssistant?._source?.config?.visible && (
               <button
               <button
                 className={clsx(
                 className={clsx(
                   "flex items-center gap-1 py-[3px] pl-1 pr-1.5 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]",
                   "flex items-center gap-1 py-[3px] pl-1 pr-1.5 rounded-md transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]",
@@ -467,14 +474,16 @@ export default function ChatInput({
               </button>
               </button>
             )}
             )}
 
 
-            {hasFeature.includes("search") && (
+            {currentAssistant?._source?.datasource?.visible && (
               <SearchPopover
               <SearchPopover
                 isSearchActive={isSearchActive}
                 isSearchActive={isSearchActive}
                 setIsSearchActive={setIsSearchActive}
                 setIsSearchActive={setIsSearchActive}
                 getDataSourcesByServer={getDataSourcesByServer}
                 getDataSourcesByServer={getDataSourcesByServer}
               />
               />
             )}
             )}
-            {!hasFeature.includes("search") && !hasFeature.includes("think") ? (
+
+            {!currentAssistant?._source?.datasource?.visible &&
+            !currentAssistant?._source?.config?.visible ? (
               <div className="px-[9px]">
               <div className="px-[9px]">
                 <Copyright />
                 <Copyright />
               </div>
               </div>
@@ -498,9 +507,7 @@ export default function ChatInput({
             <ChatSwitch
             <ChatSwitch
               isChatMode={isChatMode}
               isChatMode={isChatMode}
               onChange={(value: boolean) => {
               onChange={(value: boolean) => {
-                value && disabledChange();
                 changeMode && changeMode(value);
                 changeMode && changeMode(value);
-                setSourceData(undefined);
               }}
               }}
             />
             />
           </div>
           </div>

+ 36 - 19
src/components/Search/ListRight.tsx

@@ -1,33 +1,36 @@
+import clsx from "clsx";
+
 import TypeIcon from "@/components/Common/Icons/TypeIcon";
 import TypeIcon from "@/components/Common/Icons/TypeIcon";
 import RichIcon from "@/components/Common/Icons/RichIcon";
 import RichIcon from "@/components/Common/Icons/RichIcon";
-import VisibleKey from "../Common/VisibleKey";
-import clsx from "clsx";
+import VisibleKey from "@/components/Common/VisibleKey";
 
 
 interface ListRightProps {
 interface ListRightProps {
   item: any;
   item: any;
   isSelected: boolean;
   isSelected: boolean;
   showIndex: boolean;
   showIndex: boolean;
   currentIndex: number;
   currentIndex: number;
-  goToTwoPage?: (item: any) => void;
+  goToTwoPage?: () => void;
 }
 }
 
 
-export default function ListRight({
+export interface RichCategoriesProps {
+  item: any;
+  isSelected: boolean;
+  goToTwoPage?: () => void;
+}
+
+export function RichCategories({
   item,
   item,
   isSelected,
   isSelected,
-  showIndex,
-  currentIndex,
   goToTwoPage,
   goToTwoPage,
-}: ListRightProps) {
+}: RichCategoriesProps) {
   return (
   return (
-    <div
-      className={`flex flex-1 text-right min-w-[160px] pl-5 justify-end w-full h-full text-[12px] gap-2 items-center relative`}
-    >
+    <>
       {item?.rich_categories ? null : (
       {item?.rich_categories ? null : (
         <div
         <div
           className={`w-4 h-4 cursor-pointer`}
           className={`w-4 h-4 cursor-pointer`}
           onClick={(e) => {
           onClick={(e) => {
             e.stopPropagation();
             e.stopPropagation();
-            goToTwoPage && goToTwoPage(item);
+            goToTwoPage && goToTwoPage();
           }}
           }}
         >
         >
           <TypeIcon
           <TypeIcon
@@ -35,7 +38,7 @@ export default function ListRight({
             className="w-4 h-4 cursor-pointer"
             className="w-4 h-4 cursor-pointer"
             onClick={(e: React.MouseEvent) => {
             onClick={(e: React.MouseEvent) => {
               e.stopPropagation();
               e.stopPropagation();
-              goToTwoPage && goToTwoPage(item);
+              goToTwoPage && goToTwoPage();
             }}
             }}
           />
           />
         </div>
         </div>
@@ -48,7 +51,7 @@ export default function ListRight({
             className={`w-4 h-4 mr-2 cursor-pointer`}
             className={`w-4 h-4 mr-2 cursor-pointer`}
             onClick={(e) => {
             onClick={(e) => {
               e.stopPropagation();
               e.stopPropagation();
-              goToTwoPage && goToTwoPage(item);
+              goToTwoPage && goToTwoPage();
             }}
             }}
           />
           />
           <span
           <span
@@ -98,6 +101,26 @@ export default function ListRight({
             ""}
             ""}
         </span>
         </span>
       )}
       )}
+    </>
+  );
+}
+
+export default function ListRight({
+  item,
+  isSelected,
+  showIndex,
+  currentIndex,
+  goToTwoPage,
+}: ListRightProps) {
+  return (
+    <div
+      className={`flex flex-1 text-right min-w-[160px] pl-5 justify-end w-full h-full text-[12px] gap-2 items-center relative`}
+    >
+      <RichCategories
+        item={item}
+        isSelected={false}
+        goToTwoPage={goToTwoPage}
+      />
 
 
       {isSelected && (
       {isSelected && (
         <VisibleKey
         <VisibleKey
@@ -105,9 +128,6 @@ export default function ListRight({
           rootClassName={clsx("!absolute", [
           rootClassName={clsx("!absolute", [
             showIndex && currentIndex < 10 ? "right-9" : "right-2",
             showIndex && currentIndex < 10 ? "right-9" : "right-2",
           ])}
           ])}
-          shortcutClassName={clsx({
-            "!shadow-[-6px_0px_6px_2px_#950599]": isSelected,
-          })}
         />
         />
       )}
       )}
 
 
@@ -115,9 +135,6 @@ export default function ListRight({
         <VisibleKey
         <VisibleKey
           shortcut={String(currentIndex === 9 ? 0 : currentIndex + 1)}
           shortcut={String(currentIndex === 9 ? 0 : currentIndex + 1)}
           rootClassName="!absolute right-2"
           rootClassName="!absolute right-2"
-          shortcutClassName={clsx({
-            "!shadow-[-6px_0px_6px_2px_#950599]": isSelected,
-          })}
         />
         />
       )}
       )}
     </div>
     </div>

+ 75 - 33
src/components/Search/Search.tsx

@@ -7,18 +7,30 @@ import { useSearchStore } from "@/stores/searchStore";
 import ContextMenu from "./ContextMenu";
 import ContextMenu from "./ContextMenu";
 import { NoResults } from "@/components/Common/UI/NoResults";
 import { NoResults } from "@/components/Common/UI/NoResults";
 import Footer from "@/components/Common/UI/Footer";
 import Footer from "@/components/Common/UI/Footer";
+import platformAdapter from "@/utils/platformAdapter";
+import { Get } from "@/api/axiosRequest";
+import { useConnectStore } from "@/stores/connectStore";
+
+interface SearchResponse {
+  hits: Array<{
+    _source: any;
+    _score?: number;
+    document: any;
+    score?: number;
+    source?: any;
+  }>;
+  total?: {
+    value: number;
+  };
+  total_hits?: number;
+  failed?: any[];
+}
 
 
 interface SearchProps {
 interface SearchProps {
   isTauri: boolean;
   isTauri: boolean;
   changeInput: (val: string) => void;
   changeInput: (val: string) => void;
   isChatMode: boolean;
   isChatMode: boolean;
   input: string;
   input: string;
-  querySearch: (input: string) => Promise<any>;
-  queryDocuments: (
-    from: number,
-    size: number,
-    queryStrings: any
-  ) => Promise<any>;
   hideCoco?: () => void;
   hideCoco?: () => void;
   openSetting: () => void;
   openSetting: () => void;
   setWindowAlwaysOnTop: (isPinned: boolean) => Promise<void>;
   setWindowAlwaysOnTop: (isPinned: boolean) => Promise<void>;
@@ -28,13 +40,12 @@ function Search({
   isTauri,
   isTauri,
   isChatMode,
   isChatMode,
   input,
   input,
-  querySearch,
-  queryDocuments,
   hideCoco,
   hideCoco,
   openSetting,
   openSetting,
   setWindowAlwaysOnTop,
   setWindowAlwaysOnTop,
 }: SearchProps) {
 }: SearchProps) {
   const sourceData = useSearchStore((state) => state.sourceData);
   const sourceData = useSearchStore((state) => state.sourceData);
+  const queryTimeout = useConnectStore((state) => state.queryTimeout);
 
 
   const [IsError, setIsError] = useState<boolean>(false);
   const [IsError, setIsError] = useState<boolean>(false);
   const [suggests, setSuggests] = useState<any[]>([]);
   const [suggests, setSuggests] = useState<any[]>([]);
@@ -43,12 +54,48 @@ function Search({
 
 
   const mainWindowRef = useRef<HTMLDivElement>(null);
   const mainWindowRef = useRef<HTMLDivElement>(null);
 
 
-  const getSuggest = async () => {
-    if (!input) return;
-    try {
-      const response = await querySearch(input);
-
-      console.log("_suggest", input, response);
+  const getSuggest = useCallback(
+    async (searchInput: string) => {
+      if (!searchInput) return;
+      if (sourceData) return;
+
+      let response: SearchResponse;
+      if (isTauri) {
+        response = await platformAdapter.commands("query_coco_fusion", {
+          from: 0,
+          size: 10,
+          queryStrings: { query: searchInput },
+          queryTimeout: queryTimeout,
+        });
+        if (response && typeof response === "object" && "failed" in response) {
+          const failedResult = response as any;
+          setIsError(!!failedResult.failed?.length);
+        }
+      } else {
+        const [error, res]: any = await Get(
+          `/query/_search?query=${searchInput}`
+        );
+
+        if (error) {
+          console.error("_search", error);
+          response = { hits: [], total_hits: 0 };
+        } else {
+          const hits =
+            res?.hits?.hits?.map((hit: any) => ({
+              document: {
+                ...hit._source,
+              },
+              score: hit._score || 0,
+              source: hit._source.source || null,
+            })) || [];
+          const total = res?.hits?.total?.value || 0;
+          response = {
+            hits: hits,
+            total_hits: total,
+          };
+        }
+      }
+      console.log("_suggest", sourceData, searchInput, response);
       let data = response?.hits || [];
       let data = response?.hits || [];
 
 
       setSuggests(data);
       setSuggests(data);
@@ -58,38 +105,33 @@ function Search({
         if (!acc[name]) {
         if (!acc[name]) {
           acc[name] = [];
           acc[name] = [];
         }
         }
+        item.document.querySource = item?.source;
         acc[name].push(item);
         acc[name].push(item);
         return acc;
         return acc;
       }, {});
       }, {});
-
       setSearchData(search_data);
       setSearchData(search_data);
-
-      setIsError(false);
       setIsSearchComplete(true);
       setIsSearchComplete(true);
-    } catch (error) {
+    },
+    [sourceData, isTauri]
+  );
+  const debouncedSearch = useCallback(
+    debounce((value: string) => getSuggest(value), 300),
+    [getSuggest]
+  );
+  useEffect(() => {
+    if (!isChatMode && input) {
+      debouncedSearch(input);
+    } else if (!input && !sourceData) {
       setSuggests([]);
       setSuggests([]);
-      setIsError(true);
-      console.error("query_coco_fusion:", error);
     }
     }
-  };
-
-  const debouncedSearch = useCallback(debounce(getSuggest, 500), [input]);
-
-  useEffect(() => {
-    !isChatMode && debouncedSearch();
-    if (!input) setSuggests([]);
-  }, [input]);
+  }, [input, isChatMode, debouncedSearch]);
 
 
   return (
   return (
     <div ref={mainWindowRef} className={`h-full pb-10 w-full relative`}>
     <div ref={mainWindowRef} className={`h-full pb-10 w-full relative`}>
       {/* Search Results Panel */}
       {/* Search Results Panel */}
       {suggests.length > 0 ? (
       {suggests.length > 0 ? (
         sourceData ? (
         sourceData ? (
-          <SearchResults
-            input={input}
-            isChatMode={isChatMode}
-            queryDocuments={queryDocuments}
-          />
+          <SearchResults input={input} isChatMode={isChatMode} />
         ) : (
         ) : (
           <DropdownList
           <DropdownList
             suggests={suggests}
             suggests={suggests}

+ 14 - 23
src/components/Search/SearchListItem.tsx

@@ -1,8 +1,8 @@
-import React, { MouseEvent } from "react";
+import React from "react";
+import clsx from "clsx";
 
 
 import ItemIcon from "@/components/Common/Icons/ItemIcon";
 import ItemIcon from "@/components/Common/Icons/ItemIcon";
 import ListRight from "./ListRight";
 import ListRight from "./ListRight";
-import { useSearchStore } from "@/stores/searchStore";
 import { useAppStore } from "@/stores/appStore";
 import { useAppStore } from "@/stores/appStore";
 import { useIsMobile } from "@/hooks/useIsMobile";
 import { useIsMobile } from "@/hooks/useIsMobile";
 
 
@@ -14,7 +14,7 @@ interface SearchListItemProps {
   onItemClick: () => void;
   onItemClick: () => void;
   itemRef: (el: HTMLDivElement | null) => void;
   itemRef: (el: HTMLDivElement | null) => void;
   showIndex?: boolean;
   showIndex?: boolean;
-  goToTwoPage?: (item: any) => void;
+  goToTwoPage?: () => void;
   showListRight?: boolean;
   showListRight?: boolean;
 }
 }
 
 
@@ -32,16 +32,6 @@ const SearchListItem: React.FC<SearchListItemProps> = React.memo(
   }) => {
   }) => {
     const isTauri = useAppStore((state) => state.isTauri);
     const isTauri = useAppStore((state) => state.isTauri);
 
 
-    const setVisibleContextMenu = useSearchStore(
-      (state) => state.setVisibleContextMenu
-    );
-
-    const onContextMenu = (event: MouseEvent) => {
-      event.preventDefault();
-
-      setVisibleContextMenu(true);
-    };
-
     const isMobile = useIsMobile();
     const isMobile = useIsMobile();
 
 
     return (
     return (
@@ -49,25 +39,26 @@ const SearchListItem: React.FC<SearchListItemProps> = React.memo(
         ref={itemRef}
         ref={itemRef}
         onMouseEnter={onMouseEnter}
         onMouseEnter={onMouseEnter}
         onClick={onItemClick}
         onClick={onItemClick}
-        className={`w-full px-2 py-2.5 text-sm flex mb-0 flex-row items-center mobile:mb-2 mobile:flex-col mobile:items-start justify-between rounded-lg transition-colors cursor-pointer ${
-          isSelected
-            ? "text-white bg-[var(--coco-primary-color)] hover:bg-[var(--coco-primary-color)]"
-            : "text-[#333] dark:text-[#d8d8d8] mobile:bg-gray-200/80 mobile:dark:bg-gray-700/50"
-        } ${showListRight ? "gap-7 mobile:gap-1" : ""}`}
-        onContextMenu={onContextMenu}
+        className={clsx(
+          "w-full px-2 py-2.5 text-sm flex mb-0 flex-row items-center mobile:mb-2 mobile:flex-col mobile:items-start justify-between rounded-lg transition-colors cursor-pointer text-[#333] dark:text-[#d8d8d8] mobile:bg-gray-200/80 mobile:dark:bg-gray-700/50",
+          {
+            "bg-black/10 dark:bg-white/15": isSelected,
+            "gap-7 mobile:gap-1": showListRight,
+          }
+        )}
       >
       >
         <div
         <div
           className={`${
           className={`${
-            showListRight
-              ? "max-w-[450px] mobile:w-full"
-              : "flex-1"
+            showListRight ? "max-w-[450px] mobile:max-w-full mobile:w-full" : "flex-1"
           } min-w-0 flex gap-2 items-center justify-start `}
           } min-w-0 flex gap-2 items-center justify-start `}
         >
         >
           <ItemIcon item={item} />
           <ItemIcon item={item} />
           <span className={`text-sm truncate text-left`}>{item?.title}</span>
           <span className={`text-sm truncate text-left`}>{item?.title}</span>
         </div>
         </div>
         {!isTauri && isMobile ? (
         {!isTauri && isMobile ? (
-          <div className="text-sm truncate">{item?.summary}</div>
+          <div className="w-full text-xs text-gray-500 dark:text-gray-400 truncate">
+            {item?.summary}
+          </div>
         ) : null}
         ) : null}
         {showListRight && (isTauri || !isMobile) ? (
         {showListRight && (isTauri || !isMobile) ? (
           <ListRight
           <ListRight

+ 145 - 174
src/components/Search/SearchPopover.tsx

@@ -10,6 +10,7 @@ import {
 } from "lucide-react";
 } from "lucide-react";
 import clsx from "clsx";
 import clsx from "clsx";
 import { useTranslation } from "react-i18next";
 import { useTranslation } from "react-i18next";
+import { useDebounce } from "ahooks";
 
 
 import TypeIcon from "@/components/Common/Icons/TypeIcon";
 import TypeIcon from "@/components/Common/Icons/TypeIcon";
 import { useConnectStore } from "@/stores/connectStore";
 import { useConnectStore } from "@/stores/connectStore";
@@ -17,8 +18,9 @@ import { useSearchStore } from "@/stores/searchStore";
 import { DataSource } from "@/types/commands";
 import { DataSource } from "@/types/commands";
 import Checkbox from "@/components/Common/Checkbox";
 import Checkbox from "@/components/Common/Checkbox";
 import { useShortcutsStore } from "@/stores/shortcutsStore";
 import { useShortcutsStore } from "@/stores/shortcutsStore";
-import VisibleKey from "../Common/VisibleKey";
-import { useDebounce } from "ahooks";
+import VisibleKey from "@/components/Common/VisibleKey";
+import { useChatStore } from "@/stores/chatStore";
+import NoDataImage from "@/components/Common/NoDataImage";
 
 
 interface SearchPopoverProps {
 interface SearchPopoverProps {
   isSearchActive: boolean;
   isSearchActive: boolean;
@@ -39,6 +41,8 @@ export default function SearchPopover({
   getDataSourcesByServer,
   getDataSourcesByServer,
 }: SearchPopoverProps) {
 }: SearchPopoverProps) {
   const { t } = useTranslation();
   const { t } = useTranslation();
+  const { connected } = useChatStore();
+
   const [isRefreshDataSource, setIsRefreshDataSource] = useState(false);
   const [isRefreshDataSource, setIsRefreshDataSource] = useState(false);
   const [dataSourceList, setDataSourceList] = useState<DataSource[]>([]);
   const [dataSourceList, setDataSourceList] = useState<DataSource[]>([]);
 
 
@@ -47,7 +51,6 @@ export default function SearchPopover({
 
 
   const currentService = useConnectStore((state) => state.currentService);
   const currentService = useConnectStore((state) => state.currentService);
 
 
-  const [showDataSource, setShowDataSource] = useState(false);
   const [keyword, setKeyword] = useState("");
   const [keyword, setKeyword] = useState("");
   const debouncedKeyword = useDebounce(keyword, { wait: 500 });
   const debouncedKeyword = useDebounce(keyword, { wait: 500 });
 
 
@@ -85,8 +88,7 @@ export default function SearchPopover({
     }
     }
   }, [currentService?.id, debouncedKeyword]);
   }, [currentService?.id, debouncedKeyword]);
 
 
-  const popoverRef = useRef<HTMLDivElement>(null);
-  const buttonRef = useRef<HTMLButtonElement>(null);
+  const popoverButtonRef = useRef<HTMLButtonElement>(null);
   const internetSearch = useShortcutsStore((state) => state.internetSearch);
   const internetSearch = useShortcutsStore((state) => state.internetSearch);
   const internetSearchScope = useShortcutsStore((state) => {
   const internetSearchScope = useShortcutsStore((state) => {
     return state.internetSearchScope;
     return state.internetSearchScope;
@@ -96,26 +98,6 @@ export default function SearchPopover({
   const [visibleList, setVisibleList] = useState<DataSource[]>([]);
   const [visibleList, setVisibleList] = useState<DataSource[]>([]);
   const searchInputRef = useRef<HTMLInputElement>(null);
   const searchInputRef = useRef<HTMLInputElement>(null);
 
 
-  useEffect(() => {
-    if (!showDataSource) return;
-
-    const handleClickOutside = (event: MouseEvent) => {
-      if (
-        popoverRef.current &&
-        !popoverRef.current.contains(event.target as Node) &&
-        buttonRef.current &&
-        !buttonRef.current.contains(event.target as Node)
-      ) {
-        setShowDataSource(false);
-      }
-    };
-
-    document.addEventListener("mousedown", handleClickOutside);
-    return () => {
-      document.removeEventListener("mousedown", handleClickOutside);
-    };
-  }, [showDataSource]);
-
   useEffect(() => {
   useEffect(() => {
     if (dataSourceList.length > 0) {
     if (dataSourceList.length > 0) {
       setSourceDataIds(dataSourceList.slice(1).map((item) => item.id));
       setSourceDataIds(dataSourceList.slice(1).map((item) => item.id));
@@ -123,15 +105,17 @@ export default function SearchPopover({
   }, [dataSourceList]);
   }, [dataSourceList]);
 
 
   useEffect(() => {
   useEffect(() => {
-    getDataSourceList();
-  }, [currentService?.id, debouncedKeyword]);
+    connected && getDataSourceList();
+  }, [connected, currentService?.id, debouncedKeyword]);
 
 
   useEffect(() => {
   useEffect(() => {
-    setTotalPage(Math.ceil(dataSourceList.length / 10));
+    setTotalPage(Math.max(Math.ceil(dataSourceList.length / 10), 1));
   }, [dataSourceList]);
   }, [dataSourceList]);
 
 
   useEffect(() => {
   useEffect(() => {
-    if (dataSourceList.length === 0) return;
+    if (dataSourceList.length === 0) {
+      return setVisibleList([]);
+    }
 
 
     const startIndex = (page - 1) * 9;
     const startIndex = (page - 1) * 9;
     const endIndex = startIndex + 9;
     const endIndex = startIndex + 9;
@@ -215,165 +199,152 @@ export default function SearchPopover({
             {t("search.input.search")}
             {t("search.input.search")}
           </span>
           </span>
 
 
-          {visibleList?.length > 0 && (
-            <Popover className="relative">
-              <PopoverButton
-                as="span"
-                ref={buttonRef}
-                className={clsx("flex items-center")}
+          <Popover className="relative">
+            <PopoverButton ref={popoverButtonRef} className="flex items-center">
+              <VisibleKey
+                shortcut={internetSearchScope}
+                onKeyPress={() => {
+                  popoverButtonRef.current?.click();
+                }}
+              >
+                <ChevronDownIcon
+                  className={clsx("size-3", [
+                    isSearchActive
+                      ? "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) => {
                 onClick={(e) => {
                   e.stopPropagation();
                   e.stopPropagation();
-                  setShowDataSource((prev) => !prev);
                 }}
                 }}
               >
               >
-                <VisibleKey
-                  shortcut={internetSearchScope}
-                  onKeyPress={() => {
-                    buttonRef.current?.click();
-                  }}
-                >
-                  <ChevronDownIcon
-                    className={clsx("size-3", [
-                      isSearchActive
-                        ? "text-[#0072FF] dark:text-[#0072FF]"
-                        : "text-[#333] dark:text-white",
-                    ])}
-                  />
-                </VisibleKey>
-              </PopoverButton>
-
-              {showDataSource ? (
-                <PopoverPanel
-                  static
-                  ref={popoverRef}
-                  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 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" : ""
+                          }`}
                         />
                         />
-                      </div>
-
-                      <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 sourceDataIds.includes(item.id);
-                              });
-                            } else {
-                              return sourceDataIds.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>
+                      </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>
 
 
-                              <div className="flex items-center gap-1">
-                                <VisibleKey
-                                  shortcut={
-                                    index === 9 ? "0" : String(index + 1)
+                  {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 sourceDataIds.includes(item.id);
+                            });
+                          } else {
+                            return sourceDataIds.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)
                                   }
                                   }
-                                  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>
                               </div>
-                            </li>
-                          );
-                        })}
-                      </ul>
+                            </div>
+                          </li>
+                        );
+                      })}
+                    </ul>
+                  ) : (
+                    <div className="flex items-center justify-center py-4">
+                      <NoDataImage />
                     </div>
                     </div>
+                  )}
+                </div>
 
 
-                    <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>
+                {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>
 
 
-                      <VisibleKey shortcut="rightarrow" onKeyPress={handleNext}>
-                        <ChevronRight className="size-4" onClick={handleNext} />
-                      </VisibleKey>
+                    <div className="text-xs">
+                      {page}/{totalPage}
                     </div>
                     </div>
+
+                    <VisibleKey shortcut="rightarrow" onKeyPress={handleNext}>
+                      <ChevronRight className="size-4" onClick={handleNext} />
+                    </VisibleKey>
                   </div>
                   </div>
-                </PopoverPanel>
-              ) : null}
-            </Popover>
-          )}
+                )}
+              </div>
+            </PopoverPanel>
+          </Popover>
         </>
         </>
       )}
       )}
     </div>
     </div>

+ 4 - 6
src/components/Search/SearchResults.tsx

@@ -7,17 +7,16 @@ import { useAppStore } from "@/stores/appStore";
 interface SearchResultsProps {
 interface SearchResultsProps {
   input: string;
   input: string;
   isChatMode: boolean;
   isChatMode: boolean;
-  queryDocuments: (from: number, size: number, queryStrings: any) => Promise<any>;
 }
 }
 
 
-export function SearchResults({ input, isChatMode, queryDocuments }: SearchResultsProps) {
+export function SearchResults({ input, isChatMode }: SearchResultsProps) {
   const isTauri = useAppStore((state) => state.isTauri);
   const isTauri = useAppStore((state) => state.isTauri);
 
 
   const [selectedDocumentId, setSelectedDocumentId] = useState("1");
   const [selectedDocumentId, setSelectedDocumentId] = useState("1");
 
 
   const [detailData, setDetailData] = useState<any>({});
   const [detailData, setDetailData] = useState<any>({});
   const [viewMode, setViewMode] = useState<"detail" | "list">(() => {
   const [viewMode, setViewMode] = useState<"detail" | "list">(() => {
-    return isTauri ? "detail" : (window.innerWidth < 768 ? "list" : "detail");
+    return isTauri ? "detail" : window.innerWidth < 768 ? "list" : "detail";
   });
   });
 
 
   useEffect(() => {
   useEffect(() => {
@@ -26,8 +25,8 @@ export function SearchResults({ input, isChatMode, queryDocuments }: SearchResul
         setViewMode(window.innerWidth < 768 ? "list" : "detail");
         setViewMode(window.innerWidth < 768 ? "list" : "detail");
       };
       };
 
 
-      window.addEventListener('resize', handleResize);
-      return () => window.removeEventListener('resize', handleResize);
+      window.addEventListener("resize", handleResize);
+      return () => window.removeEventListener("resize", handleResize);
     }
     }
   }, [isTauri]);
   }, [isTauri]);
 
 
@@ -47,7 +46,6 @@ export function SearchResults({ input, isChatMode, queryDocuments }: SearchResul
           isChatMode={isChatMode}
           isChatMode={isChatMode}
           viewMode={viewMode}
           viewMode={viewMode}
           setViewMode={setViewMode}
           setViewMode={setViewMode}
-          queryDocuments={queryDocuments}
         />
         />
 
 
         {/* Right Panel */}
         {/* Right Panel */}

+ 28 - 33
src/components/SearchChat/index.tsx

@@ -24,13 +24,13 @@ import { useStartupStore } from "@/stores/startupStore";
 import { DataSource } from "@/types/commands";
 import { DataSource } from "@/types/commands";
 import { useThemeStore } from "@/stores/themeStore";
 import { useThemeStore } from "@/stores/themeStore";
 import { Get } from "@/api/axiosRequest";
 import { Get } from "@/api/axiosRequest";
+import { useConnectStore } from "@/stores/connectStore";
 
 
 interface SearchChatProps {
 interface SearchChatProps {
   isTauri?: boolean;
   isTauri?: boolean;
   hasModules?: string[];
   hasModules?: string[];
   defaultModule?: "search" | "chat";
   defaultModule?: "search" | "chat";
 
 
-  hasFeature?: string[];
   showChatHistory?: boolean;
   showChatHistory?: boolean;
 
 
   theme?: "auto" | "light" | "dark";
   theme?: "auto" | "light" | "dark";
@@ -39,36 +39,31 @@ interface SearchChatProps {
 
 
   hideCoco?: () => void;
   hideCoco?: () => void;
   setIsPinned?: (value: boolean) => void;
   setIsPinned?: (value: boolean) => void;
-  querySearch: (input: string) => Promise<any>;
-  queryDocuments: (
-    from: number,
-    size: number,
-    queryStrings: any
-  ) => Promise<any>;
   onModeChange?: (isChatMode: boolean) => void;
   onModeChange?: (isChatMode: boolean) => void;
   isMobile?: boolean;
   isMobile?: boolean;
+  assistantIDs?: string[];
 }
 }
 
 
 function SearchChat({
 function SearchChat({
   isTauri = true,
   isTauri = true,
   hasModules = ["search", "chat"],
   hasModules = ["search", "chat"],
   defaultModule = "search",
   defaultModule = "search",
-  hasFeature = ["think", "search", "think_active", "search_active"],
   theme,
   theme,
   hideCoco,
   hideCoco,
-  querySearch,
-  queryDocuments,
   searchPlaceholder,
   searchPlaceholder,
   chatPlaceholder,
   chatPlaceholder,
   showChatHistory = true,
   showChatHistory = true,
   setIsPinned,
   setIsPinned,
   onModeChange,
   onModeChange,
   isMobile = false,
   isMobile = false,
+  assistantIDs,
 }: SearchChatProps) {
 }: SearchChatProps) {
+  const currentAssistant = useConnectStore((state) => state.currentAssistant);
+
   const customInitialState = {
   const customInitialState = {
     ...initialAppState,
     ...initialAppState,
-    isDeepThinkActive: hasFeature.includes("think_active"),
-    isSearchActive: hasFeature.includes("search_active"),
+    isDeepThinkActive: currentAssistant?._source?.type === "deep_think",
+    isSearchActive: currentAssistant?._source?.datasource?.enabled === true,
   };
   };
 
 
   const [state, dispatch] = useReducer(appReducer, customInitialState);
   const [state, dispatch] = useReducer(appReducer, customInitialState);
@@ -180,26 +175,31 @@ function SearchChat({
         query?: string;
         query?: string;
       }
       }
     ): Promise<DataSource[]> => {
     ): Promise<DataSource[]> => {
+      let response: any;
       if (isTauri) {
       if (isTauri) {
-        return platformAdapter.invokeBackend("get_datasources_by_server", {
+        response = platformAdapter.invokeBackend("get_datasources_by_server", {
           id: serverId,
           id: serverId,
           options,
           options,
         });
         });
       } else {
       } else {
-        const [error, response]: any = await Get("/datasource/_search");
+        const [error, res]: any = await Get("/datasource/_search");
         if (error) {
         if (error) {
           console.error("_search", error);
           console.error("_search", error);
           return [];
           return [];
         }
         }
-        const res = response?.hits?.hits?.map((item: any) => {
+        response = res?.hits?.hits?.map((item: any) => {
           return {
           return {
             ...item,
             ...item,
             id: item._source.id,
             id: item._source.id,
             name: item._source.name,
             name: item._source.name,
           };
           };
         });
         });
-        return res || [];
       }
       }
+      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 || [];
     },
     },
     []
     []
   );
   );
@@ -308,7 +308,6 @@ function SearchChat({
           setIsSearchActive={toggleSearchActive}
           setIsSearchActive={toggleSearchActive}
           isDeepThinkActive={isDeepThinkActive}
           isDeepThinkActive={isDeepThinkActive}
           setIsDeepThinkActive={toggleDeepThinkActive}
           setIsDeepThinkActive={toggleDeepThinkActive}
-          hasFeature={hasFeature}
           getDataSourcesByServer={getDataSourcesByServer}
           getDataSourcesByServer={getDataSourcesByServer}
           setupWindowFocusListener={setupWindowFocusListener}
           setupWindowFocusListener={setupWindowFocusListener}
           checkScreenPermission={checkScreenPermission}
           checkScreenPermission={checkScreenPermission}
@@ -340,8 +339,6 @@ function SearchChat({
             input={input}
             input={input}
             isChatMode={isChatMode}
             isChatMode={isChatMode}
             changeInput={setInput}
             changeInput={setInput}
-            querySearch={querySearch}
-            queryDocuments={queryDocuments}
             hideCoco={hideCoco}
             hideCoco={hideCoco}
             openSetting={openSetting}
             openSetting={openSetting}
             setWindowAlwaysOnTop={setWindowAlwaysOnTop}
             setWindowAlwaysOnTop={setWindowAlwaysOnTop}
@@ -357,20 +354,18 @@ function SearchChat({
             : "-top-[506px] opacity-0 pointer-events-none"
             : "-top-[506px] opacity-0 pointer-events-none"
         } h-[calc(100%-90px)]`}
         } h-[calc(100%-90px)]`}
       >
       >
-        {isTransitioned && isChatMode ? (
-          <Suspense fallback={<LoadingFallback />}>
-            <ChatAI
-              ref={chatAIRef}
-              key="ChatAI"
-              isTransitioned={isTransitioned}
-              changeInput={setInput}
-              isSearchActive={isSearchActive}
-              isDeepThinkActive={isDeepThinkActive}
-              getFileUrl={getFileUrl}
-              showChatHistory={showChatHistory}
-            />
-          </Suspense>
-        ) : null}
+        <Suspense fallback={<LoadingFallback />}>
+          <ChatAI
+            ref={chatAIRef}
+            key="ChatAI"
+            changeInput={setInput}
+            isSearchActive={isSearchActive}
+            isDeepThinkActive={isDeepThinkActive}
+            getFileUrl={getFileUrl}
+            showChatHistory={showChatHistory}
+            assistantIDs={assistantIDs}
+          />
+        </Suspense>
       </div>
       </div>
 
 
       <UpdateApp checkUpdate={checkUpdate} relaunchApp={relaunchApp} />
       <UpdateApp checkUpdate={checkUpdate} relaunchApp={relaunchApp} />

+ 12 - 0
src/components/Settings/Advanced/components/Shortcuts/index.tsx

@@ -43,6 +43,12 @@ const Shortcuts = () => {
   const setHistoricalRecords = useShortcutsStore((state) => {
   const setHistoricalRecords = useShortcutsStore((state) => {
     return state.setHistoricalRecords;
     return state.setHistoricalRecords;
   });
   });
+  const aiAssistant = useShortcutsStore((state) => {
+    return state.aiAssistant;
+  });
+  const setAiAssistant = useShortcutsStore((state) => {
+    return state.setAiAssistant;
+  });
   const newSession = useShortcutsStore((state) => state.newSession);
   const newSession = useShortcutsStore((state) => state.newSession);
   const setNewSession = useShortcutsStore((state) => state.setNewSession);
   const setNewSession = useShortcutsStore((state) => state.setNewSession);
   const fixedWindow = useShortcutsStore((state) => state.fixedWindow);
   const fixedWindow = useShortcutsStore((state) => state.fixedWindow);
@@ -110,6 +116,12 @@ const Shortcuts = () => {
       value: historicalRecords,
       value: historicalRecords,
       setValue: setHistoricalRecords,
       setValue: setHistoricalRecords,
     },
     },
+    {
+      title: "settings.advanced.shortcuts.aiAssistant.title",
+      description: "settings.advanced.shortcuts.aiAssistant.description",
+      value: aiAssistant,
+      setValue: setAiAssistant,
+    },
     {
     {
       title: "settings.advanced.shortcuts.newSession.title",
       title: "settings.advanced.shortcuts.newSession.title",
       description: "settings.advanced.shortcuts.newSession.description",
       description: "settings.advanced.shortcuts.newSession.description",

+ 31 - 2
src/components/Settings/Advanced/index.tsx

@@ -33,13 +33,26 @@ const Advanced = () => {
   const setConnectionTimeout = useConnectStore((state) => {
   const setConnectionTimeout = useConnectStore((state) => {
     return state.setConnectionTimeout;
     return state.setConnectionTimeout;
   });
   });
+  const queryTimeout = useConnectStore((state) => {
+    return state.queryTimeout;
+  });
+  const setQueryTimeout = useConnectStore((state) => {
+    return state.setQueryTimeout;
+  });
 
 
   useEffect(() => {
   useEffect(() => {
-    const unlisten = useStartupStore.subscribe((state) => {
+    const unsubscribeStartup = useStartupStore.subscribe((state) => {
       emit("change-startup-store", state);
       emit("change-startup-store", state);
     });
     });
 
 
-    return unlisten;
+    const unsubscribeConnect = useConnectStore.subscribe((state) => {
+      emit("change-connect-store", state);
+    });
+
+    return () => {
+      unsubscribeStartup();
+      unsubscribeConnect();
+    };
   }, []);
   }, []);
 
 
   const startupList = [
   const startupList = [
@@ -162,6 +175,22 @@ const Advanced = () => {
             }}
             }}
           />
           />
         </SettingsItem>
         </SettingsItem>
+
+        <SettingsItem
+          icon={Unplug}
+          title={t("settings.advanced.connect.queryTimeout.title")}
+          description={t("settings.advanced.connect.queryTimeout.description")}
+        >
+          <input
+            type="number"
+            min={1}
+            value={queryTimeout}
+            className="w-20 h-8 px-2 rounded-md border bg-transparent border-black/5 dark:border-white/10"
+            onChange={(event) => {
+              setQueryTimeout(Number(event.target.value) || 5);
+            }}
+          />
+        </SettingsItem>
       </div>
       </div>
     </div>
     </div>
   );
   );

+ 4 - 0
src/constants/index.ts

@@ -1,3 +1,7 @@
 export const POPOVER_PANEL_SELECTOR = '[id^="headlessui-popover-panel"]';
 export const POPOVER_PANEL_SELECTOR = '[id^="headlessui-popover-panel"]';
 
 
 export const HISTORY_PANEL_ID = "headlessui-popover-panel:history-panel";
 export const HISTORY_PANEL_ID = "headlessui-popover-panel:history-panel";
+
+export const CONTEXT_MENU_PANEL_ID = "headlessui-popover-panel:context-menu";
+
+export const AI_ASSISTANT_PANEL_ID = "headlessui-popover-panel:ai-assistant";

+ 32 - 24
src/hooks/useChatActions.ts

@@ -4,6 +4,8 @@ import type { Chat } from "@/components/Assistant/types";
 import { useAppStore } from "@/stores/appStore";
 import { useAppStore } from "@/stores/appStore";
 import { Get, Post } from "@/api/axiosRequest";
 import { Get, Post } from "@/api/axiosRequest";
 import platformAdapter from "@/utils/platformAdapter";
 import platformAdapter from "@/utils/platformAdapter";
+import { useConnectStore } from "@/stores/connectStore";
+import { useChatStore } from "@/stores/chatStore";
 
 
 export function useChatActions(
 export function useChatActions(
   currentServiceId: string | undefined,
   currentServiceId: string | undefined,
@@ -23,6 +25,8 @@ export function useChatActions(
 ) {
 ) {
   const isTauri = useAppStore((state) => state.isTauri);
   const isTauri = useAppStore((state) => state.isTauri);
   const addError = useAppStore((state) => state.addError);
   const addError = useAppStore((state) => state.addError);
+  const currentAssistant = useConnectStore((state) => state.currentAssistant);
+  const { connected } = useChatStore();
 
 
   const [keyword, setKeyword] = useState("");
   const [keyword, setKeyword] = useState("");
 
 
@@ -145,6 +149,7 @@ export function useChatActions(
         let response: any;
         let response: any;
         if (isTauri) {
         if (isTauri) {
           if (!currentServiceId) return;
           if (!currentServiceId) return;
+          console.log("currentAssistant", currentAssistant);
           response = await platformAdapter.commands("new_chat", {
           response = await platformAdapter.commands("new_chat", {
             serverId: currentServiceId,
             serverId: currentServiceId,
             websocketId: websocketSessionId || id,
             websocketId: websocketSessionId || id,
@@ -153,6 +158,7 @@ export function useChatActions(
               search: isSearchActive,
               search: isSearchActive,
               deep_thinking: isDeepThinkActive,
               deep_thinking: isDeepThinkActive,
               datasource: sourceDataIds?.join(",") || "",
               datasource: sourceDataIds?.join(",") || "",
+              assistant_id: currentAssistant?._id || '',
             },
             },
           });
           });
         } else {
         } else {
@@ -203,6 +209,7 @@ export function useChatActions(
       isDeepThinkActive,
       isDeepThinkActive,
       curIdRef,
       curIdRef,
       websocketSessionId,
       websocketSessionId,
+      currentAssistant,
     ]
     ]
   );
   );
 
 
@@ -228,6 +235,7 @@ export function useChatActions(
               search: isSearchActive,
               search: isSearchActive,
               deep_thinking: isDeepThinkActive,
               deep_thinking: isDeepThinkActive,
               datasource: sourceDataIds?.join(",") || "",
               datasource: sourceDataIds?.join(",") || "",
+              assistant_id: currentAssistant?._id || '',
             },
             },
             message: content,
             message: content,
           });
           });
@@ -280,6 +288,7 @@ export function useChatActions(
       setCurChatEnd,
       setCurChatEnd,
       changeInput,
       changeInput,
       websocketSessionId,
       websocketSessionId,
+      currentAssistant,
     ]
     ]
   );
   );
 
 
@@ -333,9 +342,9 @@ export function useChatActions(
   );
   );
 
 
   const getChatHistory = useCallback(async () => {
   const getChatHistory = useCallback(async () => {
-    try {
-      let response: any;
-      if (isTauri) {
+    let response: any;
+    if (isTauri) {
+      try {
         if (!currentServiceId) return [];
         if (!currentServiceId) return [];
         response = await platformAdapter.commands("chat_history", {
         response = await platformAdapter.commands("chat_history", {
           serverId: currentServiceId,
           serverId: currentServiceId,
@@ -343,32 +352,31 @@ export function useChatActions(
           size: 20,
           size: 20,
           query: keyword,
           query: keyword,
         });
         });
-        response = response ? JSON.parse(response) : null;
-      } else {
-        const [error, res] = await Get(`/chat/_history`, {
-          from: 0,
-          size: 20,
-        });
-        if (error) {
-          console.error("_history", error);
-          return [];
-        }
-        response = res;
+      } catch (error) {
+        console.error("chat_history", error);
       }
       }
-      console.log("_history", response);
-      const hits = response?.hits?.hits || [];
-
-      setChats(hits);
-      return hits;
-    } catch (error) {
-      console.error("chat_history:", error);
-      return [];
+      response = response ? JSON.parse(response) : null;
+    } else {
+      const [error, res] = await Get(`/chat/_history`, {
+        from: 0,
+        size: 20,
+      });
+      if (error) {
+        console.error("_history", error);
+        return [];
+      }
+      response = res;
     }
     }
+    console.log("_history", response);
+    const hits = response?.hits?.hits || [];
+
+    setChats(hits);
+    return hits;
   }, [currentServiceId, keyword]);
   }, [currentServiceId, keyword]);
 
 
   useEffect(() => {
   useEffect(() => {
-    showChatHistory && getChatHistory();
-  }, [showChatHistory]);
+    showChatHistory && connected && getChatHistory();
+  }, [showChatHistory, connected, getChatHistory]);
 
 
   const createChatWindow = useCallback(async (createWin: any) => {
   const createChatWindow = useCallback(async (createWin: any) => {
     if (isTauri) {
     if (isTauri) {

+ 23 - 0
src/hooks/useClickAway.ts

@@ -0,0 +1,23 @@
+import { useEffect, RefObject } from 'react';
+
+export function useClickAway(
+  ref: RefObject<HTMLElement>,
+  handler: (event: MouseEvent | TouchEvent) => void
+) {
+  useEffect(() => {
+    const listener = (event: MouseEvent | TouchEvent) => {
+      if (!ref.current || ref.current.contains(event.target as Node)) {
+        return;
+      }
+      handler(event);
+    };
+
+    document.addEventListener('mousedown', listener);
+    document.addEventListener('touchstart', listener);
+
+    return () => {
+      document.removeEventListener('mousedown', listener);
+      document.removeEventListener('touchstart', listener);
+    };
+  }, [ref, handler]);
+}

+ 24 - 10
src/hooks/useEscape.ts

@@ -1,20 +1,34 @@
-import { useEffect } from "react";
+import { useCallback, useEffect } from "react";
 
 
 import platformAdapter from "@/utils/platformAdapter";
 import platformAdapter from "@/utils/platformAdapter";
+import { useSearchStore } from "@/stores/searchStore";
 
 
 const useEscape = () => {
 const useEscape = () => {
-  const handleEscape = async (event: KeyboardEvent) => {
-    if (event.key === "Escape") {
-      console.log("Escape key pressed.");
+  const visibleContextMenu = useSearchStore((state) => {
+    return state.visibleContextMenu;
+  });
+  const setVisibleContextMenu = useSearchStore((state) => {
+    return state.setVisibleContextMenu;
+  });
 
 
-      event.preventDefault();
+  const handleEscape = useCallback(() => {
+    async (event: KeyboardEvent) => {
+      if (event.key === "Escape") {
+        console.log("Escape key pressed.");
 
 
-      // Hide the Tauri app window when 'Esc' is pressed
-      await platformAdapter.invokeBackend("hide_coco");
+        event.preventDefault();
 
 
-      console.log("App window hidden successfully.");
-    }
-  };
+        if (visibleContextMenu) {
+          return setVisibleContextMenu(false);
+        }
+
+        // Hide the Tauri app window when 'Esc' is pressed
+        await platformAdapter.invokeBackend("hide_coco");
+
+        console.log("App window hidden successfully.");
+      }
+    };
+  }, [visibleContextMenu]);
 
 
   useEffect(() => {
   useEffect(() => {
     const unlisten = platformAdapter.listenEvent("tauri://focus", () => {
     const unlisten = platformAdapter.listenEvent("tauri://focus", () => {

+ 33 - 0
src/hooks/useFeatureControl.ts

@@ -0,0 +1,33 @@
+import { useState, useEffect } from "react";
+
+import { useConnectStore } from "@/stores/connectStore";
+
+interface UseFeatureControlProps {
+  initialFeatures: string[];
+  featureToToggle: string;
+  condition: (assistant: any) => boolean;
+}
+
+export const useFeatureControl = ({
+  initialFeatures,
+  featureToToggle,
+  condition,
+}: UseFeatureControlProps) => {
+  const currentAssistant = useConnectStore((state) => state.currentAssistant);
+  const [features, setFeatures] = useState<string[]>(initialFeatures);
+
+  useEffect(() => {
+    if (condition(currentAssistant)) {
+      setFeatures((prev) => prev.filter((feature) => feature !== featureToToggle));
+    } else {
+      setFeatures((prev) => {
+        if (!prev.includes(featureToToggle)) {
+          return [...prev, featureToToggle];
+        }
+        return prev;
+      });
+    }
+  }, [JSON.stringify(currentAssistant), featureToToggle]);
+
+  return features;
+};

+ 18 - 0
src/hooks/useSyncStore.ts

@@ -1,3 +1,4 @@
+import { useConnectStore } from "@/stores/connectStore";
 import { useShortcutsStore } from "@/stores/shortcutsStore";
 import { useShortcutsStore } from "@/stores/shortcutsStore";
 import { useStartupStore } from "@/stores/startupStore";
 import { useStartupStore } from "@/stores/startupStore";
 import platformAdapter from "@/utils/platformAdapter";
 import platformAdapter from "@/utils/platformAdapter";
@@ -40,6 +41,9 @@ export const useSyncStore = () => {
   const setHistoricalRecords = useShortcutsStore((state) => {
   const setHistoricalRecords = useShortcutsStore((state) => {
     return state.setHistoricalRecords;
     return state.setHistoricalRecords;
   });
   });
+  const setAiAssistant = useShortcutsStore((state) => {
+    return state.setAiAssistant;
+  });
   const setNewSession = useShortcutsStore((state) => {
   const setNewSession = useShortcutsStore((state) => {
     return state.setNewSession;
     return state.setNewSession;
   });
   });
@@ -61,6 +65,12 @@ export const useSyncStore = () => {
   const setResetFixedWindow = useShortcutsStore((state) => {
   const setResetFixedWindow = useShortcutsStore((state) => {
     return state.setResetFixedWindow;
     return state.setResetFixedWindow;
   });
   });
+  const setConnectionTimeout = useConnectStore((state) => {
+    return state.setConnectionTimeout;
+  });
+  const setQueryTimeout = useConnectStore((state) => {
+    return state.setQueryTimeout;
+  });
 
 
   useEffect(() => {
   useEffect(() => {
     if (!resetFixedWindow) {
     if (!resetFixedWindow) {
@@ -83,6 +93,7 @@ export const useSyncStore = () => {
           internetSearch,
           internetSearch,
           internetSearchScope,
           internetSearchScope,
           historicalRecords,
           historicalRecords,
+          aiAssistant,
           newSession,
           newSession,
           fixedWindow,
           fixedWindow,
           serviceList,
           serviceList,
@@ -97,6 +108,7 @@ export const useSyncStore = () => {
         setInternetSearch(internetSearch);
         setInternetSearch(internetSearch);
         setInternetSearchScope(internetSearchScope);
         setInternetSearchScope(internetSearchScope);
         setHistoricalRecords(historicalRecords);
         setHistoricalRecords(historicalRecords);
+        setAiAssistant(aiAssistant);
         setNewSession(newSession);
         setNewSession(newSession);
         setFixedWindow(fixedWindow);
         setFixedWindow(fixedWindow);
         setServiceList(serviceList);
         setServiceList(serviceList);
@@ -113,6 +125,12 @@ export const useSyncStore = () => {
         setDefaultContentForSearchWindow(defaultContentForSearchWindow);
         setDefaultContentForSearchWindow(defaultContentForSearchWindow);
         setDefaultContentForChatWindow(defaultContentForChatWindow);
         setDefaultContentForChatWindow(defaultContentForChatWindow);
       }),
       }),
+
+      platformAdapter.listenEvent("change-connect-store", ({ payload }) => {
+        const { connectionTimeout, queryTimeout } = payload;
+        setConnectionTimeout(connectionTimeout);
+        setQueryTimeout(queryTimeout);
+      }),
     ]);
     ]);
 
 
     return () => {
     return () => {

+ 18 - 19
src/hooks/useWebSocket.ts

@@ -36,6 +36,7 @@ export default function useWebSocket({
   const messageQueue = useRef<string[]>([]);
   const messageQueue = useRef<string[]>([]);
   const processingRef = useRef(false);
   const processingRef = useRef(false);
 
 
+  // web
   const { readyState, connect, disconnect } = useWebSocketAHook(
   const { readyState, connect, disconnect } = useWebSocketAHook(
     // "wss://coco.infini.cloud/ws",
     // "wss://coco.infini.cloud/ws",
     // "ws://localhost:9000/ws",
     // "ws://localhost:9000/ws",
@@ -53,17 +54,17 @@ export default function useWebSocket({
   );
   );
   useEffect(() => {
   useEffect(() => {
     if (!isTauri) {
     if (!isTauri) {
-      connect();
+      connect(); // web
     }
     }
   }, [isTauri, connect]);
   }, [isTauri, connect]);
-
   const processMessage = useCallback(
   const processMessage = useCallback(
     (msg: string) => {
     (msg: string) => {
       try {
       try {
         if (msg.includes("websocket-session-id")) {
         if (msg.includes("websocket-session-id")) {
           const sessionId = msg.split(":")[1].trim();
           const sessionId = msg.split(":")[1].trim();
           websocketIdRef.current = sessionId;
           websocketIdRef.current = sessionId;
-          setConnected(true);
+          setConnected(true); // web connected
+          console.log("setConnected:", sessionId);
           onWebsocketSessionId?.(sessionId);
           onWebsocketSessionId?.(sessionId);
         } else {
         } else {
           dealMsgRef.current?.(msg);
           dealMsgRef.current?.(msg);
@@ -74,7 +75,6 @@ export default function useWebSocket({
     },
     },
     [onWebsocketSessionId]
     [onWebsocketSessionId]
   );
   );
-
   const processQueue = useCallback(() => {
   const processQueue = useCallback(() => {
     if (processingRef.current || messageQueue.current.length === 0) return;
     if (processingRef.current || messageQueue.current.length === 0) return;
 
 
@@ -88,13 +88,14 @@ export default function useWebSocket({
     }
     }
     processingRef.current = false;
     processingRef.current = false;
   }, [processMessage]);
   }, [processMessage]);
-
   useEffect(() => {
   useEffect(() => {
+    // web
     if (readyState !== ReadyState.Open) {
     if (readyState !== ReadyState.Open) {
-      setConnected(false);
+      setConnected(false); // state
     }
     }
   }, [readyState]);
   }, [readyState]);
 
 
+  // Tauri
   // 1. WebSocket connects when loading or switching services
   // 1. WebSocket connects when loading or switching services
   // src/components/Assistant/ChatHeader.tsx
   // src/components/Assistant/ChatHeader.tsx
   // 2. If not connected or disconnected, input box has a connect button, clicking it will connect to WebSocket
   // 2. If not connected or disconnected, input box has a connect button, clicking it will connect to WebSocket
@@ -108,7 +109,7 @@ export default function useWebSocket({
           // console.log("reconnect", targetServer.id);
           // console.log("reconnect", targetServer.id);
           await platformAdapter.commands("connect_to_server", targetServer.id, clientId);
           await platformAdapter.commands("connect_to_server", targetServer.id, clientId);
         } catch (error) {
         } catch (error) {
-          setConnected(false);
+          setConnected(false); // error
           console.error("Failed to connect:", error);
           console.error("Failed to connect:", error);
         }
         }
       } else {
       } else {
@@ -117,15 +118,13 @@ export default function useWebSocket({
     },
     },
     [currentService]
     [currentService]
   );
   );
-
-
   const disconnectWS = async () => {
   const disconnectWS = async () => {
     if (!connected) return;
     if (!connected) return;
     if (isTauri) {
     if (isTauri) {
       try {
       try {
         console.log("disconnect");
         console.log("disconnect");
         await platformAdapter.commands("disconnect", clientId);
         await platformAdapter.commands("disconnect", clientId);
-        setConnected(false);
+        setConnected(false); // disconnected
       } catch (error) {
       } catch (error) {
         console.error("Failed to disconnect:", error);
         console.error("Failed to disconnect:", error);
       }
       }
@@ -133,14 +132,12 @@ export default function useWebSocket({
       disconnect();
       disconnect();
     }
     }
   };
   };
-
   const updateDealMsg = useCallback(
   const updateDealMsg = useCallback(
     (newDealMsg: (msg: string) => void) => {
     (newDealMsg: (msg: string) => void) => {
       dealMsgRef.current = newDealMsg;
       dealMsgRef.current = newDealMsg;
     },
     },
     [dealMsgRef]
     [dealMsgRef]
   );
   );
-
   useEffect(() => {
   useEffect(() => {
     if (!currentService?.id) return;
     if (!currentService?.id) return;
 
 
@@ -148,6 +145,7 @@ export default function useWebSocket({
     let unlisten_message = null;
     let unlisten_message = null;
 
 
     if (!isTauri) return;
     if (!isTauri) return;
+    
     unlisten_error = platformAdapter.listenEvent(`ws-error-${clientId}`, (event) => {
     unlisten_error = platformAdapter.listenEvent(`ws-error-${clientId}`, (event) => {
       // {
       // {
       //   "error": {
       //   "error": {
@@ -155,20 +153,21 @@ export default function useWebSocket({
       //   },
       //   },
       //   "status": 401
       //   "status": 401
       // }
       // }
-      console.error(`ws-error-${clientId}`, event);
-      if(!connected) return;
-      setConnected(false);
-      addError("WebSocket connection failed.");
+      console.error(`ws-error-${clientId}`, event, connected);
+      if(connected) {
+        addError("WebSocket connection failed.");
+      }
+      setConnected(false); // error
     });
     });
 
 
     unlisten_message = platformAdapter.listenEvent(`ws-message-${clientId}`, (event) => {
     unlisten_message = platformAdapter.listenEvent(`ws-message-${clientId}`, (event) => {
       const msg = event.payload as string;
       const msg = event.payload as string;
-      console.log(`ws-message-${clientId}`, msg);
+      // console.log(`ws-message-${clientId}`, msg);
       if (msg.includes("websocket-session-id")) {
       if (msg.includes("websocket-session-id")) {
         const sessionId = msg.split(":")[1].trim();
         const sessionId = msg.split(":")[1].trim();
         websocketIdRef.current = sessionId;
         websocketIdRef.current = sessionId;
-        console.log("sessionId:", sessionId);
-        setConnected(true);
+        console.log("setConnected sessionId:", sessionId);
+        setConnected(true); // Tauri connected
         if (onWebsocketSessionId) {
         if (onWebsocketSessionId) {
           onWebsocketSessionId(sessionId);
           onWebsocketSessionId(sessionId);
         }
         }

+ 27 - 2
src/locales/en/translation.json

@@ -119,6 +119,10 @@
           "title": "Conversation History",
           "title": "Conversation History",
           "description": "Shortcut to view past conversation history in chat mode."
           "description": "Shortcut to view past conversation history in chat mode."
         },
         },
+        "aiAssistant": {
+          "title": "AI Assistant",
+          "description": "Shortcut to view AI assistant list in chat mode."
+        },
         "newSession": {
         "newSession": {
           "title": "New Conversation",
           "title": "New Conversation",
           "description": "Shortcut to start a new conversation in chat mode."
           "description": "Shortcut to start a new conversation in chat mode."
@@ -141,6 +145,10 @@
         "connectionTimeout": {
         "connectionTimeout": {
           "title": "Connection Timeout",
           "title": "Connection Timeout",
           "description": "Retries the connection if no response is received within this time. Default: 120s."
           "description": "Retries the connection if no response is received within this time. Default: 120s."
+        },
+        "queryTimeout": {
+          "title": "Query Timeout",
+          "description": "Terminates the query if no search results are returned within this time. Default: 5s."
         }
         }
       }
       }
     },
     },
@@ -180,6 +188,7 @@
       "id": "ID",
       "id": "ID",
       "createdAt": "Created At",
       "createdAt": "Created At",
       "category": "Category",
       "category": "Category",
+      "richCategories": "RichCategories",
       "subcategory": "Subcategory",
       "subcategory": "Subcategory",
       "language": "Language",
       "language": "Language",
       "tags": "Tags",
       "tags": "Tags",
@@ -193,7 +202,8 @@
     "list": {
     "list": {
       "loading": "Loading...",
       "loading": "Loading...",
       "noResults": "No Results",
       "noResults": "No Results",
-      "noDataAlt": "No data image"
+      "noDataAlt": "No data image",
+      "failures": "Partial results returned due to service failures."
     },
     },
     "footer": {
     "footer": {
       "logoAlt": "Coco Logo",
       "logoAlt": "Coco Logo",
@@ -232,7 +242,14 @@
     },
     },
     "contextMenu": {
     "contextMenu": {
       "open": "Open",
       "open": "Open",
-      "copyLink": "Copy Link"
+      "copyLink": "Copy Link",
+      "copyAnswer": "Copy Answer",
+      "copyUppercaseAnswer": "Copy Answer (in Word)",
+      "copyQuestionAndAnswer": "Copy Question and Answer",
+      "title": {
+        "calculator": "Calculator"
+      },
+      "search": "Search Operation"
     }
     }
   },
   },
   "assistant": {
   "assistant": {
@@ -373,5 +390,13 @@
         "cancel": "Cancel"
         "cancel": "Cancel"
       }
       }
     }
     }
+  },
+  "calculator": {
+    "sum": "Sum",
+    "subtract": "Difference",
+    "multiply": "Product",
+    "divide": "Divide",
+    "remainder": "Remainder",
+    "expression": "Expression"
   }
   }
 }
 }

+ 27 - 2
src/locales/zh/translation.json

@@ -119,6 +119,10 @@
           "title": "历史记录",
           "title": "历史记录",
           "description": "在聊天模式下查看历史对话记录的快捷键。"
           "description": "在聊天模式下查看历史对话记录的快捷键。"
         },
         },
+        "aiAssistant": {
+          "title": "AI 助手",
+          "description": "在聊天模式下查看 AI 助手列表快捷键。"
+        },
         "newSession": {
         "newSession": {
           "title": "新建会话",
           "title": "新建会话",
           "description": "在聊天模式下创建新对话的快捷按键。"
           "description": "在聊天模式下创建新对话的快捷按键。"
@@ -141,6 +145,10 @@
         "connectionTimeout": {
         "connectionTimeout": {
           "title": "连接超时",
           "title": "连接超时",
           "description": "如果在此时间内未收到响应,则重试连接。默认值:120 秒。"
           "description": "如果在此时间内未收到响应,则重试连接。默认值:120 秒。"
+        },
+        "queryTimeout": {
+          "title": "查询超时",
+          "description": "在此时间内未返回搜索结果,则终止查询。默认值:5 秒。"
         }
         }
       }
       }
     },
     },
@@ -182,6 +190,7 @@
       "id": "ID",
       "id": "ID",
       "createdAt": "创建时间",
       "createdAt": "创建时间",
       "category": "分类",
       "category": "分类",
+      "richCategories": "分类",
       "subcategory": "子分类",
       "subcategory": "子分类",
       "language": "语言",
       "language": "语言",
       "tags": "标签",
       "tags": "标签",
@@ -195,7 +204,8 @@
     "list": {
     "list": {
       "loading": "加载中...",
       "loading": "加载中...",
       "noResults": "暂无结果",
       "noResults": "暂无结果",
-      "noDataAlt": "无数据图片"
+      "noDataAlt": "无数据图片",
+      "failures": "部分服务暂时不可用,请检查相关设置。"
     },
     },
     "footer": {
     "footer": {
       "logoAlt": "Coco 图标",
       "logoAlt": "Coco 图标",
@@ -234,7 +244,14 @@
     },
     },
     "contextMenu": {
     "contextMenu": {
       "open": "打开",
       "open": "打开",
-      "copyLink": "复制链接"
+      "copyLink": "复制链接",
+      "copyAnswer": "复制答案",
+      "copyUppercaseAnswer": "复制答案(大写)",
+      "copyQuestionAndAnswer": "复制问题和答案",
+      "title": {
+        "calculator": "计算器"
+      },
+      "search": "搜索操作"
     }
     }
   },
   },
   "assistant": {
   "assistant": {
@@ -374,5 +391,13 @@
         "cancel": "取消"
         "cancel": "取消"
       }
       }
     }
     }
+  },
+  "calculator": {
+    "sum": "求和",
+    "subtract": "相减",
+    "multiply": "相乘",
+    "divide": "相除",
+    "remainder": "求余",
+    "expression": "表达式"
   }
   }
 }
 }

+ 0 - 1
src/pages/chat/index.tsx

@@ -291,7 +291,6 @@ export default function Chat({}: ChatProps) {
               ref={chatAIRef}
               ref={chatAIRef}
               key="ChatAI"
               key="ChatAI"
               activeChatProp={activeChat}
               activeChatProp={activeChat}
-              isTransitioned={true}
               isSearchActive={isSearchActive}
               isSearchActive={isSearchActive}
               isDeepThinkActive={isDeepThinkActive}
               isDeepThinkActive={isDeepThinkActive}
               setIsSidebarOpen={setIsSidebarOpen}
               setIsSidebarOpen={setIsSidebarOpen}

+ 1 - 43
src/pages/main/index.tsx

@@ -8,47 +8,7 @@ import { useSyncStore } from "@/hooks/useSyncStore";
 function MainApp() {
 function MainApp() {
   const setIsTauri = useAppStore((state) => state.setIsTauri);
   const setIsTauri = useAppStore((state) => state.setIsTauri);
   setIsTauri(true);
   setIsTauri(true);
-
-  const querySearch = useCallback(async (input: string) => {
-    try {
-      const response: any = await platformAdapter.commands(
-        "query_coco_fusion",
-        {
-          from: 0,
-          size: 10,
-          queryStrings: { query: input },
-        }
-      );
-      if (!response || typeof response !== "object") {
-        throw new Error("Invalid response format");
-      }
-      return response;
-    } catch (error) {
-      console.error("query_coco_fusion error:", error);
-      throw error;
-    }
-  }, []);
-
-  const queryDocuments = useCallback(
-    async (from: number, size: number, queryStrings: any) => {
-      try {
-        const response: any = await platformAdapter.commands(
-          "query_coco_fusion",
-          {
-            from,
-            size,
-            queryStrings,
-          }
-        );
-        return response;
-      } catch (error) {
-        console.error("query_coco_fusion error:", error);
-        throw error;
-      }
-    },
-    []
-  );
-
+ 
   const hideCoco = useCallback(() => {
   const hideCoco = useCallback(() => {
     return platformAdapter.hideWindow();
     return platformAdapter.hideWindow();
   }, []);
   }, []);
@@ -58,8 +18,6 @@ function MainApp() {
   return (
   return (
     <SearchChat
     <SearchChat
       isTauri={true}
       isTauri={true}
-      querySearch={querySearch}
-      queryDocuments={queryDocuments}
       hideCoco={hideCoco}
       hideCoco={hideCoco}
       hasModules={["search", "chat"]}
       hasModules={["search", "chat"]}
     />
     />

+ 0 - 7
src/pages/web/README.md

@@ -32,12 +32,6 @@
 - **默认值**: `['search', 'chat']`
 - **默认值**: `['search', 'chat']`
 - **描述**: 启用的功能模块列表,目前支持 'search' 和 'chat' 模块
 - **描述**: 启用的功能模块列表,目前支持 'search' 和 'chat' 模块
 
 
-### `hasFeature`
-- **类型**: `string[]`
-- **可选**: 是
-- **默认值**: `['think', 'search', 'think_active', 'search_active']`
-- **描述**: 启用的特性列表,支持 'think'、'search'、'think_active'、'search_active' 特性。其中 'think_active' 表示默认开启深度思考,'search_active' 表示默认开启搜索
-
 ### `hideCoco`
 ### `hideCoco`
 - **类型**: `() => void`
 - **类型**: `() => void`
 - **可选**: 是
 - **可选**: 是
@@ -87,7 +81,6 @@ function App() {
       width={680}
       width={680}
       height={590}
       height={590}
       hasModules={['search', 'chat']}
       hasModules={['search', 'chat']}
-      hasFeature={['think', 'search', 'think_active', 'search_active']}
       hideCoco={() => console.log('hide')}
       hideCoco={() => console.log('hide')}
       theme="dark"
       theme="dark"
       searchPlaceholder=""
       searchPlaceholder=""

+ 7 - 64
src/pages/web/index.tsx

@@ -1,10 +1,9 @@
-import { useEffect, useCallback, useState } from "react";
+import { useEffect, useState } from "react";
 
 
 import SearchChat from "@/components/SearchChat";
 import SearchChat from "@/components/SearchChat";
 import { useAppStore } from "@/stores/appStore";
 import { useAppStore } from "@/stores/appStore";
-import { Get } from "@/api/axiosRequest";
 import { useShortcutsStore } from "@/stores/shortcutsStore";
 import { useShortcutsStore } from "@/stores/shortcutsStore";
-import { useIsMobile } from '@/hooks/useIsMobile';
+import { useIsMobile } from "@/hooks/useIsMobile";
 import { useModifierKeyPress } from "@/hooks/useModifierKeyPress";
 import { useModifierKeyPress } from "@/hooks/useModifierKeyPress";
 
 
 import "@/i18n";
 import "@/i18n";
@@ -17,7 +16,7 @@ interface WebAppProps {
   height?: number;
   height?: number;
   hasModules?: string[];
   hasModules?: string[];
   defaultModule?: "search" | "chat";
   defaultModule?: "search" | "chat";
-  hasFeature?: string[];
+  assistantIDs?: string[];
   hideCoco?: () => void;
   hideCoco?: () => void;
   theme?: "auto" | "light" | "dark";
   theme?: "auto" | "light" | "dark";
   searchPlaceholder?: string;
   searchPlaceholder?: string;
@@ -32,7 +31,7 @@ function WebApp({
   height = 590,
   height = 590,
   headers = {
   headers = {
     "X-API-TOKEN":
     "X-API-TOKEN":
-      "cvvitp6hpceh0ip1q1706byts41c7213k4el22v3bp6f4ta2sar0u29jp4pg08h6xcyxn085x3lq1k7wojof",
+      "cvqt6r02sdb2v3bkgip0x3ixv01f3r2lhnxoz1efbn160wm9og58wtv8t6wrv1ebvnvypuc23dx9pb33aemh",
     "APP-INTEGRATION-ID": "cvkm9hmhpcemufsg3vug",
     "APP-INTEGRATION-ID": "cvkm9hmhpcemufsg3vug",
   },
   },
   // token = "cva1j5ehpcenic3ir7k0h8fb8qtv35iwtywze248oscrej8yoivhb5b1hyovp24xejjk27jy9ddt69ewfi3n",   // https://coco.infini.cloud
   // token = "cva1j5ehpcenic3ir7k0h8fb8qtv35iwtywze248oscrej8yoivhb5b1hyovp24xejjk27jy9ddt69ewfi3n",   // https://coco.infini.cloud
@@ -42,7 +41,7 @@ function WebApp({
   hideCoco = () => {},
   hideCoco = () => {},
   hasModules = ["search", "chat"],
   hasModules = ["search", "chat"],
   defaultModule = "search",
   defaultModule = "search",
-  hasFeature = ["think_active", "search_active"],
+  assistantIDs = [],
   theme = "dark",
   theme = "dark",
   searchPlaceholder = "",
   searchPlaceholder = "",
   chatPlaceholder = "",
   chatPlaceholder = "",
@@ -67,60 +66,6 @@ function WebApp({
 
 
   useModifierKeyPress();
   useModifierKeyPress();
 
 
-  const query_coco_fusion = useCallback(async (url: string) => {
-    try {
-      const [error, response]: any = await Get(url);
-
-      if (error) {
-        console.error("_search", error);
-        return { hits: [], total: 0 };
-      }
-
-      console.log("_suggest", url, response);
-      const hits =
-        response?.hits?.hits?.map((hit: any) => ({
-          document: {
-            ...hit._source,
-          },
-          score: hit._score || 0,
-          source: hit._source.source || null,
-        })) || [];
-      const total = response?.hits?.total?.value || 0;
-
-      console.log("_suggest2", url, total, hits);
-
-      return {
-        hits: hits,
-        total_hits: total,
-      };
-    } catch (error) {
-      console.error("query_coco_fusion error:", error);
-      throw error;
-    }
-  }, []);
-
-  const querySearch = useCallback(async (input: string) => {
-    console.log(input);
-    return await query_coco_fusion(`/query/_search?query=${input}`);
-  }, []);
-
-  const queryDocuments = useCallback(
-    async (from: number, size: number, queryStrings: any) => {
-      console.log(from, size, queryStrings);
-      try {
-        let url = `/query/_search?query=${queryStrings.query}&datasource=${queryStrings.datasource}&from=${from}&size=${size}`;
-        if (queryStrings?.rich_categories) {
-          url = `/query/_search?query=${queryStrings.query}&rich_category=${queryStrings.rich_category}&from=${from}&size=${size}`;
-        }
-        return await query_coco_fusion(url);
-      } catch (error) {
-        console.error("query_coco_fusion error:", error);
-        throw error;
-      }
-    },
-    []
-  );
-
   const isMobile = useIsMobile();
   const isMobile = useIsMobile();
 
 
   const [isChatMode, setIsChatMode] = useState(false);
   const [isChatMode, setIsChatMode] = useState(false);
@@ -139,7 +84,7 @@ function WebApp({
       {isMobile && (
       {isMobile && (
         <div
         <div
           className={`fixed ${
           className={`fixed ${
-            isChatMode ? "top-2" : "top-3"
+            isChatMode ? "top-1" : "top-3"
           } right-2 flex items-center justify-center w-8 h-8 rounded-full bg-black/10 dark:bg-white/10 cursor-pointer z-50`}
           } right-2 flex items-center justify-center w-8 h-8 rounded-full bg-black/10 dark:bg-white/10 cursor-pointer z-50`}
           onClick={onCancel}
           onClick={onCancel}
         >
         >
@@ -158,16 +103,14 @@ function WebApp({
         hideCoco={hideCoco}
         hideCoco={hideCoco}
         hasModules={hasModules}
         hasModules={hasModules}
         defaultModule={defaultModule}
         defaultModule={defaultModule}
-        hasFeature={hasFeature}
         theme={theme}
         theme={theme}
         searchPlaceholder={searchPlaceholder}
         searchPlaceholder={searchPlaceholder}
         chatPlaceholder={chatPlaceholder}
         chatPlaceholder={chatPlaceholder}
-        querySearch={querySearch}
-        queryDocuments={queryDocuments}
         showChatHistory={showChatHistory}
         showChatHistory={showChatHistory}
         setIsPinned={setIsPinned}
         setIsPinned={setIsPinned}
         onModeChange={setIsChatMode}
         onModeChange={setIsChatMode}
         isMobile={isMobile}
         isMobile={isMobile}
+        assistantIDs={assistantIDs}
       />
       />
     </div>
     </div>
   );
   );

+ 8 - 5
src/stores/appStore.ts

@@ -28,7 +28,7 @@ export interface IServer {
 
 
 interface ErrorMessage {
 interface ErrorMessage {
   id: string;
   id: string;
-  type: 'error' | 'warning' | 'info';
+  type: "error" | "warning" | "info";
   message: string;
   message: string;
   timestamp: number;
   timestamp: number;
 }
 }
@@ -38,7 +38,7 @@ export type IAppStore = {
   setShowTooltip: (showTooltip: boolean) => void;
   setShowTooltip: (showTooltip: boolean) => void;
 
 
   errors: ErrorMessage[];
   errors: ErrorMessage[];
-  addError: (message: string, type?: 'error' | 'warning' | 'info') => void;
+  addError: (message: string, type?: "error" | "warning" | "info") => void;
   removeError: (id: string) => void;
   removeError: (id: string) => void;
   clearErrors: () => void;
   clearErrors: () => void;
 
 
@@ -74,20 +74,23 @@ export const useAppStore = create<IAppStore>()(
       showTooltip: true,
       showTooltip: true,
       setShowTooltip: (showTooltip: boolean) => set({ showTooltip }),
       setShowTooltip: (showTooltip: boolean) => set({ showTooltip }),
       errors: [],
       errors: [],
-      addError: (message: string, type: 'error' | 'warning' | 'info' = 'error') => 
+      addError: (
+        message: string,
+        type: "error" | "warning" | "info" = "error"
+      ) =>
         set((state) => {
         set((state) => {
           const newError = {
           const newError = {
             id: Date.now().toString(),
             id: Date.now().toString(),
             type,
             type,
             message,
             message,
-            timestamp: Date.now()
+            timestamp: Date.now(),
           };
           };
           const updatedErrors = [newError, ...state.errors].slice(0, 5);
           const updatedErrors = [newError, ...state.errors].slice(0, 5);
           return { errors: updatedErrors };
           return { errors: updatedErrors };
         }),
         }),
       removeError: (id: string) =>
       removeError: (id: string) =>
         set((state) => ({
         set((state) => ({
-          errors: state.errors.filter(error => error.id !== id)
+          errors: state.errors.filter((error) => error.id !== id),
         })),
         })),
       clearErrors: () => set({ errors: [] }),
       clearErrors: () => set({ errors: [] }),
 
 

+ 101 - 69
src/stores/connectStore.ts

@@ -1,5 +1,5 @@
 import { create } from "zustand";
 import { create } from "zustand";
-import { persist } from "zustand/middleware";
+import { persist, subscribeWithSelector } from "zustand/middleware";
 import { produce } from "immer";
 import { produce } from "immer";
 
 
 import platformAdapter from "@/utils/platformAdapter";
 import platformAdapter from "@/utils/platformAdapter";
@@ -24,77 +24,109 @@ export type IConnectStore = {
   setConnectionTimeout: (connectionTimeout: number) => void;
   setConnectionTimeout: (connectionTimeout: number) => void;
   currentSessionId?: string;
   currentSessionId?: string;
   setCurrentSessionId: (currentSessionId?: string) => void;
   setCurrentSessionId: (currentSessionId?: string) => void;
+  assistantList: any[];
+  setAssistantList: (assistantList: []) => void;
+  currentAssistant: any;
+  setCurrentAssistant: (assistant: any) => void;
+  queryTimeout: number;
+  setQueryTimeout: (queryTimeout: number) => void;
+  visibleStartPage: boolean;
+  setVisibleStartPage: (visibleStartPage: boolean) => void;
 };
 };
 
 
 export const useConnectStore = create<IConnectStore>()(
 export const useConnectStore = create<IConnectStore>()(
-  persist(
-    (set) => ({
-      serverList: [],
-      setServerList: (serverList: []) => {
-        console.log("set serverList:", serverList);
-        set(
-          produce((draft) => {
-            draft.serverList = serverList;
-          })
-        );
-      },
-      currentService: "default_coco_server",
-      setCurrentService: (server: any) => {
-        console.log("set default server:", server);
-        set(
-          produce((draft) => {
-            draft.currentService = server;
-          })
-        );
-      },
-      connector_data: {},
-      setConnectorData: async (connector_data: any[], key: string) => {
-        set(
-          produce((draft) => {
-            draft.connector_data[key] = connector_data;
-          })
-        );
-        await platformAdapter.emitEvent(CONNECTOR_CHANGE_EVENT, {
-          connector_data,
-        });
-      },
-      datasourceData: {},
-      setDatasourceData: async (datasourceData: any[], key: string) => {
-        set(
-          produce((draft) => {
-            draft.datasourceData[key] = datasourceData;
-          })
-        );
-        await platformAdapter.emitEvent(DATASOURCE_CHANGE_EVENT, {
-          datasourceData,
-        });
-      },
-      initializeListeners: () => {
-        platformAdapter.listenEvent(CONNECTOR_CHANGE_EVENT, (event: any) => {
-          const { connector_data } = event.payload;
-          set({ connector_data });
-        });
-        platformAdapter.listenEvent(DATASOURCE_CHANGE_EVENT, (event: any) => {
-          const { datasourceData } = event.payload;
-          set({ datasourceData });
-        });
-      },
-      connectionTimeout: 120,
-      setConnectionTimeout: (connectionTimeout: number) => {
-        return set(() => ({ connectionTimeout }));
-      },
-      setCurrentSessionId(currentSessionId) {
-        return set(() => ({ currentSessionId }));
-      },
-    }),
-    {
-      name: "connect-store",
-      partialize: (state) => ({
-        currentService: state.currentService,
-        connector_data: state.connector_data,
-        datasourceData: state.datasourceData,
-        connectionTimeout: state.connectionTimeout,
+  subscribeWithSelector(
+    persist(
+      (set) => ({
+        serverList: [],
+        setServerList: (serverList: []) => {
+          console.log("set serverList:", serverList);
+          set(
+            produce((draft) => {
+              draft.serverList = serverList;
+            })
+          );
+        },
+        currentService: "default_coco_server",
+        setCurrentService: (server: any) => {
+          console.log("set default server:", server);
+          set(
+            produce((draft) => {
+              draft.currentService = server;
+            })
+          );
+        },
+        connector_data: {},
+        setConnectorData: async (connector_data: any[], key: string) => {
+          set(
+            produce((draft) => {
+              draft.connector_data[key] = connector_data;
+            })
+          );
+          await platformAdapter.emitEvent(CONNECTOR_CHANGE_EVENT, {
+            connector_data,
+          });
+        },
+        datasourceData: {},
+        setDatasourceData: async (datasourceData: any[], key: string) => {
+          set(
+            produce((draft) => {
+              draft.datasourceData[key] = datasourceData;
+            })
+          );
+          await platformAdapter.emitEvent(DATASOURCE_CHANGE_EVENT, {
+            datasourceData,
+          });
+        },
+        initializeListeners: () => {
+          platformAdapter.listenEvent(CONNECTOR_CHANGE_EVENT, (event: any) => {
+            const { connector_data } = event.payload;
+            set({ connector_data });
+          });
+          platformAdapter.listenEvent(DATASOURCE_CHANGE_EVENT, (event: any) => {
+            const { datasourceData } = event.payload;
+            set({ datasourceData });
+          });
+        },
+        connectionTimeout: 120,
+        setConnectionTimeout: (connectionTimeout: number) => {
+          return set(() => ({ connectionTimeout }));
+        },
+        setCurrentSessionId(currentSessionId) {
+          return set(() => ({ currentSessionId }));
+        },
+        assistantList: [],
+        setAssistantList: (assistantList) => {
+          return set(() => ({ assistantList }));
+        },
+        currentAssistant: null,
+        setCurrentAssistant: (assistant: any) => {
+          set(
+            produce((draft) => {
+              draft.currentAssistant = assistant;
+            })
+          );
+        },
+        queryTimeout: 5,
+        setQueryTimeout: (queryTimeout: number) => {
+          return set(() => ({ queryTimeout }));
+        },
+        visibleStartPage: false,
+        setVisibleStartPage: (visibleStartPage: boolean) => {
+          return set(() => ({ visibleStartPage }));
+        },
       }),
       }),
-    }
+      {
+        name: "connect-store",
+        partialize: (state) => ({
+          currentService: state.currentService,
+          connector_data: state.connector_data,
+          datasourceData: state.datasourceData,
+          connectionTimeout: state.connectionTimeout,
+          currentAssistant: state.currentAssistant,
+          queryTimeout: state.queryTimeout,
+        }),
+      }
+    )
   )
   )
 );
 );

+ 5 - 0
src/stores/shortcutsStore.ts

@@ -27,6 +27,8 @@ export type IShortcutsStore = {
   setInternetSearchScope: (internetSearchScope: string) => void;
   setInternetSearchScope: (internetSearchScope: string) => void;
   historicalRecords: string;
   historicalRecords: string;
   setHistoricalRecords: (historicalRecords: string) => void;
   setHistoricalRecords: (historicalRecords: string) => void;
+  aiAssistant: string;
+  setAiAssistant: (aiAssistant: string) => void;
   newSession: string;
   newSession: string;
   setNewSession: (newSession: string) => void;
   setNewSession: (newSession: string) => void;
   fixedWindow: string;
   fixedWindow: string;
@@ -72,6 +74,8 @@ export const useShortcutsStore = create<IShortcutsStore>()(
       setHistoricalRecords: (historicalRecords) => {
       setHistoricalRecords: (historicalRecords) => {
         return set({ historicalRecords });
         return set({ historicalRecords });
       },
       },
+      aiAssistant: "U",
+      setAiAssistant: (aiAssistant) => set({ aiAssistant }),
       newSession: "N",
       newSession: "N",
       setNewSession: (newSession) => set({ newSession }),
       setNewSession: (newSession) => set({ newSession }),
       fixedWindow: "P",
       fixedWindow: "P",
@@ -96,6 +100,7 @@ export const useShortcutsStore = create<IShortcutsStore>()(
         deepThinking: state.deepThinking,
         deepThinking: state.deepThinking,
         internetSearch: state.internetSearch,
         internetSearch: state.internetSearch,
         historicalRecords: state.historicalRecords,
         historicalRecords: state.historicalRecords,
+        aiAssistant: state.aiAssistant,
         newSession: state.newSession,
         newSession: state.newSession,
         fixedWindow: state.fixedWindow,
         fixedWindow: state.fixedWindow,
         serviceList: state.serviceList,
         serviceList: state.serviceList,

+ 2 - 0
src/types/platform.ts

@@ -1,3 +1,4 @@
+import { IConnectStore } from "@/stores/connectStore";
 import { IShortcutsStore } from "@/stores/shortcutsStore";
 import { IShortcutsStore } from "@/stores/shortcutsStore";
 import { IStartupStore } from "@/stores/startupStore";
 import { IStartupStore } from "@/stores/startupStore";
 import { AppTheme } from "@/types/index";
 import { AppTheme } from "@/types/index";
@@ -36,6 +37,7 @@ export interface EventPayloads {
   [key: `ws-message-${string}`]: string;
   [key: `ws-message-${string}`]: string;
   "change-startup-store": IStartupStore;
   "change-startup-store": IStartupStore;
   "change-shortcuts-store": IShortcutsStore;
   "change-shortcuts-store": IShortcutsStore;
+  "change-connect-store": IConnectStore;
 }
 }
 
 
 // Window operation interface
 // Window operation interface

+ 7 - 1
src/utils/index.ts

@@ -1,9 +1,13 @@
 import { useEffect, useState } from "react";
 import { useEffect, useState } from "react";
 
 
 import platformAdapter from "./platformAdapter";
 import platformAdapter from "./platformAdapter";
+import { useAppStore } from "@/stores/appStore";
 
 
 // 1
 // 1
 export async function copyToClipboard(text: string) {
 export async function copyToClipboard(text: string) {
+  const addError = useAppStore.getState().addError;
+  const language = useAppStore.getState().language;
+
   try {
   try {
     if (window.__TAURI__) {
     if (window.__TAURI__) {
       window.__TAURI__.writeText(text);
       window.__TAURI__.writeText(text);
@@ -26,6 +30,8 @@ export async function copyToClipboard(text: string) {
     }
     }
     document.body.removeChild(textArea);
     document.body.removeChild(textArea);
   }
   }
+
+  addError(language === "zh" ? "复制成功" : "Copy Success", "info");
 }
 }
 
 
 // 2
 // 2
@@ -95,4 +101,4 @@ export const isImage = (value: string) => {
   const regex = /\.(jpe?g|png|webp|avif|gif|svg|bmp|ico|tiff?|heic|apng)$/i;
   const regex = /\.(jpe?g|png|webp|avif|gif|svg|bmp|ico|tiff?|heic|apng)$/i;
 
 
   return regex.test(value);
   return regex.test(value);
-};
+};

+ 2 - 2
src/utils/platformAdapter.ts

@@ -1,8 +1,8 @@
 // manual modification
 // manual modification
-//import { createWebAdapter } from './webAdapter';
 import { createTauriAdapter } from "./tauriAdapter";
 import { createTauriAdapter } from "./tauriAdapter";
+// import { createWebAdapter } from './webAdapter';
 
 
 let platformAdapter = createTauriAdapter();
 let platformAdapter = createTauriAdapter();
-//let platformAdapter = createWebAdapter();
+// let platformAdapter = createWebAdapter();
 
 
 export default platformAdapter;
 export default platformAdapter;

+ 3 - 3
src/utils/webAdapter.ts

@@ -3,7 +3,7 @@ import type { BasePlatformAdapter } from "@/types/platform";
 export interface WebPlatformAdapter extends BasePlatformAdapter {
 export interface WebPlatformAdapter extends BasePlatformAdapter {
   // Add web-specific methods here
   // Add web-specific methods here
   openFileDialog: (options: any) => Promise<string | string[] | null>;
   openFileDialog: (options: any) => Promise<string | string[] | null>;
-  metadata: (path: string) => Promise<Record<string, any>>;
+  metadata: (path: string, options: any) => Promise<Record<string, any>>;
 }
 }
 
 
 // Create Web adapter functions
 // Create Web adapter functions
@@ -176,8 +176,8 @@ export const createWebAdapter = (): WebPlatformAdapter => {
       return Promise.resolve();
       return Promise.resolve();
     },
     },
 
 
-    async metadata(path) {
-      console.log("metadata is not supported in web environment", path);
+    async metadata(path, options = {}) {
+      console.log("metadata is not supported in web environment", path, options);
       return Promise.resolve({});
       return Promise.resolve({});
     },
     },
   };
   };

+ 2 - 1
src/utils/wrappers/tauriWrappers.ts

@@ -1,3 +1,5 @@
+import * as commands from '@/commands';
+
 // Window operations
 // Window operations
 export const windowWrapper = {
 export const windowWrapper = {
   async getCurrentWindow() {
   async getCurrentWindow() {
@@ -53,7 +55,6 @@ export const systemWrapper = {
 // Command functions
 // Command functions
 export const commandWrapper = {
 export const commandWrapper = {
   async commands<T>(commandName: string, ...args: any[]): Promise<T> {
   async commands<T>(commandName: string, ...args: any[]): Promise<T> {
-    const commands = await import('@/commands');
     if (commandName in commands) {
     if (commandName in commands) {
       // console.log(`Command ${commandName} found`);
       // console.log(`Command ${commandName} found`);
       return (commands as any)[commandName](...args);
       return (commands as any)[commandName](...args);

+ 1 - 1
tsup.config.ts

@@ -67,7 +67,7 @@ export default defineConfig({
 
 
     const packageJson = {
     const packageJson = {
       name: "@infinilabs/search-chat",
       name: "@infinilabs/search-chat",
-      version: "1.1.3",
+      version: "1.1.5",
       main: "index.js",
       main: "index.js",
       module: "index.js",
       module: "index.js",
       type: "module",
       type: "module",

+ 41 - 26
vite.config.ts

@@ -1,8 +1,8 @@
 import { defineConfig } from "vite";
 import { defineConfig } from "vite";
 import react from "@vitejs/plugin-react";
 import react from "@vitejs/plugin-react";
-import path from 'path';
+import path from "path";
 import { config } from "dotenv";
 import { config } from "dotenv";
-import packageJson from './package.json';
+import packageJson from "./package.json";
 
 
 config();
 config();
 
 
@@ -12,12 +12,12 @@ const host = process.env.TAURI_DEV_HOST;
 // https://vitejs.dev/config/
 // https://vitejs.dev/config/
 export default defineConfig(async () => ({
 export default defineConfig(async () => ({
   define: {
   define: {
-    'process.env.VERSION': JSON.stringify(packageJson.version),
+    "process.env.VERSION": JSON.stringify(packageJson.version),
   },
   },
   plugins: [react()],
   plugins: [react()],
   resolve: {
   resolve: {
     alias: {
     alias: {
-      '@': path.resolve(__dirname, './src'),
+      "@": path.resolve(__dirname, "./src"),
     },
     },
   },
   },
   // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
   // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
@@ -31,10 +31,10 @@ export default defineConfig(async () => ({
     host: host || false,
     host: host || false,
     hmr: host
     hmr: host
       ? {
       ? {
-        protocol: "ws",
-        host,
-        port: 1421,
-      }
+          protocol: "ws",
+          host,
+          port: 1421,
+        }
       : undefined,
       : undefined,
     watch: {
     watch: {
       // 3. tell vite to ignore watching `src-tauri`
       // 3. tell vite to ignore watching `src-tauri`
@@ -61,31 +61,46 @@ export default defineConfig(async () => ({
         changeOrigin: true,
         changeOrigin: true,
         secure: false,
         secure: false,
       },
       },
+      "/assistant": {
+        target: process.env.COCO_SERVER_URL,
+        changeOrigin: true,
+        secure: false,
+      },
+      "/datasource": {
+        target: process.env.COCO_SERVER_URL,
+        changeOrigin: true,
+        secure: false,
+      },
+      "/settings": {
+        target: process.env.COCO_SERVER_URL,
+        changeOrigin: true,
+        secure: false,
+      },
     },
     },
   },
   },
   build: {
   build: {
     rollupOptions: {
     rollupOptions: {
       output: {
       output: {
         manualChunks: {
         manualChunks: {
-          vendor: ['react', 'react-dom'],
-          katex: ['rehype-katex'],
-          highlight: ['rehype-highlight'],
-          mermaid: ['mermaid'],
-          'tauri-api': [
-            '@tauri-apps/api/core',
-            '@tauri-apps/api/event',
-            '@tauri-apps/api/window',
-            '@tauri-apps/api/dpi',
-            '@tauri-apps/api/webviewWindow'
+          vendor: ["react", "react-dom"],
+          katex: ["rehype-katex"],
+          highlight: ["rehype-highlight"],
+          mermaid: ["mermaid"],
+          "tauri-api": [
+            "@tauri-apps/api/core",
+            "@tauri-apps/api/event",
+            "@tauri-apps/api/window",
+            "@tauri-apps/api/dpi",
+            "@tauri-apps/api/webviewWindow",
           ],
           ],
-          'tauri-plugins': [
-            '@tauri-apps/plugin-dialog',
-            '@tauri-apps/plugin-process',
-            'tauri-plugin-fs-pro-api',
-            'tauri-plugin-macos-permissions-api',
-            'tauri-plugin-screenshots-api',
-          ]
-        }
+          "tauri-plugins": [
+            "@tauri-apps/plugin-dialog",
+            "@tauri-apps/plugin-process",
+            "tauri-plugin-fs-pro-api",
+            "tauri-plugin-macos-permissions-api",
+            "tauri-plugin-screenshots-api",
+          ],
+        },
       },
       },
     },
     },
     chunkSizeWarningLimit: 600,
     chunkSizeWarningLimit: 600,

部分文件因为文件数量过多而无法显示