25 Commit-ok 95e7a37355 ... f770bce114

Szerző SHA1 Üzenet Dátum
  yuanfeijie f770bce114 Merge remote-tracking branch 'origin/main' 1 hónapja
  ayangweb 80ac8baca5 feat: add chat mode launch page #424 1 hónapja
  ayangweb bde658b981 feat: add chat mode launch page (#424) 1 hónapja
  BiggerRain 4380b56a30 fix: query_coco_fusion params error (#425) 1 hónapja
  ayangweb 54364565e2 feat: right-click menu support for search (#423) 1 hónapja
  BiggerRain ee4a06b6de feat: web components assistant (#422) 1 hónapja
  ayangweb 9715a92f36 refactor: change the selected background color of the item (#421) 1 hónapja
  BiggerRain 2caeb4090a docs: update README (#418) 1 hónapja
  ayangweb 983e65ee61 refactor: right-click menu returns to execute whichever one is selected (#417) 1 hónapja
  BiggerRain ec37cfe68f fix: current conversation tip (#416) 1 hónapja
  BiggerRain db66d81bd0 fix: fixed several search & chat bugs (#412) 1 hónapja
  ayangweb 5b0fdbcb2c feat: support esc to exit right-click menu (#415) 1 hónapja
  ayangweb 88955e0b95 feat: ai assistant supports shortcuts (#414) 1 hónapja
  SteveLauC aee7df608f refactor: use timeout value specified in settings in query_coco_fusion() (#413) 1 hónapja
  SteveLauC 6d8fa81141 revert: Document constructor changed in #399 (#410) 1 hónapja
  ayangweb d67d6645fe feat: automatically selects the first entry after searching (#411) 1 hónapja
  ayangweb 6329354243 feat: add keyboard event handling and double-click copying (#409) 1 hónapja
  ayangweb 3ef5226e11 refactor: add empty data prompt to search scope (#406) 1 hónapja
  ayangweb eebf49d7e0 refactor: optimize the style of the calculator (#405) 1 hónapja
  BiggerRain 04903a09cd build: build web components and publish (#404) 1 hónapja
  ayangweb 44b5f8400e feat: added support for the calculator function (#399) 1 hónapja
  ayangweb 77e6b58381 refactor: show placeholder image when history is empty (#398) 1 hónapja
  BiggerRain f6e5e826fd chore: update assistant icon & think mode (#397) 1 hónapja
  SteveLauC 886400bcbc fix: correct datasource ID in returned documents (#396) 1 hónapja
  BiggerRain 53258ee834 feat: add support for switching AI assistants (#395) 1 hónapja
75 módosított fájl, 2561 hozzáadás és 1361 törlés
  1. 2 2
      .env
  2. 59 26
      README.md
  3. 10 1
      docs/content.en/docs/release-notes/_index.md
  4. BIN
      public/assets/calculator.png
  5. 0 0
      public/assets/fonts/icons/iconfont.js
  6. BIN
      public/assets/no_data_dark.png
  7. BIN
      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
 
+<div align="center">
+
 **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,
 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
@@ -12,20 +20,15 @@ and internal resources. Coco enhances collaboration by making information instan
 insights based on your enterprise's specific data.
 
 > **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,
   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
   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
-cd coco-app
+# Install pnpm
 npm install -g pnpm
+
+# Install dependencies
 pnpm install
+
+# Start development server
 pnpm tauri dev
 ```
 
-#### Desktop Development:
-
-To start desktop development, run:
+### Production Build
 
 ```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.
+
+---
+
+<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 keyboard-only operation to history list #385
 - 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
 
 - fix: fixed the problem of not being able to search in secondary directories #338
 - fix: active shadow setting #354
 - 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:fixed several search & chat bugs #412
 
 ### Improvements
 
@@ -41,6 +48,8 @@ Information about release notes of Coco Server is provided here.
 - style: modify the style #370
 - style: search list details display #378
 - 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)
 

BIN
public/assets/calculator.png


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 0
public/assets/fonts/icons/iconfont.js


BIN
public/assets/no_data_dark.png


BIN
public/assets/no_data_light.png


+ 73 - 1
src-tauri/Cargo.lock

@@ -416,7 +416,7 @@ dependencies = [
  "anyhow",
  "arrayvec",
  "log",
- "nom",
+ "nom 7.1.3",
  "num-rational",
  "v_frame",
 ]
@@ -723,6 +723,24 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 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]]
 name = "chrono"
 version = "0.4.40"
@@ -770,6 +788,7 @@ dependencies = [
  "applications",
  "async-trait",
  "base64 0.13.1",
+ "chinese-number",
  "dirs 5.0.1",
  "env_logger",
  "futures",
@@ -781,7 +800,9 @@ dependencies = [
  "hyper 0.14.32",
  "lazy_static",
  "log",
+ "meval",
  "notify",
+ "num2words",
  "once_cell",
  "ordered-float",
  "pizza-common",
@@ -1478,6 +1499,26 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 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]]
 name = "enumflags2"
 version = "0.7.11"
@@ -3283,6 +3324,16 @@ dependencies = [
  "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]]
 name = "mime"
 version = "0.3.17"
@@ -3437,6 +3488,12 @@ version = "0.1.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
 
+[[package]]
+name = "nom"
+version = "1.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce"
+
 [[package]]
 name = "nom"
 version = "7.1.3"
@@ -3471,6 +3528,12 @@ dependencies = [
  "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]]
 name = "num-bigint"
 version = "0.4.6"
@@ -3538,6 +3601,15 @@ dependencies = [
  "autocfg",
 ]
 
+[[package]]
+name = "num2words"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ab78b987a0e1e6cf869a443f7f1c9dc696117d47780227e961e742d0b45d706"
+dependencies = [
+ "num-bigfloat",
+]
+
 [[package]]
 name = "num_enum"
 version = "0.7.3"

+ 3 - 0
src-tauri/Cargo.toml

@@ -74,6 +74,9 @@ tungstenite = "0.24.0"
 env_logger = "0.11.5"
 tokio-util = "0.7.14"
 tauri-plugin-windows-version = "2"
+meval = "0.2"
+chinese-number = "0.7"
+num2words = "1"
 
 [target."cfg(target_os = \"macos\")".dependencies]
 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)
         })?;
 
-
     common::http::get_response_body_text(response).await
 }
 
@@ -135,17 +134,18 @@ pub async fn new_chat<R: Runtime>(
     let mut headers = HashMap::new();
     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
         .text()
         .await
         .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" {
         return Err(format!("Unexpected result: {}", chat_response.result));
@@ -179,8 +179,8 @@ pub async fn send_message<R: Runtime>(
         query_params,
         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
 }
@@ -222,8 +222,20 @@ pub async fn update_session_chat(
         None,
         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())
 }
+
+#[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>,
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
 pub struct Document {
     pub id: String,
     pub created: Option<String>,
@@ -55,8 +55,15 @@ pub struct Document {
     pub owner: Option<UserInfo>,
     pub last_updated_by: Option<EditorInfo>,
 }
+
 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 {
             id,
             created: None,

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

@@ -50,7 +50,7 @@ where
 {
     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)
         .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::delete_session_chat,
             assistant::update_session_chat,
+            assistant::assistant_search,
             // server::get_coco_server_datasources,
             // server::get_coco_server_connectors,
             server::websocket::connect_to_server,
@@ -136,7 +137,8 @@ pub fn run() {
             server::transcription::transcription,
             local::application::get_default_search_paths,
             local::application::list_app_with_metadata_in,
-            util::open
+            util::open,
+            server::system_settings::get_system_settings
         ])
         .setup(|app| {
             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> {
     let application_search =
         local::application::ApplicationSearchSource::new(app_handle.clone(), 1000f64).await?;
+    let calculator_search = local::calculator::CalculatorSource::new(2000f64);
 
     // Register the application search source
     let registry = app_handle.state::<SearchSourceRegistry>();
     registry.register_source(application_search).await;
+    registry.register_source(calculator_search).await;
 
     Ok(())
 }

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

@@ -11,6 +11,8 @@ use std::path::PathBuf;
 use tauri::{AppHandle, Runtime};
 use tauri_plugin_fs_pro::{icon, metadata, name, IconOptions};
 
+const DATA_SOURCE_ID: &str = "Applications";
+
 #[tauri::command]
 pub fn get_default_search_paths() -> Vec<String> {
     #[cfg(target_os = "macos")]
@@ -232,7 +234,7 @@ impl SearchSource for ApplicationSearchSource {
                 .unwrap_or("My Computer".into())
                 .to_string_lossy()
                 .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();
 
                 total_hits += 1;
+
                 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
                 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 calculator;
 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,
     size: u64,
     query_strings: HashMap<String, String>,
+    query_timeout: u64,
 ) -> Result<MultiSourceQueryResponse, SearchError> {
+    let query_source_to_search = query_strings.get("querysource");
+
     let search_sources = app_handle.state::<SearchSourceRegistry>();
 
     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;
 
     // 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
     for query_source in sources_list {
         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);
 
         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 {
                 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 search;
 pub mod servers;
+pub mod system_settings;
 pub mod transcription;
 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 { useAppStore } from '@/stores/appStore';
+import { useAppStore } from "@/stores/appStore";
 
 import {
   handleChangeRequestHeader,
@@ -44,21 +44,22 @@ axios.interceptors.response.use(
 
 export const handleApiError = (error: any) => {
   const addError = useAppStore.getState().addError;
-  
-  let message = 'Request failed';
-  
+
+  let message = "Request failed";
+
   if (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) {
     // Request failed to send
-    message = 'Network connection failed';
+    message = "Network connection failed";
   } else {
     // Other errors
     message = error.message;
   }
-  
-  addError(message, 'error');
+
+  addError(message, "error");
   return error;
 };
 

+ 34 - 15
src/commands/servers.ts

@@ -15,7 +15,7 @@ import {
   TranscriptionResponse,
   MultiSourceQueryResponse,
 } from "@/types/commands";
-import { useAppStore } from '@/stores/appStore';
+import { useAppStore } from "@/stores/appStore";
 
 async function invokeWithErrorHandler<T>(
   command: string,
@@ -26,27 +26,27 @@ async function invokeWithErrorHandler<T>(
     const result = await invoke<T>(command, args);
     // 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;
       if (failedResult.failed?.length > 0) {
         failedResult.failed.forEach((error: any) => {
           // 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);
-      if (typeof res === 'string') {
+      if (typeof res === "string") {
         throw new Error(result);
       }
     }
 
     return result;
   } catch (error: any) {
-    const errorMessage = error || 'Command execution failed';
-    addError(errorMessage, 'error');
+    const errorMessage = error || "Command execution failed";
+    addError(command + ":" + errorMessage, "error");
     throw error;
   }
 }
@@ -234,7 +234,10 @@ export function send_message({
 }
 
 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: {
@@ -248,10 +251,19 @@ export const 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) => {
-  const response = await invokeWithErrorHandler<UploadAttachmentResponse>("upload_attachment", {
-    ...payload,
-  });
+  const response = await invokeWithErrorHandler<UploadAttachmentResponse>(
+    "upload_attachment",
+    {
+      ...payload,
+    }
+  );
 
   if (response?.acknowledged) {
     return response.attachments;
@@ -259,7 +271,9 @@ export const upload_attachment = async (payload: UploadAttachmentPayload) => {
 };
 
 export const get_attachment = (payload: GetAttachmentPayload) => {
-  return invokeWithErrorHandler<GetAttachmentResponse>("get_attachment", { ...payload });
+  return invokeWithErrorHandler<GetAttachmentResponse>("get_attachment", {
+    ...payload,
+  });
 };
 
 export const delete_attachment = (payload: DeleteAttachmentPayload) => {
@@ -267,13 +281,18 @@ export const delete_attachment = (payload: DeleteAttachmentPayload) => {
 };
 
 export const transcription = (payload: TranscriptionPayload) => {
-  return invokeWithErrorHandler<TranscriptionResponse>("transcription", { ...payload });
+  return invokeWithErrorHandler<TranscriptionResponse>("transcription", {
+    ...payload,
+  });
 };
 
 export const query_coco_fusion = (payload: {
   from: 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";
 
 interface ChatAIProps {
-  isTransitioned: boolean;
   isSearchActive?: boolean;
   isDeepThinkActive?: boolean;
   activeChatProp?: Chat;
@@ -36,6 +35,7 @@ interface ChatAIProps {
   isChatPage?: boolean;
   getFileUrl: (path: string) => string;
   showChatHistory?: boolean;
+  assistantIDs?: string[];
 }
 
 export interface ChatAIRef {
@@ -49,7 +49,6 @@ const ChatAI = memo(
   forwardRef<ChatAIRef, ChatAIProps>(
     (
       {
-        isTransitioned,
         changeInput,
         isSearchActive,
         isDeepThinkActive,
@@ -60,11 +59,10 @@ const ChatAI = memo(
         isChatPage = false,
         getFileUrl,
         showChatHistory,
+        assistantIDs,
       },
       ref
     ) => {
-      if (!isTransitioned) return null;
-
       useImperativeHandle(ref, () => ({
         init: init,
         cancelChat: () => cancelChat(activeChat),
@@ -76,6 +74,9 @@ const ChatAI = memo(
         useChatStore();
 
       const currentService = useConnectStore((state) => state.currentService);
+      const visibleStartPage = useConnectStore((state) => {
+        return state.visibleStartPage;
+      });
 
       const addError = useAppStore.getState().addError;
 
@@ -201,10 +202,14 @@ const ChatAI = memo(
         async (value: string) => {
           try {
             console.log("init", isLogin, curChatEnd, activeChat?._id);
-            if (!isLogin || !curChatEnd) {
+            if (!isLogin) {
               addError("Please login to continue chatting");
               return;
             }
+            if (!curChatEnd) {
+              addError("Please wait for the current conversation to complete");
+              return;
+            }
             setShowPrevSuggestion(false);
             if (!activeChat?._id) {
               await createNewChat(value, activeChat, websocketSessionId);
@@ -307,20 +312,6 @@ const ChatAI = memo(
         };
       }, [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(() => {
         setIsSidebarOpenChat(!isSidebarOpenChat);
         setIsSidebarOpen && setIsSidebarOpen(!isSidebarOpenChat);
@@ -388,8 +379,9 @@ const ChatAI = memo(
             reconnect={reconnect}
             isChatPage={isChatPage}
             isLogin={isLogin}
-            setIsLogin={setIsLoginChat}
+            setIsLogin={setIsLogin}
             showChatHistory={showChatHistory}
+            assistantIDs={assistantIDs}
           />
           {isLogin ? (
             <ChatContent
@@ -413,7 +405,9 @@ const ChatAI = memo(
             <ConnectPrompt />
           )}
 
-          {showPrevSuggestion ? <PrevSuggestion sendMessage={init} /> : null}
+          {showPrevSuggestion && !visibleStartPage && (
+            <PrevSuggestion sendMessage={init} />
+          )}
         </div>
       );
     }

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

@@ -10,6 +10,7 @@ import type { Chat, IChunkData } from "./types";
 // import SessionFile from "./SessionFile";
 import { useConnectStore } from "@/stores/connectStore";
 import SessionFile from "./SessionFile";
+import Splash from "./Splash";
 
 interface ChatContentProps {
   activeChat?: Chat;
@@ -145,6 +146,8 @@ export const ChatContent = ({
       )}
 
       {sessionId && <SessionFile sessionId={sessionId} />}
+
+      <Splash />
     </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 { useKeyPress } from "ahooks";
 
-import logoImg from "@/assets/icon.svg";
 import HistoryIcon from "@/icons/History";
 import PinOffIcon from "@/icons/PinOff";
 import PinIcon from "@/icons/Pin";
-import ServerIcon from "@/icons/Server";
 import WindowsFullIcon from "@/icons/WindowsFull";
 import { useAppStore, IServer } from "@/stores/appStore";
-import { useChatStore } from "@/stores/chatStore";
 import type { Chat } from "./types";
-import { useConnectStore } from "@/stores/connectStore";
 import platformAdapter from "@/utils/platformAdapter";
 import VisibleKey from "../Common/VisibleKey";
 import { useShortcutsStore } from "@/stores/shortcutsStore";
 import { HISTORY_PANEL_ID } from "@/constants";
+import { AssistantList } from "./AssistantList";
+import { ServerList } from "./ServerList";
 
 interface ChatHeaderProps {
   onCreateNewChat: () => void;
@@ -46,6 +25,7 @@ interface ChatHeaderProps {
   setIsLogin: (isLogin: boolean) => void;
   isChatPage?: boolean;
   showChatHistory?: boolean;
+  assistantIDs?: string[];
 }
 
 export function ChatHeader({
@@ -59,21 +39,11 @@ export function ChatHeader({
   setIsLogin,
   isChatPage = false,
   showChatHistory = true,
+  assistantIDs,
 }: ChatHeaderProps) {
-  const { t } = useTranslation();
-
-  const setEndpoint = useAppStore((state) => state.setEndpoint);
   const isPinned = useAppStore((state) => state.isPinned);
   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 historicalRecords = useShortcutsStore((state) => {
     return state.historicalRecords;
@@ -84,79 +54,8 @@ export function ChatHeader({
   const fixedWindow = useShortcutsStore((state) => {
     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 () => {
     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 (
     <header
       className="flex items-center justify-between py-2 px-3"
@@ -225,38 +93,7 @@ export function ChatHeader({
           </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 ? (
           <button
@@ -291,117 +128,12 @@ export function ChatHeader({
             </VisibleKey>
           </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 : (
             <button className="inline-flex" onClick={onOpenChatAI}>

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

@@ -1,9 +1,11 @@
 import { useTranslation } from "react-i18next";
 
 import { ChatMessage } from "@/components/ChatMessage";
+import { useConnectStore } from "@/stores/connectStore";
 
 export const Greetings = () => {
   const { t } = useTranslation();
+  const currentAssistant = useConnectStore((state) => state.currentAssistant);
 
   return (
     <ChatMessage
@@ -12,7 +14,9 @@ export const Greetings = () => {
         _id: "greetings",
         _source: {
           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>
       {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="mb-4 space-y-3 text-xs">
               {Data?.map((item) => (

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

@@ -103,7 +103,7 @@ export const PickSource = ({
         )}
       </button>
       {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="mb-4 space-y-3 text-xs">
               {Data?.map((item) => (

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

@@ -1,8 +1,7 @@
 import { MoveRight } from "lucide-react";
 import { FC, useEffect, useState } from "react";
 
-import { Get } from "@/api/axiosRequest";
-import { useAppStore } from "@/stores/appStore";
+import { useConnectStore } from "@/stores/connectStore";
 
 interface PrevSuggestionProps {
   sendMessage: (message: string) => void;
@@ -11,35 +10,18 @@ interface PrevSuggestionProps {
 const PrevSuggestion: FC<PrevSuggestionProps> = (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[]>([]);
 
   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 (
     <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>
       {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="mb-4 space-y-2 text-xs">
               {Data?.keyword ? (

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

@@ -12,6 +12,9 @@ import { MessageActions } from "./MessageActions";
 import Markdown from "./Markdown";
 import { SuggestionList } from "./SuggestionList";
 import { UserMessage } from "./UserMessage";
+import { useConnectStore } from "@/stores/connectStore";
+import FontIcon from "@/components/Common/Icons/FontIcon";
+import clsx from "clsx";
 
 interface ChatMessageProps {
   message: Message;
@@ -40,6 +43,8 @@ export const ChatMessage = memo(function ChatMessage({
 }: ChatMessageProps) {
   const { t } = useTranslation();
 
+  const currentAssistant = useConnectStore((state) => state.currentAssistant);
+
   const isAssistant = message?._source?.type === "assistant";
   const messageContent = message?._source?.message || "";
   const details = message?._source?.details || [];
@@ -49,6 +54,7 @@ export const ChatMessage = memo(function ChatMessage({
     isTyping === false && (messageContent || response?.message_chunk);
 
   const [suggestion, setSuggestion] = useState<string[]>([]);
+  const visibleStartPage = useConnectStore((state) => state.visibleStartPage);
 
   const getSuggestion = (suggestion: string[]) => {
     setSuggestion(suggestion);
@@ -117,7 +123,13 @@ export const ChatMessage = memo(function ChatMessage({
 
   return (
     <div
-      className={`py-8 flex ${isAssistant ? "justify-start" : "justify-end"}`}
+      className={clsx(
+        "py-8 flex",
+        [isAssistant ? "justify-start" : "justify-end"],
+        {
+          hidden: visibleStartPage,
+        }
+      )}
     >
       <div
         className={`px-4 flex gap-4 ${
@@ -125,18 +137,29 @@ export const ChatMessage = memo(function ChatMessage({
         }`}
       >
         <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 ? (
-              <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}
-            {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 pl-7 text-[#333] dark:text-[#d8d8d8] leading-relaxed">
               {renderContent()}

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

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

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

@@ -27,7 +27,7 @@ export const Sidebar = forwardRef<{ refreshData: () => void }, SidebarProps>(
       return (
         <div
           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
               ? "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"
@@ -37,9 +37,9 @@ export const Sidebar = forwardRef<{ refreshData: () => void }, SidebarProps>(
           <img
             src={item?.provider?.icon || cocoLogoImg}
             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" />
           <button className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300">
             {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 { Chat } from "@/components/Assistant/types";
+import NoDataImage from "../NoDataImage";
 
 dayjs.extend(isSameOrAfter);
 
@@ -106,7 +107,7 @@ const HistoryList: FC<HistoryListProps> = (props) => {
   ];
 
   const debouncedSearch = useMemo(() => {
-    return debounce((value: string) => onSearch(value), 500);
+    return debounce((value: string) => onSearch(value), 300);
   }, [onSearch]);
 
   useKeyPress(["uparrow", "downarrow"], (_, key) => {
@@ -158,7 +159,7 @@ const HistoryList: FC<HistoryListProps> = (props) => {
       ref={listRef}
       id={id}
       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">
@@ -197,194 +198,202 @@ const HistoryList: FC<HistoryListProps> = (props) => {
         </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);
-                            }}
+                            }
+
+                            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
-                                      shortcut={shortcut}
-                                      onKeyPress={onClick}
+                                      shortcut="O"
+                                      onKeyPress={() => {
+                                        moreButtonRef.current?.click();
+                                      }}
                                     >
-                                      <Icon
-                                        className="size-4"
-                                        style={{
-                                          color: iconColor,
-                                        }}
-                                      />
+                                      <Ellipsis className="size-4 text-[#979797]" />
                                     </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>
-          </DialogPanel>
+          </Dialog>
+        </>
+      ) : (
+        <div className="flex items-center justify-center flex-1">
+          <NoDataImage />
         </div>
-      </Dialog>
+      )}
     </div>
   );
 };

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

@@ -20,6 +20,16 @@ function TypeIcon({
   const endpoint_http = useAppStore((state) => state.endpoint_http);
   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.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 { 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 { useOSKeyPress } from "@/hooks/useOSKeyPress";
 import { useSearchStore } from "@/stores/searchStore";
 import { copyToClipboard, OpenURLWithBrowser } from "@/utils";
 import { isMac } from "@/utils/platform";
+import { CONTEXT_MENU_PANEL_ID } from "@/constants";
+import { useShortcutsStore } from "@/stores/shortcutsStore";
+import { Input } from "@headlessui/react";
 
 interface State {
   activeMenuIndex: number;
@@ -25,55 +23,101 @@ interface ContextMenuProps {
 
 const ContextMenu = ({ hideCoco }: ContextMenuProps) => {
   const containerRef = useRef<HTMLDivElement>(null);
-
-  const { t } = useTranslation();
-
+  const { t, i18n } = useTranslation();
+  const state = useReactive<State>({
+    activeMenuIndex: 0,
+  });
   const visibleContextMenu = useSearchStore((state) => {
     return state.visibleContextMenu;
   });
-
   const setVisibleContextMenu = useSearchStore((state) => {
     return state.setVisibleContextMenu;
   });
-
+  const setOpenPopover = useShortcutsStore((state) => state.setOpenPopover);
   const selectedSearchContent = useSearchStore((state) => {
     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(() => {
     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 />,
         keys: isMac ? ["↩︎"] : ["Enter"],
         shortcut: "enter",
+        hide: category === "Calculator",
         clickEvent: () => {
-          OpenURLWithBrowser(selectedSearchContent?.url);
-
-          setVisibleContextMenu(false);
+          OpenURLWithBrowser(url);
 
           hideCoco && hideCoco();
         },
       },
       {
-        name: "search.contextMenu.copyLink",
+        name: t("search.contextMenu.copyLink"),
         icon: <Link />,
         keys: isMac ? ["⌘", "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]);
 
-  const state = useReactive<State>({
-    activeMenuIndex: 0,
-  });
+  const shortcuts = useCreation(() => {
+    return menus.map((item) => item.shortcut);
+  }, [menus]);
 
   useEffect(() => {
     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 (
     <>
       {visibleContextMenu && (
@@ -138,41 +189,44 @@ const ContextMenu = ({ hideCoco }: ContextMenuProps) => {
 
             setVisibleContextMenu(false);
           }}
-        ></div>
+        />
       )}
 
       <div
         ref={containerRef}
+        id={visibleContextMenu ? CONTEXT_MENU_PANEL_ID : ""}
         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,
           }
         )}
       >
-        <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;
 
             return (
               <li
                 key={name}
                 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,
                   }
                 )}
                 onMouseEnter={() => {
                   state.activeMenuIndex = index;
                 }}
-                onClick={clickEvent}
+                onClick={() => handleClick(clickEvent)}
               >
                 <div className="flex items-center gap-2 text-black/80 dark:text-white/80">
                   {cloneElement(icon, { className: "size-4" })}
 
-                  <span>{t(name)}</span>
+                  <span>{name}</span>
                 </div>
 
                 <div className="flex gap-[4px] text-black/60 dark:text-white/60">
@@ -180,7 +234,7 @@ const ContextMenu = ({ hideCoco }: ContextMenuProps) => {
                     <kbd
                       key={key}
                       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,
                         }
@@ -194,6 +248,25 @@ const ContextMenu = ({ hideCoco }: ContextMenuProps) => {
             );
           })}
         </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>
     </>
   );

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

@@ -5,6 +5,7 @@ import { formatter } from "@/utils/index";
 import TypeIcon from "@/components/Common/Icons/TypeIcon";
 import defaultThumbnail from "@/assets/coconut-tree.png";
 import ItemIcon from "@/components/Common/Icons/ItemIcon";
+import { RichCategories } from "./ListRight";
 
 interface DocumentDetailProps {
   document: any;
@@ -19,10 +20,13 @@ interface DetailItemProps {
 
 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="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}
-      {value}
+      <div className="truncate">{value}</div>
     </div>
   </div>
 );
@@ -36,6 +40,52 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({ document }) => {
     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 (
     <div className="p-3">
       {/* <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" />}
         />
 
+        {/* 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 && (
           <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 SearchListItem from "./SearchListItem";
 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 {
   onSelectDocument: (id: string) => void;
-  getDocDetail: (detail: any) => void;
+  getDocDetail: (detail: Record<string, any>) => void;
   input: string;
   isChatMode: boolean;
   selectedId?: string;
   viewMode: "detail" | "list";
   setViewMode: (mode: "detail" | "list") => void;
-  queryDocuments: (
-    from: number,
-    size: number,
-    queryStrings: any
-  ) => Promise<any>;
 }
 
 const PAGE_SIZE = 20;
@@ -32,10 +31,11 @@ export const DocumentList: React.FC<DocumentListProps> = ({
   isChatMode,
   viewMode,
   setViewMode,
-  queryDocuments,
 }) => {
   const { t } = useTranslation();
   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 [total, setTotal] = useState(0);
@@ -49,6 +49,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
       let queryStrings: any = {
         query: input,
         datasource: sourceData?.source?.id,
+        querysource: sourceData?.querySource?.id,
       };
 
       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,
@@ -110,23 +138,18 @@ export const DocumentList: React.FC<DocumentListProps> = ({
     (e: KeyboardEvent) => {
       if (!data?.list?.length) return;
 
-      if (e.key === "ArrowUp" || e.key === "ArrowDown") {
+      const handleArrowKeys = () => {
         e.preventDefault();
         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) => {
-          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);
           itemRefs.current[nextIndex]?.scrollIntoView({
             behavior: "smooth",
@@ -134,11 +157,25 @@ export const DocumentList: React.FC<DocumentListProps> = ({
           });
           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]
@@ -174,7 +211,10 @@ export const DocumentList: React.FC<DocumentListProps> = ({
         />
       </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 && (
           <div>
             {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 { isNil } from "lodash-es";
-import { useUnmount } from "ahooks";
+import { useDebounceFn, useUnmount } from "ahooks";
+import { useTranslation } from "react-i18next";
 
 import { useSearchStore } from "@/stores/searchStore";
 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 SearchListItem from "./SearchListItem";
 import { metaOrCtrlKey, isMetaOrCtrlKey } from "@/utils/keyboardUtils";
-import { OpenURLWithBrowser } from "@/utils/index";
+import { copyToClipboard, OpenURLWithBrowser } from "@/utils/index";
 import VisibleKey from "@/components/Common/VisibleKey";
+import Calculator from "./Calculator";
+import { useShortcutsStore } from "@/stores/shortcutsStore";
 
 type ISearchData = Record<string, any[]>;
 
@@ -28,12 +31,12 @@ function DropdownList({
   IsError,
   isChatMode,
 }: DropdownListProps) {
+  const { t } = useTranslation();
+
   let globalIndex = 0;
   const globalItemIndexMap: any[] = [];
 
-  const setSourceData = useSearchStore(
-    (state: { setSourceData: any }) => state.setSourceData
-  );
+  const setSourceData = useSearchStore((state) => state.setSourceData);
 
   const [showError, setShowError] = useState<boolean>(IsError);
   const [selectedItem, setSelectedItem] = useState<number | null>(null);
@@ -42,11 +45,18 @@ function DropdownList({
   const containerRef = 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(() => {
+    setSelectedItem(null);
     setSelectedSearchContent(void 0);
   });
 
@@ -66,15 +76,19 @@ function DropdownList({
     }
   }, [isChatMode]);
 
+  const { run } = useDebounceFn(() => setSelectedItem(0), { wait: 200 });
+
+  useEffect(() => {
+    setSelectedItem(null);
+
+    run();
+  }, [SearchData]);
+
+  const openPopover = useShortcutsStore((state) => state.openPopover);
+
   const handleKeyDown = useCallback(
     (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") {
         e.preventDefault();
@@ -100,7 +114,11 @@ function DropdownList({
 
       if (e.key === "ArrowRight" && selectedItem !== null) {
         e.preventDefault();
+
         const item = globalItemIndexMap[selectedItem];
+
+        if (hideArrowRight(item)) return;
+
         goToTwoPage(item);
       }
 
@@ -109,6 +127,8 @@ function DropdownList({
         const item = globalItemIndexMap[selectedItem];
         if (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) => {
@@ -161,6 +181,16 @@ function DropdownList({
     setSourceData(item);
   }
 
+  const setVisibleContextMenu = useSearchStore(
+    (state) => state.setVisibleContextMenu
+  );
+
+  const handleContextMenu = (event: MouseEvent) => {
+    event.preventDefault();
+
+    setVisibleContextMenu(true);
+  };
+
   return (
     <div
       ref={containerRef}
@@ -171,8 +201,7 @@ function DropdownList({
       {showError ? (
         <div className="flex items-center gap-2 text-sm text-[#333] p-2">
           <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" />
           <X
             className="text-[#666] w-[16px] h-[16px] cursor-pointer"
@@ -180,56 +209,78 @@ function DropdownList({
           />
         </div>
       ) : 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>
-              ) : 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>
   );
 }

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

@@ -15,7 +15,7 @@ import SearchPopover from "./SearchPopover";
 // import AudioRecording from "../AudioRecording";
 import { DataSource } from "@/types/commands";
 // import InputExtra from "./InputExtra";
-// import { useConnectStore } from "@/stores/connectStore";
+import { useConnectStore } from "@/stores/connectStore";
 import { useShortcutsStore } from "@/stores/shortcutsStore";
 import Copyright from "@/components/Common/Copyright";
 import VisibleKey from "@/components/Common/VisibleKey";
@@ -55,7 +55,6 @@ interface ChatInputProps {
   getFileMetadata: (path: string) => Promise<any>;
   getFileIcon: (path: string, size: number) => Promise<string>;
   hideCoco?: () => void;
-  hasFeature?: string[];
   hasModules?: string[];
   searchPlaceholder?: string;
   chatPlaceholder?: string;
@@ -77,7 +76,6 @@ export default function ChatInput({
   isChatPage = false,
   getDataSourcesByServer,
   setupWindowFocusListener,
-  hasFeature = ["think", "search", "think_icon", "search_icon"],
   hideCoco,
   hasModules = [],
   searchPlaceholder,
@@ -85,24 +83,17 @@ export default function ChatInput({
 }: ChatInputProps) {
   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 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 modifierKey = useShortcutsStore((state) => {
-    return state.modifierKey;
-  });
-
+  const modifierKey = useShortcutsStore((state) => state.modifierKey);
   const modeSwitch = useShortcutsStore((state) => state.modeSwitch);
   const returnToInput = useShortcutsStore((state) => state.returnToInput);
   const deepThinking = useShortcutsStore((state) => state.deepThinking);
@@ -140,6 +131,9 @@ export default function ChatInput({
   const setModifierKeyPressed = useShortcutsStore((state) => {
     return state.setModifierKeyPressed;
   });
+  const setVisibleStartPage = useConnectStore((state) => {
+    return state.setVisibleStartPage;
+  });
 
   useEffect(() => {
     const handleFocus = () => {
@@ -163,6 +157,8 @@ export default function ChatInput({
   }, [isChatMode, textareaRef, inputRef]);
 
   const handleSubmit = useCallback(() => {
+    setVisibleStartPage(false);
+
     const trimmedValue = inputValue.trim();
     console.log("handleSubmit", trimmedValue, disabled);
     if (trimmedValue && !disabled) {
@@ -183,13 +179,23 @@ export default function ChatInput({
 
   useKeyPress(`${modifierKey}.${returnToInput}`, handleToggleFocus);
 
+  const visibleContextMenu = useSearchStore((state) => {
+    return state.visibleContextMenu;
+  });
+  const setVisibleContextMenu = useSearchStore((state) => {
+    return state.setVisibleContextMenu;
+  });
+
   const handleKeyDown = useCallback(
     (e: KeyboardEvent) => {
       // console.log("handleKeyDown", e.code, e.key);
 
       if (e.key === "Escape") {
-        handleEscapeKey();
-        return;
+        if (visibleContextMenu) {
+          return setVisibleContextMenu(false);
+        }
+
+        return handleEscapeKey();
       }
 
       pressedKeys.add(e.key);
@@ -239,6 +245,7 @@ export default function ChatInput({
       setIsCommandPressed,
       disabledChange,
       curChatEnd,
+      visibleContextMenu,
     ]
   );
 
@@ -399,7 +406,7 @@ export default function ChatInput({
         )}
 
         {!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")}
             <div
               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
                 className={clsx(
                   "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>
             )}
 
-            {hasFeature.includes("search") && (
+            {currentAssistant?._source?.datasource?.visible && (
               <SearchPopover
                 isSearchActive={isSearchActive}
                 setIsSearchActive={setIsSearchActive}
                 getDataSourcesByServer={getDataSourcesByServer}
               />
             )}
-            {!hasFeature.includes("search") && !hasFeature.includes("think") ? (
+
+            {!currentAssistant?._source?.datasource?.visible &&
+            !currentAssistant?._source?.config?.visible ? (
               <div className="px-[9px]">
                 <Copyright />
               </div>
@@ -498,9 +507,7 @@ export default function ChatInput({
             <ChatSwitch
               isChatMode={isChatMode}
               onChange={(value: boolean) => {
-                value && disabledChange();
                 changeMode && changeMode(value);
-                setSourceData(undefined);
               }}
             />
           </div>

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

@@ -1,33 +1,36 @@
+import clsx from "clsx";
+
 import TypeIcon from "@/components/Common/Icons/TypeIcon";
 import RichIcon from "@/components/Common/Icons/RichIcon";
-import VisibleKey from "../Common/VisibleKey";
-import clsx from "clsx";
+import VisibleKey from "@/components/Common/VisibleKey";
 
 interface ListRightProps {
   item: any;
   isSelected: boolean;
   showIndex: boolean;
   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,
   isSelected,
-  showIndex,
-  currentIndex,
   goToTwoPage,
-}: ListRightProps) {
+}: RichCategoriesProps) {
   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 : (
         <div
           className={`w-4 h-4 cursor-pointer`}
           onClick={(e) => {
             e.stopPropagation();
-            goToTwoPage && goToTwoPage(item);
+            goToTwoPage && goToTwoPage();
           }}
         >
           <TypeIcon
@@ -35,7 +38,7 @@ export default function ListRight({
             className="w-4 h-4 cursor-pointer"
             onClick={(e: React.MouseEvent) => {
               e.stopPropagation();
-              goToTwoPage && goToTwoPage(item);
+              goToTwoPage && goToTwoPage();
             }}
           />
         </div>
@@ -48,7 +51,7 @@ export default function ListRight({
             className={`w-4 h-4 mr-2 cursor-pointer`}
             onClick={(e) => {
               e.stopPropagation();
-              goToTwoPage && goToTwoPage(item);
+              goToTwoPage && goToTwoPage();
             }}
           />
           <span
@@ -98,6 +101,26 @@ export default function ListRight({
             ""}
         </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 && (
         <VisibleKey
@@ -105,9 +128,6 @@ export default function ListRight({
           rootClassName={clsx("!absolute", [
             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
           shortcut={String(currentIndex === 9 ? 0 : currentIndex + 1)}
           rootClassName="!absolute right-2"
-          shortcutClassName={clsx({
-            "!shadow-[-6px_0px_6px_2px_#950599]": isSelected,
-          })}
         />
       )}
     </div>

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

@@ -7,18 +7,30 @@ import { useSearchStore } from "@/stores/searchStore";
 import ContextMenu from "./ContextMenu";
 import { NoResults } from "@/components/Common/UI/NoResults";
 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 {
   isTauri: boolean;
   changeInput: (val: string) => void;
   isChatMode: boolean;
   input: string;
-  querySearch: (input: string) => Promise<any>;
-  queryDocuments: (
-    from: number,
-    size: number,
-    queryStrings: any
-  ) => Promise<any>;
   hideCoco?: () => void;
   openSetting: () => void;
   setWindowAlwaysOnTop: (isPinned: boolean) => Promise<void>;
@@ -28,13 +40,12 @@ function Search({
   isTauri,
   isChatMode,
   input,
-  querySearch,
-  queryDocuments,
   hideCoco,
   openSetting,
   setWindowAlwaysOnTop,
 }: SearchProps) {
   const sourceData = useSearchStore((state) => state.sourceData);
+  const queryTimeout = useConnectStore((state) => state.queryTimeout);
 
   const [IsError, setIsError] = useState<boolean>(false);
   const [suggests, setSuggests] = useState<any[]>([]);
@@ -43,12 +54,48 @@ function Search({
 
   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 || [];
 
       setSuggests(data);
@@ -58,38 +105,33 @@ function Search({
         if (!acc[name]) {
           acc[name] = [];
         }
+        item.document.querySource = item?.source;
         acc[name].push(item);
         return acc;
       }, {});
-
       setSearchData(search_data);
-
-      setIsError(false);
       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([]);
-      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 (
     <div ref={mainWindowRef} className={`h-full pb-10 w-full relative`}>
       {/* Search Results Panel */}
       {suggests.length > 0 ? (
         sourceData ? (
-          <SearchResults
-            input={input}
-            isChatMode={isChatMode}
-            queryDocuments={queryDocuments}
-          />
+          <SearchResults input={input} isChatMode={isChatMode} />
         ) : (
           <DropdownList
             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 ListRight from "./ListRight";
-import { useSearchStore } from "@/stores/searchStore";
 import { useAppStore } from "@/stores/appStore";
 import { useIsMobile } from "@/hooks/useIsMobile";
 
@@ -14,7 +14,7 @@ interface SearchListItemProps {
   onItemClick: () => void;
   itemRef: (el: HTMLDivElement | null) => void;
   showIndex?: boolean;
-  goToTwoPage?: (item: any) => void;
+  goToTwoPage?: () => void;
   showListRight?: boolean;
 }
 
@@ -32,16 +32,6 @@ const SearchListItem: React.FC<SearchListItemProps> = React.memo(
   }) => {
     const isTauri = useAppStore((state) => state.isTauri);
 
-    const setVisibleContextMenu = useSearchStore(
-      (state) => state.setVisibleContextMenu
-    );
-
-    const onContextMenu = (event: MouseEvent) => {
-      event.preventDefault();
-
-      setVisibleContextMenu(true);
-    };
-
     const isMobile = useIsMobile();
 
     return (
@@ -49,25 +39,26 @@ const SearchListItem: React.FC<SearchListItemProps> = React.memo(
         ref={itemRef}
         onMouseEnter={onMouseEnter}
         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
           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 `}
         >
           <ItemIcon item={item} />
           <span className={`text-sm truncate text-left`}>{item?.title}</span>
         </div>
         {!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}
         {showListRight && (isTauri || !isMobile) ? (
           <ListRight

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

@@ -10,6 +10,7 @@ import {
 } from "lucide-react";
 import clsx from "clsx";
 import { useTranslation } from "react-i18next";
+import { useDebounce } from "ahooks";
 
 import TypeIcon from "@/components/Common/Icons/TypeIcon";
 import { useConnectStore } from "@/stores/connectStore";
@@ -17,8 +18,9 @@ import { useSearchStore } from "@/stores/searchStore";
 import { DataSource } from "@/types/commands";
 import Checkbox from "@/components/Common/Checkbox";
 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 {
   isSearchActive: boolean;
@@ -39,6 +41,8 @@ export default function SearchPopover({
   getDataSourcesByServer,
 }: SearchPopoverProps) {
   const { t } = useTranslation();
+  const { connected } = useChatStore();
+
   const [isRefreshDataSource, setIsRefreshDataSource] = useState(false);
   const [dataSourceList, setDataSourceList] = useState<DataSource[]>([]);
 
@@ -47,7 +51,6 @@ export default function SearchPopover({
 
   const currentService = useConnectStore((state) => state.currentService);
 
-  const [showDataSource, setShowDataSource] = useState(false);
   const [keyword, setKeyword] = useState("");
   const debouncedKeyword = useDebounce(keyword, { wait: 500 });
 
@@ -85,8 +88,7 @@ export default function SearchPopover({
     }
   }, [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 internetSearchScope = useShortcutsStore((state) => {
     return state.internetSearchScope;
@@ -96,26 +98,6 @@ export default function SearchPopover({
   const [visibleList, setVisibleList] = useState<DataSource[]>([]);
   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(() => {
     if (dataSourceList.length > 0) {
       setSourceDataIds(dataSourceList.slice(1).map((item) => item.id));
@@ -123,15 +105,17 @@ export default function SearchPopover({
   }, [dataSourceList]);
 
   useEffect(() => {
-    getDataSourceList();
-  }, [currentService?.id, debouncedKeyword]);
+    connected && getDataSourceList();
+  }, [connected, currentService?.id, debouncedKeyword]);
 
   useEffect(() => {
-    setTotalPage(Math.ceil(dataSourceList.length / 10));
+    setTotalPage(Math.max(Math.ceil(dataSourceList.length / 10), 1));
   }, [dataSourceList]);
 
   useEffect(() => {
-    if (dataSourceList.length === 0) return;
+    if (dataSourceList.length === 0) {
+      return setVisibleList([]);
+    }
 
     const startIndex = (page - 1) * 9;
     const endIndex = startIndex + 9;
@@ -215,165 +199,152 @@ export default function SearchPopover({
             {t("search.input.search")}
           </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) => {
                   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>
-                            </li>
-                          );
-                        })}
-                      </ul>
+                            </div>
+                          </li>
+                        );
+                      })}
+                    </ul>
+                  ) : (
+                    <div className="flex items-center justify-center py-4">
+                      <NoDataImage />
                     </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>
+
+                    <VisibleKey shortcut="rightarrow" onKeyPress={handleNext}>
+                      <ChevronRight className="size-4" onClick={handleNext} />
+                    </VisibleKey>
                   </div>
-                </PopoverPanel>
-              ) : null}
-            </Popover>
-          )}
+                )}
+              </div>
+            </PopoverPanel>
+          </Popover>
         </>
       )}
     </div>

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

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

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

@@ -24,13 +24,13 @@ import { useStartupStore } from "@/stores/startupStore";
 import { DataSource } from "@/types/commands";
 import { useThemeStore } from "@/stores/themeStore";
 import { Get } from "@/api/axiosRequest";
+import { useConnectStore } from "@/stores/connectStore";
 
 interface SearchChatProps {
   isTauri?: boolean;
   hasModules?: string[];
   defaultModule?: "search" | "chat";
 
-  hasFeature?: string[];
   showChatHistory?: boolean;
 
   theme?: "auto" | "light" | "dark";
@@ -39,36 +39,31 @@ interface SearchChatProps {
 
   hideCoco?: () => void;
   setIsPinned?: (value: boolean) => void;
-  querySearch: (input: string) => Promise<any>;
-  queryDocuments: (
-    from: number,
-    size: number,
-    queryStrings: any
-  ) => Promise<any>;
   onModeChange?: (isChatMode: boolean) => void;
   isMobile?: boolean;
+  assistantIDs?: string[];
 }
 
 function SearchChat({
   isTauri = true,
   hasModules = ["search", "chat"],
   defaultModule = "search",
-  hasFeature = ["think", "search", "think_active", "search_active"],
   theme,
   hideCoco,
-  querySearch,
-  queryDocuments,
   searchPlaceholder,
   chatPlaceholder,
   showChatHistory = true,
   setIsPinned,
   onModeChange,
   isMobile = false,
+  assistantIDs,
 }: SearchChatProps) {
+  const currentAssistant = useConnectStore((state) => state.currentAssistant);
+
   const customInitialState = {
     ...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);
@@ -180,26 +175,31 @@ function SearchChat({
         query?: string;
       }
     ): Promise<DataSource[]> => {
+      let response: any;
       if (isTauri) {
-        return platformAdapter.invokeBackend("get_datasources_by_server", {
+        response = platformAdapter.invokeBackend("get_datasources_by_server", {
           id: serverId,
           options,
         });
       } else {
-        const [error, response]: any = await Get("/datasource/_search");
+        const [error, res]: any = await Get("/datasource/_search");
         if (error) {
           console.error("_search", error);
           return [];
         }
-        const res = response?.hits?.hits?.map((item: any) => {
+        response = res?.hits?.hits?.map((item: any) => {
           return {
             ...item,
             id: item._source.id,
             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}
           isDeepThinkActive={isDeepThinkActive}
           setIsDeepThinkActive={toggleDeepThinkActive}
-          hasFeature={hasFeature}
           getDataSourcesByServer={getDataSourcesByServer}
           setupWindowFocusListener={setupWindowFocusListener}
           checkScreenPermission={checkScreenPermission}
@@ -340,8 +339,6 @@ function SearchChat({
             input={input}
             isChatMode={isChatMode}
             changeInput={setInput}
-            querySearch={querySearch}
-            queryDocuments={queryDocuments}
             hideCoco={hideCoco}
             openSetting={openSetting}
             setWindowAlwaysOnTop={setWindowAlwaysOnTop}
@@ -357,20 +354,18 @@ function SearchChat({
             : "-top-[506px] opacity-0 pointer-events-none"
         } 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>
 
       <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) => {
     return state.setHistoricalRecords;
   });
+  const aiAssistant = useShortcutsStore((state) => {
+    return state.aiAssistant;
+  });
+  const setAiAssistant = useShortcutsStore((state) => {
+    return state.setAiAssistant;
+  });
   const newSession = useShortcutsStore((state) => state.newSession);
   const setNewSession = useShortcutsStore((state) => state.setNewSession);
   const fixedWindow = useShortcutsStore((state) => state.fixedWindow);
@@ -110,6 +116,12 @@ const Shortcuts = () => {
       value: historicalRecords,
       setValue: setHistoricalRecords,
     },
+    {
+      title: "settings.advanced.shortcuts.aiAssistant.title",
+      description: "settings.advanced.shortcuts.aiAssistant.description",
+      value: aiAssistant,
+      setValue: setAiAssistant,
+    },
     {
       title: "settings.advanced.shortcuts.newSession.title",
       description: "settings.advanced.shortcuts.newSession.description",

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

@@ -33,13 +33,26 @@ const Advanced = () => {
   const setConnectionTimeout = useConnectStore((state) => {
     return state.setConnectionTimeout;
   });
+  const queryTimeout = useConnectStore((state) => {
+    return state.queryTimeout;
+  });
+  const setQueryTimeout = useConnectStore((state) => {
+    return state.setQueryTimeout;
+  });
 
   useEffect(() => {
-    const unlisten = useStartupStore.subscribe((state) => {
+    const unsubscribeStartup = useStartupStore.subscribe((state) => {
       emit("change-startup-store", state);
     });
 
-    return unlisten;
+    const unsubscribeConnect = useConnectStore.subscribe((state) => {
+      emit("change-connect-store", state);
+    });
+
+    return () => {
+      unsubscribeStartup();
+      unsubscribeConnect();
+    };
   }, []);
 
   const startupList = [
@@ -162,6 +175,22 @@ const Advanced = () => {
             }}
           />
         </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>
   );

+ 4 - 0
src/constants/index.ts

@@ -1,3 +1,7 @@
 export const POPOVER_PANEL_SELECTOR = '[id^="headlessui-popover-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 { Get, Post } from "@/api/axiosRequest";
 import platformAdapter from "@/utils/platformAdapter";
+import { useConnectStore } from "@/stores/connectStore";
+import { useChatStore } from "@/stores/chatStore";
 
 export function useChatActions(
   currentServiceId: string | undefined,
@@ -23,6 +25,8 @@ export function useChatActions(
 ) {
   const isTauri = useAppStore((state) => state.isTauri);
   const addError = useAppStore((state) => state.addError);
+  const currentAssistant = useConnectStore((state) => state.currentAssistant);
+  const { connected } = useChatStore();
 
   const [keyword, setKeyword] = useState("");
 
@@ -145,6 +149,7 @@ export function useChatActions(
         let response: any;
         if (isTauri) {
           if (!currentServiceId) return;
+          console.log("currentAssistant", currentAssistant);
           response = await platformAdapter.commands("new_chat", {
             serverId: currentServiceId,
             websocketId: websocketSessionId || id,
@@ -153,6 +158,7 @@ export function useChatActions(
               search: isSearchActive,
               deep_thinking: isDeepThinkActive,
               datasource: sourceDataIds?.join(",") || "",
+              assistant_id: currentAssistant?._id || '',
             },
           });
         } else {
@@ -203,6 +209,7 @@ export function useChatActions(
       isDeepThinkActive,
       curIdRef,
       websocketSessionId,
+      currentAssistant,
     ]
   );
 
@@ -228,6 +235,7 @@ export function useChatActions(
               search: isSearchActive,
               deep_thinking: isDeepThinkActive,
               datasource: sourceDataIds?.join(",") || "",
+              assistant_id: currentAssistant?._id || '',
             },
             message: content,
           });
@@ -280,6 +288,7 @@ export function useChatActions(
       setCurChatEnd,
       changeInput,
       websocketSessionId,
+      currentAssistant,
     ]
   );
 
@@ -333,9 +342,9 @@ export function useChatActions(
   );
 
   const getChatHistory = useCallback(async () => {
-    try {
-      let response: any;
-      if (isTauri) {
+    let response: any;
+    if (isTauri) {
+      try {
         if (!currentServiceId) return [];
         response = await platformAdapter.commands("chat_history", {
           serverId: currentServiceId,
@@ -343,32 +352,31 @@ export function useChatActions(
           size: 20,
           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]);
 
   useEffect(() => {
-    showChatHistory && getChatHistory();
-  }, [showChatHistory]);
+    showChatHistory && connected && getChatHistory();
+  }, [showChatHistory, connected, getChatHistory]);
 
   const createChatWindow = useCallback(async (createWin: any) => {
     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 { useSearchStore } from "@/stores/searchStore";
 
 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(() => {
     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 { useStartupStore } from "@/stores/startupStore";
 import platformAdapter from "@/utils/platformAdapter";
@@ -40,6 +41,9 @@ export const useSyncStore = () => {
   const setHistoricalRecords = useShortcutsStore((state) => {
     return state.setHistoricalRecords;
   });
+  const setAiAssistant = useShortcutsStore((state) => {
+    return state.setAiAssistant;
+  });
   const setNewSession = useShortcutsStore((state) => {
     return state.setNewSession;
   });
@@ -61,6 +65,12 @@ export const useSyncStore = () => {
   const setResetFixedWindow = useShortcutsStore((state) => {
     return state.setResetFixedWindow;
   });
+  const setConnectionTimeout = useConnectStore((state) => {
+    return state.setConnectionTimeout;
+  });
+  const setQueryTimeout = useConnectStore((state) => {
+    return state.setQueryTimeout;
+  });
 
   useEffect(() => {
     if (!resetFixedWindow) {
@@ -83,6 +93,7 @@ export const useSyncStore = () => {
           internetSearch,
           internetSearchScope,
           historicalRecords,
+          aiAssistant,
           newSession,
           fixedWindow,
           serviceList,
@@ -97,6 +108,7 @@ export const useSyncStore = () => {
         setInternetSearch(internetSearch);
         setInternetSearchScope(internetSearchScope);
         setHistoricalRecords(historicalRecords);
+        setAiAssistant(aiAssistant);
         setNewSession(newSession);
         setFixedWindow(fixedWindow);
         setServiceList(serviceList);
@@ -113,6 +125,12 @@ export const useSyncStore = () => {
         setDefaultContentForSearchWindow(defaultContentForSearchWindow);
         setDefaultContentForChatWindow(defaultContentForChatWindow);
       }),
+
+      platformAdapter.listenEvent("change-connect-store", ({ payload }) => {
+        const { connectionTimeout, queryTimeout } = payload;
+        setConnectionTimeout(connectionTimeout);
+        setQueryTimeout(queryTimeout);
+      }),
     ]);
 
     return () => {

+ 18 - 19
src/hooks/useWebSocket.ts

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

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

@@ -119,6 +119,10 @@
           "title": "Conversation History",
           "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": {
           "title": "New Conversation",
           "description": "Shortcut to start a new conversation in chat mode."
@@ -141,6 +145,10 @@
         "connectionTimeout": {
           "title": "Connection Timeout",
           "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",
       "createdAt": "Created At",
       "category": "Category",
+      "richCategories": "RichCategories",
       "subcategory": "Subcategory",
       "language": "Language",
       "tags": "Tags",
@@ -193,7 +202,8 @@
     "list": {
       "loading": "Loading...",
       "noResults": "No Results",
-      "noDataAlt": "No data image"
+      "noDataAlt": "No data image",
+      "failures": "Partial results returned due to service failures."
     },
     "footer": {
       "logoAlt": "Coco Logo",
@@ -232,7 +242,14 @@
     },
     "contextMenu": {
       "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": {
@@ -373,5 +390,13 @@
         "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": "历史记录",
           "description": "在聊天模式下查看历史对话记录的快捷键。"
         },
+        "aiAssistant": {
+          "title": "AI 助手",
+          "description": "在聊天模式下查看 AI 助手列表快捷键。"
+        },
         "newSession": {
           "title": "新建会话",
           "description": "在聊天模式下创建新对话的快捷按键。"
@@ -141,6 +145,10 @@
         "connectionTimeout": {
           "title": "连接超时",
           "description": "如果在此时间内未收到响应,则重试连接。默认值:120 秒。"
+        },
+        "queryTimeout": {
+          "title": "查询超时",
+          "description": "在此时间内未返回搜索结果,则终止查询。默认值:5 秒。"
         }
       }
     },
@@ -182,6 +190,7 @@
       "id": "ID",
       "createdAt": "创建时间",
       "category": "分类",
+      "richCategories": "分类",
       "subcategory": "子分类",
       "language": "语言",
       "tags": "标签",
@@ -195,7 +204,8 @@
     "list": {
       "loading": "加载中...",
       "noResults": "暂无结果",
-      "noDataAlt": "无数据图片"
+      "noDataAlt": "无数据图片",
+      "failures": "部分服务暂时不可用,请检查相关设置。"
     },
     "footer": {
       "logoAlt": "Coco 图标",
@@ -234,7 +244,14 @@
     },
     "contextMenu": {
       "open": "打开",
-      "copyLink": "复制链接"
+      "copyLink": "复制链接",
+      "copyAnswer": "复制答案",
+      "copyUppercaseAnswer": "复制答案(大写)",
+      "copyQuestionAndAnswer": "复制问题和答案",
+      "title": {
+        "calculator": "计算器"
+      },
+      "search": "搜索操作"
     }
   },
   "assistant": {
@@ -374,5 +391,13 @@
         "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}
               key="ChatAI"
               activeChatProp={activeChat}
-              isTransitioned={true}
               isSearchActive={isSearchActive}
               isDeepThinkActive={isDeepThinkActive}
               setIsSidebarOpen={setIsSidebarOpen}

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

@@ -8,47 +8,7 @@ import { useSyncStore } from "@/hooks/useSyncStore";
 function MainApp() {
   const setIsTauri = useAppStore((state) => state.setIsTauri);
   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(() => {
     return platformAdapter.hideWindow();
   }, []);
@@ -58,8 +18,6 @@ function MainApp() {
   return (
     <SearchChat
       isTauri={true}
-      querySearch={querySearch}
-      queryDocuments={queryDocuments}
       hideCoco={hideCoco}
       hasModules={["search", "chat"]}
     />

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

@@ -32,12 +32,6 @@
 - **默认值**: `['search', 'chat']`
 - **描述**: 启用的功能模块列表,目前支持 'search' 和 'chat' 模块
 
-### `hasFeature`
-- **类型**: `string[]`
-- **可选**: 是
-- **默认值**: `['think', 'search', 'think_active', 'search_active']`
-- **描述**: 启用的特性列表,支持 'think'、'search'、'think_active'、'search_active' 特性。其中 'think_active' 表示默认开启深度思考,'search_active' 表示默认开启搜索
-
 ### `hideCoco`
 - **类型**: `() => void`
 - **可选**: 是
@@ -87,7 +81,6 @@ function App() {
       width={680}
       height={590}
       hasModules={['search', 'chat']}
-      hasFeature={['think', 'search', 'think_active', 'search_active']}
       hideCoco={() => console.log('hide')}
       theme="dark"
       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 { useAppStore } from "@/stores/appStore";
-import { Get } from "@/api/axiosRequest";
 import { useShortcutsStore } from "@/stores/shortcutsStore";
-import { useIsMobile } from '@/hooks/useIsMobile';
+import { useIsMobile } from "@/hooks/useIsMobile";
 import { useModifierKeyPress } from "@/hooks/useModifierKeyPress";
 
 import "@/i18n";
@@ -17,7 +16,7 @@ interface WebAppProps {
   height?: number;
   hasModules?: string[];
   defaultModule?: "search" | "chat";
-  hasFeature?: string[];
+  assistantIDs?: string[];
   hideCoco?: () => void;
   theme?: "auto" | "light" | "dark";
   searchPlaceholder?: string;
@@ -32,7 +31,7 @@ function WebApp({
   height = 590,
   headers = {
     "X-API-TOKEN":
-      "cvvitp6hpceh0ip1q1706byts41c7213k4el22v3bp6f4ta2sar0u29jp4pg08h6xcyxn085x3lq1k7wojof",
+      "cvqt6r02sdb2v3bkgip0x3ixv01f3r2lhnxoz1efbn160wm9og58wtv8t6wrv1ebvnvypuc23dx9pb33aemh",
     "APP-INTEGRATION-ID": "cvkm9hmhpcemufsg3vug",
   },
   // token = "cva1j5ehpcenic3ir7k0h8fb8qtv35iwtywze248oscrej8yoivhb5b1hyovp24xejjk27jy9ddt69ewfi3n",   // https://coco.infini.cloud
@@ -42,7 +41,7 @@ function WebApp({
   hideCoco = () => {},
   hasModules = ["search", "chat"],
   defaultModule = "search",
-  hasFeature = ["think_active", "search_active"],
+  assistantIDs = [],
   theme = "dark",
   searchPlaceholder = "",
   chatPlaceholder = "",
@@ -67,60 +66,6 @@ function WebApp({
 
   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 [isChatMode, setIsChatMode] = useState(false);
@@ -139,7 +84,7 @@ function WebApp({
       {isMobile && (
         <div
           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`}
           onClick={onCancel}
         >
@@ -158,16 +103,14 @@ function WebApp({
         hideCoco={hideCoco}
         hasModules={hasModules}
         defaultModule={defaultModule}
-        hasFeature={hasFeature}
         theme={theme}
         searchPlaceholder={searchPlaceholder}
         chatPlaceholder={chatPlaceholder}
-        querySearch={querySearch}
-        queryDocuments={queryDocuments}
         showChatHistory={showChatHistory}
         setIsPinned={setIsPinned}
         onModeChange={setIsChatMode}
         isMobile={isMobile}
+        assistantIDs={assistantIDs}
       />
     </div>
   );

+ 8 - 5
src/stores/appStore.ts

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

+ 101 - 69
src/stores/connectStore.ts

@@ -1,5 +1,5 @@
 import { create } from "zustand";
-import { persist } from "zustand/middleware";
+import { persist, subscribeWithSelector } from "zustand/middleware";
 import { produce } from "immer";
 
 import platformAdapter from "@/utils/platformAdapter";
@@ -24,77 +24,109 @@ export type IConnectStore = {
   setConnectionTimeout: (connectionTimeout: number) => void;
   currentSessionId?: string;
   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>()(
-  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;
   historicalRecords: string;
   setHistoricalRecords: (historicalRecords: string) => void;
+  aiAssistant: string;
+  setAiAssistant: (aiAssistant: string) => void;
   newSession: string;
   setNewSession: (newSession: string) => void;
   fixedWindow: string;
@@ -72,6 +74,8 @@ export const useShortcutsStore = create<IShortcutsStore>()(
       setHistoricalRecords: (historicalRecords) => {
         return set({ historicalRecords });
       },
+      aiAssistant: "U",
+      setAiAssistant: (aiAssistant) => set({ aiAssistant }),
       newSession: "N",
       setNewSession: (newSession) => set({ newSession }),
       fixedWindow: "P",
@@ -96,6 +100,7 @@ export const useShortcutsStore = create<IShortcutsStore>()(
         deepThinking: state.deepThinking,
         internetSearch: state.internetSearch,
         historicalRecords: state.historicalRecords,
+        aiAssistant: state.aiAssistant,
         newSession: state.newSession,
         fixedWindow: state.fixedWindow,
         serviceList: state.serviceList,

+ 2 - 0
src/types/platform.ts

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

+ 7 - 1
src/utils/index.ts

@@ -1,9 +1,13 @@
 import { useEffect, useState } from "react";
 
 import platformAdapter from "./platformAdapter";
+import { useAppStore } from "@/stores/appStore";
 
 // 1
 export async function copyToClipboard(text: string) {
+  const addError = useAppStore.getState().addError;
+  const language = useAppStore.getState().language;
+
   try {
     if (window.__TAURI__) {
       window.__TAURI__.writeText(text);
@@ -26,6 +30,8 @@ export async function copyToClipboard(text: string) {
     }
     document.body.removeChild(textArea);
   }
+
+  addError(language === "zh" ? "复制成功" : "Copy Success", "info");
 }
 
 // 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;
 
   return regex.test(value);
-};
+};

+ 2 - 2
src/utils/platformAdapter.ts

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

+ 3 - 3
src/utils/webAdapter.ts

@@ -3,7 +3,7 @@ import type { BasePlatformAdapter } from "@/types/platform";
 export interface WebPlatformAdapter extends BasePlatformAdapter {
   // Add web-specific methods here
   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
@@ -176,8 +176,8 @@ export const createWebAdapter = (): WebPlatformAdapter => {
       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({});
     },
   };

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

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

+ 1 - 1
tsup.config.ts

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

+ 41 - 26
vite.config.ts

@@ -1,8 +1,8 @@
 import { defineConfig } from "vite";
 import react from "@vitejs/plugin-react";
-import path from 'path';
+import path from "path";
 import { config } from "dotenv";
-import packageJson from './package.json';
+import packageJson from "./package.json";
 
 config();
 
@@ -12,12 +12,12 @@ const host = process.env.TAURI_DEV_HOST;
 // https://vitejs.dev/config/
 export default defineConfig(async () => ({
   define: {
-    'process.env.VERSION': JSON.stringify(packageJson.version),
+    "process.env.VERSION": JSON.stringify(packageJson.version),
   },
   plugins: [react()],
   resolve: {
     alias: {
-      '@': path.resolve(__dirname, './src'),
+      "@": path.resolve(__dirname, "./src"),
     },
   },
   // 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,
     hmr: host
       ? {
-        protocol: "ws",
-        host,
-        port: 1421,
-      }
+          protocol: "ws",
+          host,
+          port: 1421,
+        }
       : undefined,
     watch: {
       // 3. tell vite to ignore watching `src-tauri`
@@ -61,31 +61,46 @@ export default defineConfig(async () => ({
         changeOrigin: true,
         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: {
     rollupOptions: {
       output: {
         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,

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott