mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-17 20:17:43 +01:00
Compare commits
48 Commits
v0.4.0
...
upload-fil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ee6b9a6c9 | ||
|
|
24b1758b11 | ||
|
|
ac21074db6 | ||
|
|
496ae025d8 | ||
|
|
ac5a196746 | ||
|
|
aa99588001 | ||
|
|
163df77e8a | ||
|
|
21509f35e5 | ||
|
|
7bf59aa259 | ||
|
|
4aa377e486 | ||
|
|
feb716039c | ||
|
|
448d2a6069 | ||
|
|
c31a4aa52a | ||
|
|
73ac29ef3b | ||
|
|
3cd73f13ab | ||
|
|
95ccbaec3e | ||
|
|
d52ce481f9 | ||
|
|
573e1cf038 | ||
|
|
5162604cfd | ||
|
|
e38053682d | ||
|
|
018ec9e4ed | ||
|
|
f9e5c6cc28 | ||
|
|
6bb64e92d9 | ||
|
|
7962c329c7 | ||
|
|
dd6bd2093d | ||
|
|
25d998a41c | ||
|
|
3cfb03dd49 | ||
|
|
386b9cc48b | ||
|
|
006b679386 | ||
|
|
d47fb3cbc6 | ||
|
|
26f71cff08 | ||
|
|
ae8f95e19c | ||
|
|
4c49daf510 | ||
|
|
8d2528e521 | ||
|
|
4895322397 | ||
|
|
a8a4d435fc | ||
|
|
1c0335feb4 | ||
|
|
8498578425 | ||
|
|
326e161505 | ||
|
|
e96e6b4a89 | ||
|
|
853ea38058 | ||
|
|
4e127f8cdc | ||
|
|
51ada19d42 | ||
|
|
86f3741302 | ||
|
|
bb50b150c0 | ||
|
|
a092354fee | ||
|
|
2ffbb79358 | ||
|
|
661b5d1b77 |
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -50,6 +50,8 @@ jobs:
|
||||
|
||||
- platform: "ubuntu-22.04"
|
||||
target: "x86_64-unknown-linux-gnu"
|
||||
- platform: "ubuntu-22.04-arm"
|
||||
target: "aarch64-unknown-linux-gnu"
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
@@ -67,10 +69,10 @@ jobs:
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
if: startsWith(matrix.platform, 'ubuntu-22.04')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
|
||||
|
||||
- name: Install Rust stable
|
||||
run: rustup toolchain install stable
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -25,3 +25,4 @@ src/components/web
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.env
|
||||
|
||||
3
Makefile
3
Makefile
@@ -76,3 +76,6 @@ clean-rebuild:
|
||||
@echo "Cleaning up and rebuilding..."
|
||||
rm -rf node_modules
|
||||
$(MAKE) dev-build
|
||||
|
||||
add-dep-pizza-engine:
|
||||
cd src-tauri && cargo add --git ssh://git@github.com/infinilabs/pizza.git pizza-engine --features query_string_parser,persistence
|
||||
@@ -93,6 +93,12 @@ pnpm tauri build
|
||||
- [Coco Server Documentation](https://docs.infinilabs.com/coco-server/main/)
|
||||
- [Tauri Documentation](https://tauri.app/)
|
||||
|
||||
## Contributors
|
||||
|
||||
<a href="https://github.com/infinilabs/coco-app/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=infinilabs/coco-app" />
|
||||
</a>
|
||||
|
||||
## 📄 License
|
||||
|
||||
Coco AI is an open-source project licensed under the [MIT License](LICENSE). You can freely use, modify, and
|
||||
|
||||
38
docs/content.en/docs/getting-started/installation/ubuntu.md
Normal file
38
docs/content.en/docs/getting-started/installation/ubuntu.md
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
weight: 10
|
||||
title: "Ubuntu"
|
||||
asciinema: true
|
||||
---
|
||||
|
||||
# Ubuntu
|
||||
|
||||
> NOTE: Coco app only works fully under [X11][x11_protocol].
|
||||
>
|
||||
> Don't know if you running X11 or not? take a look at this [question][if_x11]!
|
||||
|
||||
[x11_protocol]: https://en.wikipedia.org/wiki/X_Window_System
|
||||
[if_x11]: https://unix.stackexchange.com/q/202891/498440
|
||||
|
||||
|
||||
## Goto [https://coco.rs/](https://coco.rs/)
|
||||
|
||||
## Download the package
|
||||
|
||||
Download the package of your architecture, it should be put in your `Downloads` directory
|
||||
and look like this:
|
||||
|
||||
```sh
|
||||
$ cd ~/Downloads
|
||||
$ ls
|
||||
Coco-AI-x.y.z-bbbb-deb-linux-amd64.zip
|
||||
# or Coco-AI-x.y.z-bbbb-deb-linux-arm64.zip depending on your architecture
|
||||
```
|
||||
|
||||
## Install it
|
||||
|
||||
Unzip and install it
|
||||
|
||||
```
|
||||
$ unzip Coco-AI-x.y.z-bbbb-deb-linux-amd64.zip
|
||||
$ sudo dpkg -i Coco-AI-x.y.z-bbbb-deb-linux-amd64.deb
|
||||
```
|
||||
@@ -13,10 +13,39 @@ Information about release notes of Coco Server is provided here.
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- feat: check or enter to close the list of assistants #469
|
||||
- feat: add dimness settings for pinned window #470
|
||||
- feat: supports Shift + Enter input box line feeds #472
|
||||
- feat: support for snapshot version updates #480
|
||||
- feat: history list add put away button #482
|
||||
- feat: the chat input box supports multi-line input #490
|
||||
- feat: add `~/Applications` to the search path #493
|
||||
- feat: the chat content has added a button to return to the bottom #495
|
||||
- feat: the search input box supports multi-line input #501
|
||||
- feat: websocket support self-signed TLS #504
|
||||
- feat: add option to allow self-signed certificates #509
|
||||
|
||||
### 🐛 Bug fix
|
||||
|
||||
- fix: several issues around search #502
|
||||
- fix: fixed the newly created session has no title when it is deleted #511
|
||||
- fix: loading chat history for potential empty attachments
|
||||
|
||||
### ✈️ Improvements
|
||||
|
||||
- chore: adjust list error message #475
|
||||
- fix: solve the problem of modifying the assistant in the chat #476
|
||||
- chore: refine wording on search failure
|
||||
- chore:search and MCP show hidden logic #494
|
||||
- chore: greetings show hidden logic #496
|
||||
- refactor: fetch app list in settings in real time #498
|
||||
- chore: UpdateApp component loading location #499
|
||||
- chore: add clear monitoring & cache calculation to optimize performance #500
|
||||
- refactor: optimizing the code #505
|
||||
- refactor: optimized the modification operation of the numeric input box #508
|
||||
- style: modify the style of the search input box #513
|
||||
- style: chat input icons show #515
|
||||
|
||||
## 0.4.0 (2025-04-27)
|
||||
|
||||
### Breaking changes
|
||||
|
||||
53
package.json
53
package.json
@@ -19,36 +19,37 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.0.0",
|
||||
"@headlessui/react": "^2.2.0",
|
||||
"@tauri-apps/api": "^2.4.0",
|
||||
"@headlessui/react": "^2.2.2",
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
"@tauri-apps/plugin-autostart": "~2.2.0",
|
||||
"@tauri-apps/plugin-deep-link": "^2.2.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.0",
|
||||
"@tauri-apps/plugin-deep-link": "^2.2.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||
"@tauri-apps/plugin-global-shortcut": "~2.0.0",
|
||||
"@tauri-apps/plugin-http": "~2.0.2",
|
||||
"@tauri-apps/plugin-log": "~2.4.0",
|
||||
"@tauri-apps/plugin-os": "^2.2.1",
|
||||
"@tauri-apps/plugin-process": "^2.2.0",
|
||||
"@tauri-apps/plugin-shell": "^2.2.0",
|
||||
"@tauri-apps/plugin-updater": "^2.6.1",
|
||||
"@tauri-apps/plugin-process": "^2.2.1",
|
||||
"@tauri-apps/plugin-shell": "^2.2.1",
|
||||
"@tauri-apps/plugin-updater": "github:infinilabs/tauri-plugin-updater#v2",
|
||||
"@tauri-apps/plugin-websocket": "~2.3.0",
|
||||
"@tauri-apps/plugin-window": "2.0.0-alpha.1",
|
||||
"@wavesurfer/react": "^1.0.9",
|
||||
"@wavesurfer/react": "^1.0.11",
|
||||
"ahooks": "^3.8.4",
|
||||
"axios": "^1.8.4",
|
||||
"axios": "^1.9.0",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^16.4.7",
|
||||
"dotenv": "^16.5.0",
|
||||
"filesize": "^10.1.6",
|
||||
"i18next": "^23.16.8",
|
||||
"i18next-browser-languagedetector": "^8.0.4",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "^0.461.0",
|
||||
"mermaid": "^11.5.0",
|
||||
"mermaid": "^11.6.0",
|
||||
"nanoid": "^5.1.5",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hotkeys-hook": "^4.6.1",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-hotkeys-hook": "^4.6.2",
|
||||
"react-i18next": "^15.5.1",
|
||||
"react-markdown": "^9.1.0",
|
||||
"react-router-dom": "^6.30.0",
|
||||
"react-window": "^1.8.11",
|
||||
@@ -58,25 +59,25 @@
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"tauri-plugin-fs-pro-api": "^2.4.0",
|
||||
"tauri-plugin-macos-permissions-api": "^2.2.0",
|
||||
"tauri-plugin-screenshots-api": "^2.1.0",
|
||||
"tauri-plugin-macos-permissions-api": "^2.3.0",
|
||||
"tauri-plugin-screenshots-api": "^2.2.0",
|
||||
"tauri-plugin-windows-version-api": "^2.0.0",
|
||||
"use-debounce": "^10.0.4",
|
||||
"uuid": "^11.1.0",
|
||||
"wavesurfer.js": "^7.9.3",
|
||||
"zustand": "^5.0.3"
|
||||
"wavesurfer.js": "^7.9.5",
|
||||
"zustand": "^5.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.4.0",
|
||||
"@tauri-apps/cli": "^2.5.0",
|
||||
"@types/dom-speech-recognition": "^0.0.4",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/node": "^22.13.11",
|
||||
"@types/react": "^18.3.19",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@types/node": "^22.15.17",
|
||||
"@types/react": "^18.3.21",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@types/react-katex": "^3.0.4",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"cross-env": "^7.0.3",
|
||||
"immer": "^10.1.1",
|
||||
@@ -85,8 +86,8 @@
|
||||
"sass": "^1.87.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tsup": "^8.4.0",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^5.4.14"
|
||||
"tsx": "^4.19.4",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^5.4.19"
|
||||
}
|
||||
}
|
||||
|
||||
1756
pnpm-lock.yaml
generated
1756
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
1656
src-tauri/Cargo.lock
generated
1656
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,26 @@ tauri-build = { version = "2", features = ["default"] }
|
||||
default = ["desktop"]
|
||||
desktop = []
|
||||
cargo-clippy = []
|
||||
# If enabled, code that relies on pizza_engine will be activated.
|
||||
#
|
||||
# Only do this if:
|
||||
# 1. Pizza engine is listed in the `dependencies` section
|
||||
#
|
||||
# ```toml
|
||||
# [dependencies]
|
||||
# pizza-engine = { git = "ssh://git@github.com/infinilabs/pizza.git", features = ["query_string_parser", "persistence"] }
|
||||
# ```
|
||||
#
|
||||
# 2. It is a private repo, you have access to it.
|
||||
#
|
||||
# So, for external contributors, do NOT enable this feature.
|
||||
#
|
||||
# Previously, We listed it in the dependencies and marked it optional, but cargo
|
||||
# would fetch all the dependencies regardless of wheterh they are optional or not,
|
||||
# so we removed it.
|
||||
#
|
||||
# https://github.com/rust-lang/cargo/issues/4544#issuecomment-1906902755
|
||||
use_pizza_engine = []
|
||||
|
||||
[dependencies]
|
||||
pizza-common = { git = "https://github.com/infinilabs/pizza-common", branch = "main" }
|
||||
@@ -37,17 +57,15 @@ tauri-plugin-store = "2.2.0"
|
||||
tauri-plugin-os = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-fs = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
tauri-plugin-process = "2"
|
||||
tauri-plugin-drag = "2"
|
||||
tauri-plugin-macos-permissions = "2"
|
||||
tauri-plugin-fs-pro = "2"
|
||||
tauri-plugin-screenshots = "2"
|
||||
applications = { git = "https://github.com/infinilabs/applications-rs", rev = "fb8f475993a2a774ce08d7a58f9f2ac264248a24" }
|
||||
|
||||
applications = { git = "https://github.com/infinilabs/applications-rs", rev = "7bb507e6b12f73c96f3a52f0578d0246a689f381" }
|
||||
tokio-native-tls = "0.3" # For wss connections
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-tungstenite = { version = "0.20", features = ["rustls-tls-webpki-roots"] }
|
||||
tokio-tungstenite = { version = "0.20", features = ["native-tls"] }
|
||||
hyper = { version = "0.14", features = ["client"] }
|
||||
reqwest = { version = "0.12", features = ["json", "multipart"] }
|
||||
futures = "0.3.31"
|
||||
@@ -62,19 +80,19 @@ hostname = "0.3"
|
||||
plist = "1.7"
|
||||
base64 = "0.13"
|
||||
walkdir = "2"
|
||||
fuzzy_prefix_search = "0.2"
|
||||
log = "0.4"
|
||||
|
||||
futures-util = "0.3.31"
|
||||
url = "2.5.2"
|
||||
http = "1.1.0"
|
||||
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"
|
||||
tauri-plugin-log = "2"
|
||||
chrono = "0.4.41"
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }
|
||||
@@ -82,7 +100,6 @@ tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2"
|
||||
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
|
||||
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
|
||||
|
||||
|
||||
[profile.dev]
|
||||
incremental = true # Compile your binary in smaller steps.
|
||||
|
||||
@@ -96,4 +113,7 @@ strip = true # Ensures debug symbols are removed.
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-autostart = "^2.2"
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
tauri-plugin-updater = { git = "https://github.com/infinilabs/plugins-workspace", branch = "v2" }
|
||||
|
||||
[target."cfg(target_os = \"windows\")".dependencies]
|
||||
enigo="0.3"
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"core:window:allow-set-theme",
|
||||
"process:default",
|
||||
"updater:default",
|
||||
"windows-version:default"
|
||||
"windows-version:default",
|
||||
"log:default"
|
||||
]
|
||||
}
|
||||
|
||||
2
src-tauri/rust-toolchain.toml
Normal file
2
src-tauri/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly-2024-10-29"
|
||||
@@ -55,39 +55,3 @@ 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 {
|
||||
Self {
|
||||
id,
|
||||
created: None,
|
||||
updated: None,
|
||||
source,
|
||||
r#type: None,
|
||||
category: Some(category),
|
||||
subcategory: None,
|
||||
categories: None,
|
||||
rich_categories: None,
|
||||
title: Some(name),
|
||||
summary: None,
|
||||
lang: None,
|
||||
content: None,
|
||||
icon: None,
|
||||
thumbnail: None,
|
||||
cover: None,
|
||||
tags: None,
|
||||
url: Some(url),
|
||||
size: None,
|
||||
metadata: None,
|
||||
payload: None,
|
||||
owner: None,
|
||||
last_updated_by: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,9 @@ pub async fn get_response_body_text(response: Response) -> Result<String, String
|
||||
let body = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read response body: {}", e))?;
|
||||
.map_err(|e| format!("Failed to read response body: {}, code: {}", e, status))?;
|
||||
|
||||
log::debug!("Response status: {}, body: {}", status, &body);
|
||||
|
||||
if status < 200 || status >= 400 {
|
||||
// Try to parse the error body
|
||||
|
||||
@@ -4,6 +4,7 @@ mod common;
|
||||
mod local;
|
||||
mod search;
|
||||
mod server;
|
||||
mod settings;
|
||||
mod setup;
|
||||
mod shortcut;
|
||||
mod util;
|
||||
@@ -15,7 +16,9 @@ use crate::server::servers::{load_or_insert_default_server, load_servers_token};
|
||||
use autostart::{change_autostart, enable_autostart};
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::OnceLock;
|
||||
use tauri::async_runtime::block_on;
|
||||
use tauri::plugin::TauriPlugin;
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::ActivationPolicy;
|
||||
use tauri::{
|
||||
@@ -30,6 +33,10 @@ lazy_static! {
|
||||
static ref PREVIOUS_MONITOR_NAME: Mutex<Option<String>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
/// To allow us to access tauri's `AppHandle` when its context is inaccessible,
|
||||
/// store it globally. It will be set in `init()`.
|
||||
pub(crate) static GLOBAL_TAURI_APP_HANDLE: OnceLock<AppHandle> = OnceLock::new();
|
||||
|
||||
#[tauri::command]
|
||||
async fn change_window_height(handle: AppHandle, height: u32) {
|
||||
let window: WebviewWindow = handle.get_webview_window(MAIN_WINDOW_LABEL).unwrap();
|
||||
@@ -55,8 +62,6 @@ struct Payload {
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let ctx = tauri::generate_context!();
|
||||
// Initialize logger
|
||||
env_logger::init();
|
||||
|
||||
let mut app_builder = tauri::Builder::default();
|
||||
|
||||
@@ -83,7 +88,8 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_screenshots::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_windows_version::init());
|
||||
.plugin(tauri_plugin_windows_version::init())
|
||||
.plugin(set_up_tauri_logger());
|
||||
|
||||
// Conditional compilation for macOS
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -134,12 +140,31 @@ pub fn run() {
|
||||
server::attachment::get_attachment,
|
||||
server::attachment::delete_attachment,
|
||||
server::transcription::transcription,
|
||||
local::application::get_default_search_paths,
|
||||
local::application::list_app_with_metadata_in,
|
||||
util::open,
|
||||
server::system_settings::get_system_settings
|
||||
server::system_settings::get_system_settings,
|
||||
simulate_mouse_click,
|
||||
local::get_disabled_local_query_sources,
|
||||
local::enable_local_query_source,
|
||||
local::disable_local_query_source,
|
||||
local::application::get_app_list,
|
||||
local::application::get_app_search_path,
|
||||
local::application::get_app_metadata,
|
||||
local::application::set_app_alias,
|
||||
local::application::register_app_hotkey,
|
||||
local::application::unregister_app_hotkey,
|
||||
local::application::disable_app_search,
|
||||
local::application::enable_app_search,
|
||||
local::application::add_app_search_path,
|
||||
local::application::remove_app_search_path,
|
||||
settings::set_allow_self_signature,
|
||||
settings::get_allow_self_signature,
|
||||
])
|
||||
.setup(|app| {
|
||||
let app_handle = app.handle().clone();
|
||||
GLOBAL_TAURI_APP_HANDLE
|
||||
.set(app_handle.clone())
|
||||
.expect("variable already initialized");
|
||||
|
||||
let registry = SearchSourceRegistry::default();
|
||||
|
||||
app.manage(registry); // Store registry in Tauri's app state
|
||||
@@ -233,31 +258,20 @@ pub async fn init<R: Runtime>(app_handle: &AppHandle<R>) {
|
||||
crate::server::servers::try_register_server_to_search_source(app_handle.clone(), &server)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
local::start_pizza_engine_runtime();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn show_coco<R: Runtime>(app_handle: AppHandle<R>) {
|
||||
if let Some(window) = app_handle.get_window(MAIN_WINDOW_LABEL) {
|
||||
let _ = app_handle.emit("show-coco", ());
|
||||
|
||||
move_window_to_active_monitor(&window);
|
||||
|
||||
let _ = window.show();
|
||||
let _ = window.unminimize();
|
||||
let _ = window.set_focus();
|
||||
|
||||
let _ = app_handle.emit("show-coco", ());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,7 +415,7 @@ fn open_settings(app: &tauri::AppHandle) {
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_app_search_source<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
|
||||
init_app_search_source(&app_handle).await?;
|
||||
local::init_local_search_source(&app_handle).await?;
|
||||
let _ = server::connector::refresh_all_connectors(&app_handle).await;
|
||||
let _ = server::datasource::refresh_all_datasources(&app_handle).await;
|
||||
|
||||
@@ -412,3 +426,98 @@ async fn get_app_search_source<R: Runtime>(app_handle: AppHandle<R>) -> Result<(
|
||||
async fn show_settings(app_handle: AppHandle) {
|
||||
open_settings(&app_handle);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn simulate_mouse_click<R: Runtime>(window: WebviewWindow<R>, is_chat_mode: bool) {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use enigo::{Button, Coordinate, Direction, Enigo, Mouse, Settings};
|
||||
use std::{thread, time::Duration};
|
||||
|
||||
if let Ok(mut enigo) = Enigo::new(&Settings::default()) {
|
||||
// Save the current mouse position
|
||||
if let Ok((original_x, original_y)) = enigo.location() {
|
||||
// Retrieve the window's outer position (top-left corner)
|
||||
if let Ok(position) = window.outer_position() {
|
||||
// Retrieve the window's inner size (client area)
|
||||
if let Ok(size) = window.inner_size() {
|
||||
// Calculate the center position of the title bar
|
||||
let x = position.x + (size.width as i32 / 2);
|
||||
let y = if is_chat_mode {
|
||||
position.y + size.height as i32 - 50
|
||||
} else {
|
||||
position.y + 30
|
||||
};
|
||||
|
||||
// Move the mouse cursor to the calculated position
|
||||
if enigo.move_mouse(x, y, Coordinate::Abs).is_ok() {
|
||||
// // Simulate a left mouse click
|
||||
let _ = enigo.button(Button::Left, Direction::Click);
|
||||
// let _ = enigo.button(Button::Left, Direction::Release);
|
||||
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
|
||||
// Move the mouse cursor back to the original position
|
||||
let _ = enigo.move_mouse(original_x, original_y, Coordinate::Abs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let _ = window;
|
||||
let _ = is_chat_mode;
|
||||
}
|
||||
}
|
||||
|
||||
/// Log format:
|
||||
///
|
||||
/// ```text
|
||||
/// [time] [log level] [file module:line] message
|
||||
/// ```
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
///
|
||||
/// ```text
|
||||
/// [05-11 17:00:00] [INF] [coco_lib:625] Coco-AI started
|
||||
/// ```
|
||||
fn set_up_tauri_logger() -> TauriPlugin<tauri::Wry> {
|
||||
use log::Level;
|
||||
|
||||
fn format_log_level(level: Level) -> &'static str {
|
||||
match level {
|
||||
Level::Trace => "TRC",
|
||||
Level::Debug => "DBG",
|
||||
Level::Info => "INF",
|
||||
Level::Warn => "WAR",
|
||||
Level::Error => "ERR",
|
||||
}
|
||||
}
|
||||
|
||||
fn format_target_and_line(record: &log::Record) -> String {
|
||||
let mut str = record.target().to_string();
|
||||
if let Some(line) = record.line() {
|
||||
str.push(':');
|
||||
str.push_str(&line.to_string());
|
||||
}
|
||||
|
||||
str
|
||||
}
|
||||
|
||||
tauri_plugin_log::Builder::new()
|
||||
.format(|out, message, record| {
|
||||
let now = chrono::Local::now().format("%m-%d %H:%M:%S");
|
||||
let level = format_log_level(record.level());
|
||||
let target_and_line = format_target_and_line(record);
|
||||
out.finish(format_args!(
|
||||
"[{}] [{}] [{}] {}",
|
||||
now, level, target_and_line, message
|
||||
));
|
||||
})
|
||||
.level(log::LevelFilter::Debug)
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -1,313 +0,0 @@
|
||||
use crate::common::document::{DataSourceReference, Document};
|
||||
use crate::common::error::SearchError;
|
||||
use crate::common::search::{QueryResponse, QuerySource, SearchQuery};
|
||||
use crate::common::traits::SearchSource;
|
||||
use crate::local::LOCAL_QUERY_SOURCE_TYPE;
|
||||
use applications::App;
|
||||
use async_trait::async_trait;
|
||||
use fuzzy_prefix_search::Trie;
|
||||
use std::collections::HashMap;
|
||||
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")]
|
||||
return vec![
|
||||
"/Applications".into(),
|
||||
"/System/Applications".into(),
|
||||
"/System/Library/CoreServices".into(),
|
||||
];
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let paths = applications::get_default_search_paths();
|
||||
let mut ret = Vec::with_capacity(paths.len());
|
||||
for search_path in paths {
|
||||
let path_string = search_path
|
||||
.into_os_string()
|
||||
.into_string()
|
||||
.expect("path should be UTF-8 encoded");
|
||||
|
||||
ret.push(path_string);
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to return `app`'s path.
|
||||
///
|
||||
/// * Windows: return the path to application's exe
|
||||
/// * macOS: return the path to the `.app` bundle
|
||||
/// * Linux: return the path to the `.desktop` file
|
||||
fn get_app_path(app: &App) -> PathBuf {
|
||||
if cfg!(target_os = "windows") {
|
||||
assert!(
|
||||
app.icon_path.is_some(),
|
||||
"we only accept Applications with icons"
|
||||
);
|
||||
app.app_path_exe
|
||||
.as_ref()
|
||||
.expect("icon is Some, exe path should be Some as well")
|
||||
.to_path_buf()
|
||||
} else {
|
||||
app.app_desktop_path.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to return `app`'s path.
|
||||
///
|
||||
/// * Windows/macOS: extract `app_path`'s file name and remove the file extension
|
||||
/// * Linux: return the name specified in `.desktop` file
|
||||
async fn get_app_name(app: &App) -> String {
|
||||
if cfg!(target_os = "linux") {
|
||||
app.name.clone()
|
||||
} else {
|
||||
let app_path = get_app_path(app);
|
||||
name(app_path.clone()).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to return an absolute path to `app`'s icon.
|
||||
///
|
||||
/// On macOS/Windows, we cache icons in our data directory using the `icon()` function.
|
||||
async fn get_app_icon_path<R: Runtime>(
|
||||
tauri_app_handle: &AppHandle<R>,
|
||||
app: &App,
|
||||
) -> Result<PathBuf, String> {
|
||||
if cfg!(target_os = "linux") {
|
||||
let icon_path = app
|
||||
.icon_path
|
||||
.as_ref()
|
||||
.expect("We only accept applications with icons")
|
||||
.to_path_buf();
|
||||
|
||||
Ok(icon_path)
|
||||
} else {
|
||||
let app_path = get_app_path(app);
|
||||
let options = IconOptions {
|
||||
size: Some(256),
|
||||
save_path: None,
|
||||
};
|
||||
|
||||
icon(tauri_app_handle.clone(), app_path, Some(options))
|
||||
.await
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Return all the Apps found under `search_path`.
|
||||
///
|
||||
/// Note: apps with no icons will be filtered out.
|
||||
fn list_app_in(search_path: Vec<String>) -> Result<Vec<App>, String> {
|
||||
let search_path = search_path
|
||||
.into_iter()
|
||||
.map(PathBuf::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let apps = applications::get_all_apps(&search_path).map_err(|err| err.to_string())?;
|
||||
|
||||
Ok(apps
|
||||
.into_iter()
|
||||
.filter(|app| app.icon_path.is_some())
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppMetadata {
|
||||
name: String,
|
||||
r#where: PathBuf,
|
||||
size: u64,
|
||||
icon: PathBuf,
|
||||
created: u128,
|
||||
modified: u128,
|
||||
last_opened: u128,
|
||||
}
|
||||
|
||||
/// List apps that are in the `search_path`.
|
||||
///
|
||||
/// Different from `list_app_in()`, every app is JSON object containing its metadata, e.g.:
|
||||
///
|
||||
/// ```json
|
||||
/// {
|
||||
/// "name": "Finder",
|
||||
/// "where": "/System/Library/CoreServices",
|
||||
/// "size": 49283072,
|
||||
/// "icon": "/xxx.png",
|
||||
/// "created": 1744625204,
|
||||
/// "modified": 1744625204,
|
||||
/// "lastOpened": 1744625250
|
||||
/// }
|
||||
/// ```
|
||||
#[tauri::command]
|
||||
pub async fn list_app_with_metadata_in<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
search_path: Vec<String>,
|
||||
) -> Result<Vec<AppMetadata>, String> {
|
||||
let apps = list_app_in(search_path)?;
|
||||
|
||||
let mut apps_with_meta = Vec::with_capacity(apps.len());
|
||||
|
||||
// name version where Type(hardcoded Application) Size Created Modify
|
||||
for app in apps.iter() {
|
||||
let app_path = get_app_path(app);
|
||||
let app_name = get_app_name(app).await;
|
||||
let app_path_where = {
|
||||
let mut app_path_clone = app_path.clone();
|
||||
let truncated = app_path_clone.pop();
|
||||
if !truncated {
|
||||
panic!("every app file should live somewhere");
|
||||
}
|
||||
|
||||
app_path_clone
|
||||
};
|
||||
let icon = get_app_icon_path(&app_handle, app).await?;
|
||||
|
||||
let raw_app_metadata = metadata(app_path.clone(), None).await?;
|
||||
|
||||
let app_metadata = AppMetadata {
|
||||
name: app_name,
|
||||
r#where: app_path_where,
|
||||
size: raw_app_metadata.size,
|
||||
icon,
|
||||
created: raw_app_metadata.created_at,
|
||||
modified: raw_app_metadata.modified_at,
|
||||
last_opened: raw_app_metadata.accessed_at,
|
||||
};
|
||||
|
||||
apps_with_meta.push(app_metadata);
|
||||
}
|
||||
|
||||
Ok(apps_with_meta)
|
||||
}
|
||||
|
||||
pub struct ApplicationSearchSource {
|
||||
base_score: f64,
|
||||
// app name -> app icon path
|
||||
icons: HashMap<String, PathBuf>,
|
||||
application_paths: Trie<PathBuf>,
|
||||
}
|
||||
|
||||
impl ApplicationSearchSource {
|
||||
pub async fn new<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
base_score: f64,
|
||||
) -> Result<Self, String> {
|
||||
let application_paths = Trie::new();
|
||||
let mut icons = HashMap::new();
|
||||
|
||||
let default_search_path = get_default_search_paths();
|
||||
let apps = list_app_in(default_search_path)?;
|
||||
|
||||
for app in &apps {
|
||||
let app_path = get_app_path(app);
|
||||
let app_name = get_app_name(app).await;
|
||||
let app_icon_path = get_app_icon_path(&app_handle, app).await?;
|
||||
|
||||
if app_name.is_empty() || app_name.eq("Coco-AI") {
|
||||
continue;
|
||||
}
|
||||
|
||||
application_paths.insert(&app_name, app_path);
|
||||
icons.insert(app_name, app_icon_path);
|
||||
}
|
||||
|
||||
Ok(ApplicationSearchSource {
|
||||
base_score,
|
||||
icons,
|
||||
application_paths,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SearchSource for ApplicationSearchSource {
|
||||
fn get_type(&self) -> QuerySource {
|
||||
QuerySource {
|
||||
r#type: LOCAL_QUERY_SOURCE_TYPE.into(),
|
||||
name: hostname::get()
|
||||
.unwrap_or("My Computer".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_lowercase();
|
||||
|
||||
if query_string.is_empty() {
|
||||
return Ok(QueryResponse {
|
||||
source: self.get_type(),
|
||||
hits: Vec::new(),
|
||||
total_hits: 0,
|
||||
});
|
||||
}
|
||||
|
||||
let mut total_hits = 0;
|
||||
let mut hits = Vec::new();
|
||||
|
||||
let query_string_len = query_string.len();
|
||||
let mut results = self
|
||||
.application_paths
|
||||
.search_within_distance_scored(&query_string, query_string_len - 1);
|
||||
|
||||
// Check for NaN or extreme score values and handle them properly
|
||||
results.sort_by(|a, b| {
|
||||
// If either score is NaN, consider them equal (you can customize this logic as needed)
|
||||
if a.score.is_nan() || b.score.is_nan() {
|
||||
std::cmp::Ordering::Equal
|
||||
} else {
|
||||
// Otherwise, compare the scores as usual
|
||||
b.score
|
||||
.partial_cmp(&a.score)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
}
|
||||
});
|
||||
|
||||
if !results.is_empty() {
|
||||
for result in results {
|
||||
let app_name = result.word;
|
||||
let app_path = result.data.first().unwrap().clone();
|
||||
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(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()) {
|
||||
doc.icon = Some(icon_path.as_os_str().to_str().unwrap().to_string());
|
||||
}
|
||||
|
||||
hits.push((doc, self.base_score + result.score as f64));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(QueryResponse {
|
||||
source: self.get_type(),
|
||||
hits,
|
||||
total_hits,
|
||||
})
|
||||
}
|
||||
}
|
||||
38
src-tauri/src/local/application/mod.rs
Normal file
38
src-tauri/src/local/application/mod.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use serde::Serialize;
|
||||
|
||||
#[cfg(feature = "use_pizza_engine")]
|
||||
mod with_feature;
|
||||
|
||||
#[cfg(not(feature = "use_pizza_engine"))]
|
||||
mod without_feature;
|
||||
|
||||
#[cfg(feature = "use_pizza_engine")]
|
||||
pub use with_feature::*;
|
||||
|
||||
#[cfg(not(feature = "use_pizza_engine"))]
|
||||
pub use without_feature::*;
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppEntry {
|
||||
path: String,
|
||||
name: String,
|
||||
icon_path: String,
|
||||
alias: String,
|
||||
hotkey: String,
|
||||
is_disabled: bool,
|
||||
}
|
||||
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppMetadata {
|
||||
name: String,
|
||||
r#where: String,
|
||||
size: u64,
|
||||
icon: String,
|
||||
created: u128,
|
||||
modified: u128,
|
||||
last_opened: u128,
|
||||
}
|
||||
1086
src-tauri/src/local/application/with_feature.rs
Normal file
1086
src-tauri/src/local/application/with_feature.rs
Normal file
File diff suppressed because it is too large
Load Diff
121
src-tauri/src/local/application/without_feature.rs
Normal file
121
src-tauri/src/local/application/without_feature.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use crate::common::error::SearchError;
|
||||
use crate::common::search::{QueryResponse, QuerySource, SearchQuery};
|
||||
use crate::common::traits::SearchSource;
|
||||
use crate::local::LOCAL_QUERY_SOURCE_TYPE;
|
||||
use async_trait::async_trait;
|
||||
use tauri::{AppHandle, Runtime};
|
||||
use super::AppEntry;
|
||||
use super::AppMetadata;
|
||||
|
||||
pub(crate) const QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME: &str = "Applications";
|
||||
|
||||
pub struct ApplicationSearchSource;
|
||||
|
||||
impl ApplicationSearchSource {
|
||||
pub async fn init<R: Runtime>(_app_handle: AppHandle<R>) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SearchSource for ApplicationSearchSource {
|
||||
fn get_type(&self) -> QuerySource {
|
||||
QuerySource {
|
||||
r#type: LOCAL_QUERY_SOURCE_TYPE.into(),
|
||||
name: hostname::get()
|
||||
.unwrap_or("My Computer".into())
|
||||
.to_string_lossy()
|
||||
.into(),
|
||||
id: QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME.into(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn search(&self, _query: SearchQuery) -> Result<QueryResponse, SearchError> {
|
||||
Ok(QueryResponse {
|
||||
source: self.get_type(),
|
||||
hits: Vec::new(),
|
||||
total_hits: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_app_alias(_app_path: String, _alias: String) -> Result<(), String> {
|
||||
unreachable!("app list should be empty, there is no way this can be invoked")
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn register_app_hotkey<R: Runtime>(
|
||||
_tauri_app_handle: AppHandle<R>,
|
||||
_app_path: String,
|
||||
_hotkey: String,
|
||||
) -> Result<(), String> {
|
||||
unreachable!("app list should be empty, there is no way this can be invoked")
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn unregister_app_hotkey<R: Runtime>(
|
||||
_tauri_app_handle: AppHandle<R>,
|
||||
_app_path: String,
|
||||
) -> Result<(), String> {
|
||||
unreachable!("app list should be empty, there is no way this can be invoked")
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn disable_app_search<R: Runtime>(
|
||||
_tauri_app_handle: AppHandle<R>,
|
||||
_app_path: String,
|
||||
) -> Result<(), String> {
|
||||
// no-op
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn enable_app_search<R: Runtime>(
|
||||
_tauri_app_handle: AppHandle<R>,
|
||||
_app_path: String,
|
||||
) -> Result<(), String> {
|
||||
// no-op
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn add_app_search_path<R: Runtime>(
|
||||
_tauri_app_handle: AppHandle<R>,
|
||||
_search_path: String,
|
||||
) -> Result<(), String> {
|
||||
// no-op
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn remove_app_search_path<R: Runtime>(
|
||||
_tauri_app_handle: AppHandle<R>,
|
||||
_search_path: String,
|
||||
) -> Result<(), String> {
|
||||
// no-op
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_app_search_path<R: Runtime>(_tauri_app_handle: AppHandle<R>) -> Vec<String> {
|
||||
// Return an empty list
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_app_list<R: Runtime>(
|
||||
_tauri_app_handle: AppHandle<R>,
|
||||
) -> Result<Vec<AppEntry>, String> {
|
||||
// Return an empty list
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_app_metadata<R: Runtime>(
|
||||
_tauri_app_handle: AppHandle<R>,
|
||||
_app_path: String,
|
||||
) -> Result<AppMetadata, String> {
|
||||
unreachable!("app list should be empty, there is no way this can be invoked")
|
||||
}
|
||||
@@ -11,7 +11,7 @@ use num2words::Num2Words;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
const DATA_SOURCE_ID: &str = "Calculator";
|
||||
pub(crate) const DATA_SOURCE_ID: &str = "Calculator";
|
||||
|
||||
pub struct CalculatorSource {
|
||||
base_score: f64,
|
||||
|
||||
@@ -2,4 +2,163 @@ pub mod application;
|
||||
pub mod calculator;
|
||||
pub mod file_system;
|
||||
|
||||
use std::any::Any;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use crate::common::register::SearchSourceRegistry;
|
||||
use serde_json::Value as Json;
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
use tauri_plugin_store::StoreExt;
|
||||
|
||||
pub const LOCAL_QUERY_SOURCE_TYPE: &str = "local";
|
||||
pub const TAURI_STORE_LOCAL_QUERY_SOURCE_ENABLED_STATE: &str = "local_query_source_enabled_state";
|
||||
|
||||
trait SearchSourceState {
|
||||
#[cfg_attr(not(feature = "use_pizza_engine"), allow(unused))]
|
||||
fn as_mut_any(&mut self) -> &mut dyn Any;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
trait Task: Send + Sync {
|
||||
fn search_source_id(&self) -> &'static str;
|
||||
|
||||
async fn exec(&mut self, state: &mut Option<Box<dyn SearchSourceState>>);
|
||||
}
|
||||
|
||||
static RUNTIME_TX: OnceLock<tokio::sync::mpsc::UnboundedSender<Box<dyn Task>>> = OnceLock::new();
|
||||
|
||||
pub(crate) fn start_pizza_engine_runtime() {
|
||||
std::thread::spawn(|| {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
let main = async {
|
||||
let mut states: HashMap<String, Option<Box<dyn SearchSourceState>>> = HashMap::new();
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
RUNTIME_TX.set(tx).unwrap();
|
||||
|
||||
while let Some(mut task) = rx.recv().await {
|
||||
let opt_search_source_state = match states.entry(task.search_source_id().into()) {
|
||||
Entry::Occupied(o) => o.into_mut(),
|
||||
Entry::Vacant(v) => v.insert(None),
|
||||
};
|
||||
task.exec(opt_search_source_state).await;
|
||||
}
|
||||
};
|
||||
|
||||
rt.block_on(main);
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) async fn init_local_search_source<R: Runtime>(
|
||||
app_handle: &AppHandle<R>,
|
||||
) -> Result<(), String> {
|
||||
let enabled_status_store = app_handle
|
||||
.store(TAURI_STORE_LOCAL_QUERY_SOURCE_ENABLED_STATE)
|
||||
.map_err(|e| e.to_string())?;
|
||||
if enabled_status_store.is_empty() {
|
||||
enabled_status_store.set(
|
||||
application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME,
|
||||
Json::Bool(true),
|
||||
);
|
||||
enabled_status_store.set(calculator::DATA_SOURCE_ID, Json::Bool(true));
|
||||
}
|
||||
let registry = app_handle.state::<SearchSourceRegistry>();
|
||||
|
||||
application::ApplicationSearchSource::init(app_handle.clone()).await?;
|
||||
|
||||
for (id, enabled) in enabled_status_store.entries() {
|
||||
let enabled = match enabled {
|
||||
Json::Bool(b) => b,
|
||||
_ => unreachable!("enabled state should be stored as a boolean"),
|
||||
};
|
||||
|
||||
if enabled {
|
||||
if id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
||||
registry
|
||||
.register_source(application::ApplicationSearchSource)
|
||||
.await;
|
||||
}
|
||||
|
||||
if id == calculator::DATA_SOURCE_ID {
|
||||
let calculator_search = calculator::CalculatorSource::new(2000f64);
|
||||
registry.register_source(calculator_search).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_disabled_local_query_sources<R: Runtime>(app_handle: AppHandle<R>) -> Vec<String> {
|
||||
let enabled_status_store = app_handle
|
||||
.store(TAURI_STORE_LOCAL_QUERY_SOURCE_ENABLED_STATE)
|
||||
.unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"tauri store [{}] should exist and be loaded, but that's not true due to error [{}]",
|
||||
TAURI_STORE_LOCAL_QUERY_SOURCE_ENABLED_STATE, e
|
||||
)
|
||||
});
|
||||
let mut disabled_local_query_sources = Vec::new();
|
||||
|
||||
for (id, enabled) in enabled_status_store.entries() {
|
||||
let enabled = match enabled {
|
||||
Json::Bool(b) => b,
|
||||
_ => unreachable!("enabled state should be stored as a boolean"),
|
||||
};
|
||||
|
||||
if !enabled {
|
||||
disabled_local_query_sources.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
disabled_local_query_sources
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn enable_local_query_source<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
query_source_id: String,
|
||||
) {
|
||||
let registry = app_handle.state::<SearchSourceRegistry>();
|
||||
if query_source_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
||||
let application_search = application::ApplicationSearchSource;
|
||||
registry.register_source(application_search).await;
|
||||
}
|
||||
if query_source_id == calculator::DATA_SOURCE_ID {
|
||||
let calculator_search = calculator::CalculatorSource::new(2000f64);
|
||||
registry.register_source(calculator_search).await;
|
||||
}
|
||||
|
||||
let enabled_status_store = app_handle
|
||||
.store(TAURI_STORE_LOCAL_QUERY_SOURCE_ENABLED_STATE)
|
||||
.unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"tauri store [{}] should exist and be loaded, but that's not true due to error [{}]",
|
||||
TAURI_STORE_LOCAL_QUERY_SOURCE_ENABLED_STATE, e
|
||||
)
|
||||
});
|
||||
enabled_status_store.set(query_source_id, Json::Bool(true));
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn disable_local_query_source<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
query_source_id: String,
|
||||
) {
|
||||
let registry = app_handle.state::<SearchSourceRegistry>();
|
||||
registry.remove_source(&query_source_id).await;
|
||||
|
||||
let enabled_status_store = app_handle
|
||||
.store(TAURI_STORE_LOCAL_QUERY_SOURCE_ENABLED_STATE)
|
||||
.unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"tauri store [{}] should exist and be loaded, but that's not true due to error [{}]",
|
||||
TAURI_STORE_LOCAL_QUERY_SOURCE_ENABLED_STATE, e
|
||||
)
|
||||
});
|
||||
enabled_status_store.set(query_source_id, Json::Bool(false));
|
||||
}
|
||||
|
||||
@@ -82,6 +82,18 @@ pub async fn query_coco_fusion<R: Runtime>(
|
||||
.push((query_hit, score));
|
||||
}
|
||||
}
|
||||
Ok(Ok(Err(err))) => {
|
||||
failed_requests.push(FailedRequest {
|
||||
source: QuerySource {
|
||||
r#type: "N/A".into(),
|
||||
name: "N/A".into(),
|
||||
id: "N/A".into(),
|
||||
},
|
||||
status: 0,
|
||||
error: Some(err.to_string()),
|
||||
reason: None,
|
||||
});
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
failed_requests.push(FailedRequest {
|
||||
source: QuerySource {
|
||||
@@ -95,7 +107,7 @@ pub async fn query_coco_fusion<R: Runtime>(
|
||||
});
|
||||
}
|
||||
// Timeout reached, skip this request
|
||||
Ok(_) => {
|
||||
_ => {
|
||||
failed_requests.push(FailedRequest {
|
||||
source: QuerySource {
|
||||
r#type: "N/A".into(),
|
||||
@@ -103,19 +115,7 @@ pub async fn query_coco_fusion<R: Runtime>(
|
||||
id: "N/A".into(),
|
||||
},
|
||||
status: 0,
|
||||
error: Some("Query source timed out".to_string()),
|
||||
reason: None,
|
||||
});
|
||||
}
|
||||
Err(_) => {
|
||||
failed_requests.push(FailedRequest {
|
||||
source: QuerySource {
|
||||
r#type: "N/A".into(),
|
||||
name: "N/A".into(),
|
||||
id: "N/A".into(),
|
||||
},
|
||||
status: 0,
|
||||
error: Some("Task panicked".to_string()),
|
||||
error: Some(format!("{:?}", &result)),
|
||||
reason: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,14 +40,14 @@ pub struct AttachmentHit {
|
||||
pub struct AttachmentHits {
|
||||
pub total: Value,
|
||||
pub max_score: Option<f64>,
|
||||
pub hits: Vec<AttachmentHit>,
|
||||
pub hits: Option<Vec<AttachmentHit>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GetAttachmentResponse {
|
||||
pub took: u32,
|
||||
pub timed_out: bool,
|
||||
pub _shards: Value,
|
||||
pub _shards: Option<Value>,
|
||||
pub hits: AttachmentHits,
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ pub async fn get_attachment(
|
||||
.map_err(|e| format!("Request error: {}", e))?;
|
||||
|
||||
let body = get_response_body_text(response).await?;
|
||||
|
||||
|
||||
serde_json::from_str::<GetAttachmentResponse>(&body)
|
||||
.map_err(|e| format!("Failed to parse attachment response: {}", e))
|
||||
}
|
||||
|
||||
@@ -34,6 +34,10 @@ pub async fn refresh_all_connectors<R: Runtime>(app_handle: &AppHandle<R>) -> Re
|
||||
// Collect all the tasks for fetching and refreshing connectors
|
||||
let mut server_map = HashMap::new();
|
||||
for server in servers {
|
||||
if !server.enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
// dbg!("start fetch connectors for server: {}", &server.id);
|
||||
let connectors = match get_connectors_by_server(app_handle.clone(), server.id.clone()).await
|
||||
{
|
||||
|
||||
@@ -32,7 +32,7 @@ pub fn save_datasource_to_cache(server_id: &str, datasources: Vec<DataSource>) {
|
||||
#[allow(dead_code)]
|
||||
pub fn get_datasources_from_cache(server_id: &str) -> Option<HashMap<String, DataSource>> {
|
||||
let cache = DATASOURCE_CACHE.read().unwrap(); // Acquire read lock
|
||||
// dbg!("cache: {:?}", &cache);
|
||||
// dbg!("cache: {:?}", &cache);
|
||||
let server_cache = cache.get(server_id)?; // Get the server's cache
|
||||
Some(server_cache.clone())
|
||||
}
|
||||
@@ -47,6 +47,10 @@ pub async fn refresh_all_datasources<R: Runtime>(_app_handle: &AppHandle<R>) ->
|
||||
for server in servers {
|
||||
// dbg!("fetch datasources for server: {}", &server.id);
|
||||
|
||||
if !server.enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Attempt to get datasources by server, and continue even if it fails
|
||||
let connectors = match datasource_search(server.id.as_str(), None).await {
|
||||
Ok(connectors) => {
|
||||
@@ -130,8 +134,8 @@ pub async fn datasource_search(
|
||||
None,
|
||||
Some(reqwest::Body::from(body.to_string())),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Error fetching datasource: {}", e))?;
|
||||
.await
|
||||
.map_err(|e| format!("Error fetching datasource: {}", e))?;
|
||||
|
||||
// Parse the search results from the response
|
||||
let datasources: Vec<DataSource> = parse_search_results(resp).await.map_err(|e| {
|
||||
@@ -186,8 +190,8 @@ pub async fn mcp_server_search(
|
||||
None,
|
||||
Some(reqwest::Body::from(body.to_string())),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Error fetching datasource: {}", e))?;
|
||||
.await
|
||||
.map_err(|e| format!("Error fetching datasource: {}", e))?;
|
||||
|
||||
// Parse the search results from the response
|
||||
let mcp_server: Vec<DataSource> = parse_search_results(resp).await.map_err(|e| {
|
||||
|
||||
@@ -7,15 +7,24 @@ use std::time::Duration;
|
||||
use tauri_plugin_store::JsonValue;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
pub static HTTP_CLIENT: Lazy<Mutex<Client>> = Lazy::new(|| {
|
||||
let client = Client::builder()
|
||||
pub(crate) fn new_reqwest_http_client(accept_invalid_certs: bool) -> Client {
|
||||
Client::builder()
|
||||
.read_timeout(Duration::from_secs(3)) // Set a timeout of 3 second
|
||||
.connect_timeout(Duration::from_secs(3)) // Set a timeout of 3 second
|
||||
.timeout(Duration::from_secs(10)) // Set a timeout of 10 seconds
|
||||
.danger_accept_invalid_certs(true) // example for self-signed certificates
|
||||
.danger_accept_invalid_certs(accept_invalid_certs) // allow self-signed certificates
|
||||
.build()
|
||||
.expect("Failed to build client");
|
||||
Mutex::new(client)
|
||||
.expect("Failed to build client")
|
||||
}
|
||||
|
||||
pub static HTTP_CLIENT: Lazy<Mutex<Client>> = Lazy::new(|| {
|
||||
let allow_self_signature = crate::settings::_get_allow_self_signature(
|
||||
crate::GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app store not set")
|
||||
.clone(),
|
||||
);
|
||||
Mutex::new(new_reqwest_http_client(allow_self_signature))
|
||||
});
|
||||
|
||||
pub struct HttpClient;
|
||||
@@ -35,6 +44,14 @@ impl HttpClient {
|
||||
headers: Option<HashMap<String, String>>,
|
||||
body: Option<reqwest::Body>,
|
||||
) -> Result<reqwest::Response, String> {
|
||||
log::debug!(
|
||||
"Sending Request: {}, query_params: {:?}, header: {:?}, body: {:?}",
|
||||
&url,
|
||||
&query_params,
|
||||
&headers,
|
||||
&body
|
||||
);
|
||||
|
||||
let request_builder =
|
||||
Self::get_request_builder(method, url, headers, query_params, body).await;
|
||||
|
||||
@@ -42,6 +59,14 @@ impl HttpClient {
|
||||
dbg!("Failed to send request: {}", &e);
|
||||
format!("Failed to send request: {}", e)
|
||||
})?;
|
||||
|
||||
log::debug!(
|
||||
"Request: {}, Response status: {:?}, header: {:?}",
|
||||
&url,
|
||||
&response.status(),
|
||||
&response.headers()
|
||||
);
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
@@ -140,9 +165,12 @@ impl HttpClient {
|
||||
headers.insert("X-API-TOKEN".to_string(), t);
|
||||
}
|
||||
|
||||
// dbg!(&server_id);
|
||||
// dbg!(&url);
|
||||
// dbg!(&headers);
|
||||
log::debug!(
|
||||
"Sending request to server: {}, url: {}, headers: {:?}",
|
||||
&server_id,
|
||||
&url,
|
||||
&headers
|
||||
);
|
||||
|
||||
Self::send_raw_request(method, &url, query_params, Some(headers), body).await
|
||||
} else {
|
||||
@@ -184,7 +212,7 @@ impl HttpClient {
|
||||
query_params,
|
||||
body,
|
||||
)
|
||||
.await
|
||||
.await
|
||||
}
|
||||
|
||||
// Convenience method for PUT requests
|
||||
@@ -204,7 +232,7 @@ impl HttpClient {
|
||||
query_params,
|
||||
body,
|
||||
)
|
||||
.await
|
||||
.await
|
||||
}
|
||||
|
||||
// Convenience method for DELETE requests
|
||||
@@ -223,6 +251,6 @@ impl HttpClient {
|
||||
query_params,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,11 @@ use crate::common::search::{QueryHits, QueryResponse, QuerySource, SearchQuery,
|
||||
use crate::common::server::Server;
|
||||
use crate::common::traits::SearchSource;
|
||||
use crate::server::http_client::HttpClient;
|
||||
use crate::server::servers::get_server_token;
|
||||
use async_trait::async_trait;
|
||||
// use futures::stream::StreamExt;
|
||||
use ordered_float::OrderedFloat;
|
||||
use reqwest::{Client, Method, RequestBuilder};
|
||||
use std::collections::HashMap;
|
||||
use tauri_plugin_store::JsonValue;
|
||||
// use std::hash::Hash;
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -74,45 +73,11 @@ const COCO_SERVERS: &str = "coco-servers";
|
||||
|
||||
pub struct CocoSearchSource {
|
||||
server: Server,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl CocoSearchSource {
|
||||
pub fn new(server: Server, client: Client) -> Self {
|
||||
CocoSearchSource { server, client }
|
||||
}
|
||||
|
||||
async fn build_request_from_query(
|
||||
&self,
|
||||
query: &SearchQuery,
|
||||
) -> Result<RequestBuilder, String> {
|
||||
self.build_request(query.from, query.size, &query.query_strings)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn build_request(
|
||||
&self,
|
||||
from: u64,
|
||||
size: u64,
|
||||
query_strings: &HashMap<String, String>,
|
||||
) -> Result<RequestBuilder, String> {
|
||||
let url = HttpClient::join_url(&self.server.endpoint, "/query/_search");
|
||||
let mut request_builder = self.client.request(Method::GET, url);
|
||||
|
||||
if !self.server.public {
|
||||
if let Some(token) = get_server_token(&self.server.id)
|
||||
.await?
|
||||
.map(|t| t.access_token)
|
||||
{
|
||||
request_builder = request_builder.header("X-API-TOKEN", token);
|
||||
}
|
||||
}
|
||||
|
||||
let result = request_builder
|
||||
.query(&[("from", &from.to_string()), ("size", &size.to_string())])
|
||||
.query(query_strings);
|
||||
|
||||
Ok(result)
|
||||
pub fn new(server: Server) -> Self {
|
||||
CocoSearchSource { server }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,17 +92,22 @@ impl SearchSource for CocoSearchSource {
|
||||
}
|
||||
|
||||
async fn search(&self, query: SearchQuery) -> Result<QueryResponse, SearchError> {
|
||||
// Build the request from the provided query
|
||||
let request_builder = self
|
||||
.build_request_from_query(&query)
|
||||
.await
|
||||
.map_err(|e| SearchError::InternalError(e.to_string()))?;
|
||||
let url = "/query/_search";
|
||||
|
||||
// Send the HTTP request and handle errors
|
||||
let response = request_builder
|
||||
.send()
|
||||
let mut query_args: HashMap<String, JsonValue> = HashMap::new();
|
||||
query_args.insert("from".into(), JsonValue::Number(query.from.into()));
|
||||
query_args.insert("size".into(), JsonValue::Number(query.size.into()));
|
||||
for (key, value) in query.query_strings {
|
||||
query_args.insert(key, JsonValue::String(value));
|
||||
}
|
||||
|
||||
let response = HttpClient::get(
|
||||
&self.server.id,
|
||||
&url,
|
||||
Some(query_args),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| SearchError::HttpError(format!("Failed to send search request: {}", e)))?;
|
||||
.map_err(|e| SearchError::HttpError(format!("Error to send search request: {}", e)))?;
|
||||
|
||||
// Use the helper function to parse the response body
|
||||
let response_body = get_response_body_text(response)
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::server::http_client::HttpClient;
|
||||
use crate::server::search::CocoSearchSource;
|
||||
use crate::COCO_TAURI_STORE;
|
||||
use lazy_static::lazy_static;
|
||||
use reqwest::{Client, Method};
|
||||
use reqwest::Method;
|
||||
use serde_json::from_value;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::collections::HashMap;
|
||||
@@ -447,7 +447,7 @@ pub async fn try_register_server_to_search_source(
|
||||
) {
|
||||
if server.enabled {
|
||||
let registry = app_handle.state::<SearchSourceRegistry>();
|
||||
let source = CocoSearchSource::new(server.clone(), Client::new());
|
||||
let source = CocoSearchSource::new(server.clone());
|
||||
registry.register_source(source).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ use crate::server::servers::{get_server_by_id, get_server_token};
|
||||
use futures::StreamExt;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tauri::{AppHandle, Emitter, Runtime};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_tungstenite::MaybeTlsStream;
|
||||
use tokio_tungstenite::WebSocketStream;
|
||||
use tokio_tungstenite::{connect_async, MaybeTlsStream};
|
||||
|
||||
use tokio_tungstenite::{connect_async_tls_with_config, Connector};
|
||||
#[derive(Default)]
|
||||
pub struct WebSocketManager {
|
||||
connections: Arc<Mutex<HashMap<String, Arc<WebSocketInstance>>>>,
|
||||
@@ -22,9 +22,15 @@ struct WebSocketInstance {
|
||||
|
||||
fn convert_to_websocket(endpoint: &str) -> Result<String, String> {
|
||||
let url = url::Url::parse(endpoint).map_err(|e| format!("Invalid URL: {}", e))?;
|
||||
let ws_protocol = if url.scheme() == "https" { "wss://" } else { "ws://" };
|
||||
let ws_protocol = if url.scheme() == "https" {
|
||||
"wss://"
|
||||
} else {
|
||||
"ws://"
|
||||
};
|
||||
let host = url.host_str().ok_or("No host found in URL")?;
|
||||
let port = url.port_or_known_default().unwrap_or(if url.scheme() == "https" { 443 } else { 80 });
|
||||
let port = url
|
||||
.port_or_known_default()
|
||||
.unwrap_or(if url.scheme() == "https" { 443 } else { 80 });
|
||||
|
||||
let ws_endpoint = if port == 80 || port == 443 {
|
||||
format!("{}{}{}", ws_protocol, host, "/ws")
|
||||
@@ -35,7 +41,8 @@ fn convert_to_websocket(endpoint: &str) -> Result<String, String> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn connect_to_server(
|
||||
pub async fn connect_to_server<R: Runtime>(
|
||||
tauri_app_handle: AppHandle<R>,
|
||||
id: String,
|
||||
client_id: String,
|
||||
state: tauri::State<'_, WebSocketManager>,
|
||||
@@ -54,16 +61,43 @@ pub async fn connect_to_server(
|
||||
tokio_tungstenite::tungstenite::client::IntoClientRequest::into_client_request(&endpoint)
|
||||
.map_err(|e| format!("Failed to create WebSocket request: {}", e))?;
|
||||
|
||||
request.headers_mut().insert("Connection", "Upgrade".parse().unwrap());
|
||||
request.headers_mut().insert("Upgrade", "websocket".parse().unwrap());
|
||||
request.headers_mut().insert("Sec-WebSocket-Version", "13".parse().unwrap());
|
||||
request.headers_mut().insert("Sec-WebSocket-Key", generate_key().parse().unwrap());
|
||||
request
|
||||
.headers_mut()
|
||||
.insert("Connection", "Upgrade".parse().unwrap());
|
||||
request
|
||||
.headers_mut()
|
||||
.insert("Upgrade", "websocket".parse().unwrap());
|
||||
request
|
||||
.headers_mut()
|
||||
.insert("Sec-WebSocket-Version", "13".parse().unwrap());
|
||||
request
|
||||
.headers_mut()
|
||||
.insert("Sec-WebSocket-Key", generate_key().parse().unwrap());
|
||||
|
||||
if let Some(token) = token {
|
||||
request.headers_mut().insert("X-API-TOKEN", token.parse().unwrap());
|
||||
request
|
||||
.headers_mut()
|
||||
.insert("X-API-TOKEN", token.parse().unwrap());
|
||||
}
|
||||
|
||||
let (ws_stream, _) = connect_async(request).await.map_err(|e| format!("WebSocket error: {:?}", e))?;
|
||||
let allow_self_signature =
|
||||
crate::settings::get_allow_self_signature(tauri_app_handle.clone()).await;
|
||||
let tls_connector = tokio_native_tls::native_tls::TlsConnector::builder()
|
||||
.danger_accept_invalid_certs(allow_self_signature)
|
||||
.build()
|
||||
.map_err(|e| format!("TLS build error: {:?}", e))?;
|
||||
|
||||
let connector = Connector::NativeTls(tls_connector.into());
|
||||
|
||||
let (ws_stream, _) = connect_async_tls_with_config(
|
||||
request,
|
||||
None, // WebSocketConfig
|
||||
true, // disable_nagle
|
||||
Some(connector), // Connector
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("WebSocket TLS error: {:?}", e))?;
|
||||
|
||||
let (cancel_tx, mut cancel_rx) = mpsc::channel(1);
|
||||
|
||||
let instance = Arc::new(WebSocketInstance {
|
||||
@@ -112,9 +146,11 @@ pub async fn connect_to_server(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn disconnect(client_id: String, state: tauri::State<'_, WebSocketManager>) -> Result<(), String> {
|
||||
pub async fn disconnect(
|
||||
client_id: String,
|
||||
state: tauri::State<'_, WebSocketManager>,
|
||||
) -> Result<(), String> {
|
||||
let instance = {
|
||||
let mut connections = state.connections.lock().await;
|
||||
connections.remove(&client_id)
|
||||
@@ -129,4 +165,4 @@ pub async fn disconnect(client_id: String, state: tauri::State<'_, WebSocketMana
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
72
src-tauri/src/settings.rs
Normal file
72
src-tauri/src/settings.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use crate::COCO_TAURI_STORE;
|
||||
use serde_json::Value as Json;
|
||||
use tauri::{AppHandle, Runtime};
|
||||
use tauri_plugin_store::StoreExt;
|
||||
|
||||
const SETTINGS_ALLOW_SELF_SIGNATURE: &str = "settings_allow_self_signature";
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_allow_self_signature<R: Runtime>(tauri_app_handle: AppHandle<R>, value: bool) {
|
||||
use crate::server::http_client;
|
||||
|
||||
let store = tauri_app_handle
|
||||
.store(COCO_TAURI_STORE)
|
||||
.unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"store [{}] not found/loaded, error [{}]",
|
||||
COCO_TAURI_STORE, e
|
||||
)
|
||||
});
|
||||
|
||||
let old_value = match store
|
||||
.get(SETTINGS_ALLOW_SELF_SIGNATURE)
|
||||
.expect("should be initialized upon first get call")
|
||||
{
|
||||
Json::Bool(b) => b,
|
||||
_ => unreachable!(
|
||||
"{} should be stored in a boolean",
|
||||
SETTINGS_ALLOW_SELF_SIGNATURE
|
||||
),
|
||||
};
|
||||
|
||||
if old_value == value {
|
||||
return;
|
||||
}
|
||||
|
||||
store.set(SETTINGS_ALLOW_SELF_SIGNATURE, value);
|
||||
|
||||
let mut guard = http_client::HTTP_CLIENT.lock().await;
|
||||
*guard = http_client::new_reqwest_http_client(value)
|
||||
}
|
||||
|
||||
/// Synchronous version of `async get_allow_self_signature()`.
|
||||
pub fn _get_allow_self_signature<R: Runtime>(tauri_app_handle: AppHandle<R>) -> bool {
|
||||
let store = tauri_app_handle
|
||||
.store(COCO_TAURI_STORE)
|
||||
.unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"store [{}] not found/loaded, error [{}]",
|
||||
COCO_TAURI_STORE, e
|
||||
)
|
||||
});
|
||||
if !store.has(SETTINGS_ALLOW_SELF_SIGNATURE) {
|
||||
// default to false
|
||||
store.set(SETTINGS_ALLOW_SELF_SIGNATURE, false);
|
||||
}
|
||||
|
||||
match store
|
||||
.get(SETTINGS_ALLOW_SELF_SIGNATURE)
|
||||
.expect("should be Some")
|
||||
{
|
||||
Json::Bool(b) => b,
|
||||
_ => unreachable!(
|
||||
"{} should be stored in a boolean",
|
||||
SETTINGS_ALLOW_SELF_SIGNATURE
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_allow_self_signature<R: Runtime>(tauri_app_handle: AppHandle<R>) -> bool {
|
||||
_get_allow_self_signature(tauri_app_handle)
|
||||
}
|
||||
@@ -20,7 +20,7 @@ pub use linux::*;
|
||||
|
||||
pub fn default(app: &mut App, main_window: WebviewWindow, settings_window: WebviewWindow) {
|
||||
// Development mode automatically opens the console: https://tauri.app/develop/debug
|
||||
#[cfg(any(dev, debug_assertions))]
|
||||
#[cfg(all(dev, debug_assertions))]
|
||||
main_window.open_devtools();
|
||||
|
||||
platform(app, main_window.clone(), settings_window.clone());
|
||||
|
||||
@@ -59,6 +59,7 @@ export const handleApiError = (error: any) => {
|
||||
message = error.message;
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
addError(message, "error");
|
||||
return error;
|
||||
};
|
||||
|
||||
@@ -28,10 +28,10 @@ async function invokeWithErrorHandler<T>(
|
||||
|
||||
if (result && typeof result === "object" && "failed" in result) {
|
||||
const failedResult = result as any;
|
||||
if (failedResult.failed?.length > 0) {
|
||||
if (failedResult.failed?.length > 0 && failedResult?.hits?.length == 0) {
|
||||
failedResult.failed.forEach((error: any) => {
|
||||
// addError(error.error, 'error');
|
||||
console.error(error.error);
|
||||
addError(error.error, 'error');
|
||||
// console.error(error.error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
||||
const aiAssistant = useShortcutsStore((state) => state.aiAssistant);
|
||||
const [assistants, setAssistants] = useState<any[]>([]);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const popoverRef = useRef<HTMLDivElement>(null);
|
||||
const popoverButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const [keyword, setKeyword] = useState("");
|
||||
@@ -195,25 +196,41 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
||||
setTimeout(() => setIsRefreshing(false), 1000);
|
||||
};
|
||||
|
||||
useKeyPress(["uparrow", "downarrow"], (_, key) => {
|
||||
const isClose = isNil(popoverButtonRef.current?.dataset["open"]);
|
||||
const index = assistants.findIndex(
|
||||
(item) => item._id === currentAssistant?._id
|
||||
);
|
||||
const length = assistants.length;
|
||||
useKeyPress(
|
||||
["uparrow", "downarrow", "enter"],
|
||||
(event, key) => {
|
||||
const isClose = isNil(popoverButtonRef.current?.dataset["open"]);
|
||||
|
||||
if (isClose || length <= 1) return;
|
||||
if (isClose) return;
|
||||
|
||||
let nextIndex = index;
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
if (key === "uparrow") {
|
||||
nextIndex = index > 0 ? index - 1 : length - 1;
|
||||
} else {
|
||||
nextIndex = index < length - 1 ? index + 1 : 0;
|
||||
if (key === "enter") {
|
||||
return popoverButtonRef.current?.click();
|
||||
}
|
||||
|
||||
const index = assistants.findIndex(
|
||||
(item) => item._id === currentAssistant?._id
|
||||
);
|
||||
const length = assistants.length;
|
||||
|
||||
if (length <= 1) return;
|
||||
|
||||
let nextIndex = index;
|
||||
|
||||
if (key === "uparrow") {
|
||||
nextIndex = index > 0 ? index - 1 : length - 1;
|
||||
} else {
|
||||
nextIndex = index < length - 1 ? index + 1 : 0;
|
||||
}
|
||||
|
||||
setCurrentAssistant(assistants[nextIndex]);
|
||||
},
|
||||
{
|
||||
target: popoverRef,
|
||||
}
|
||||
|
||||
setCurrentAssistant(assistants[nextIndex]);
|
||||
});
|
||||
);
|
||||
|
||||
const handlePrev = useCallback(() => {
|
||||
if (pagination.current <= 1) return;
|
||||
@@ -231,7 +248,7 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Popover>
|
||||
<Popover ref={popoverRef}>
|
||||
<PopoverButton
|
||||
ref={popoverButtonRef}
|
||||
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"
|
||||
@@ -326,6 +343,7 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
|
||||
)}
|
||||
onClick={() => {
|
||||
setCurrentAssistant(assistant);
|
||||
popoverButtonRef.current?.click();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-center size-6 bg-white border border-[#E6E6E6] rounded-full overflow-hidden">
|
||||
|
||||
@@ -129,7 +129,7 @@ const ChatAI = memo(
|
||||
const dealMsgRef = useRef<((msg: string) => void) | null>(null);
|
||||
|
||||
const clientId = isChatPage ? "standalone" : "popup";
|
||||
const { reconnect, disconnectWS, updateDealMsg } = useWebSocket({
|
||||
const { reconnect, updateDealMsg } = useWebSocket({
|
||||
clientId,
|
||||
connected,
|
||||
setConnected,
|
||||
@@ -164,10 +164,10 @@ const ChatAI = memo(
|
||||
isMCPActive,
|
||||
changeInput,
|
||||
websocketSessionId,
|
||||
showChatHistory,
|
||||
showChatHistory
|
||||
);
|
||||
|
||||
const { dealMsg, messageTimeoutRef } = useMessageHandler(
|
||||
const { dealMsg } = useMessageHandler(
|
||||
curIdRef,
|
||||
setCurChatEnd,
|
||||
setTimedoutShow,
|
||||
@@ -184,24 +184,18 @@ const ChatAI = memo(
|
||||
}, [dealMsg, updateDealMsg]);
|
||||
|
||||
const clearChat = useCallback(() => {
|
||||
console.log("clearChat");
|
||||
//console.log("clearChat");
|
||||
setTimedoutShow(false);
|
||||
chatClose(activeChat);
|
||||
setActiveChat(undefined);
|
||||
setCurChatEnd(true);
|
||||
clearChatPage && clearChatPage();
|
||||
}, [
|
||||
activeChat,
|
||||
chatClose,
|
||||
clearChatPage,
|
||||
setCurChatEnd,
|
||||
setTimedoutShow,
|
||||
]);
|
||||
}, [activeChat, chatClose]);
|
||||
|
||||
const init = useCallback(
|
||||
async (value: string) => {
|
||||
try {
|
||||
console.log("init", isLogin, curChatEnd, activeChat?._id);
|
||||
//console.log("init", isLogin, curChatEnd, activeChat?._id);
|
||||
if (!isLogin) {
|
||||
addError("Please login to continue chatting");
|
||||
return;
|
||||
@@ -222,7 +216,7 @@ const ChatAI = memo(
|
||||
[
|
||||
isLogin,
|
||||
curChatEnd,
|
||||
activeChat,
|
||||
activeChat?._id,
|
||||
createNewChat,
|
||||
handleSendMessage,
|
||||
websocketSessionId,
|
||||
@@ -234,21 +228,6 @@ const ChatAI = memo(
|
||||
createChatWindow(createWin);
|
||||
}, [createChatWindow, createWin]);
|
||||
|
||||
useEffect(() => {
|
||||
setCurChatEnd(true);
|
||||
return () => {
|
||||
if (messageTimeoutRef.current) {
|
||||
clearTimeout(messageTimeoutRef.current);
|
||||
}
|
||||
Promise.resolve().then(() => {
|
||||
chatClose(activeChat);
|
||||
setActiveChat(undefined);
|
||||
setCurChatEnd(true);
|
||||
disconnectWS();
|
||||
});
|
||||
};
|
||||
}, [chatClose, setCurChatEnd]);
|
||||
|
||||
const onSelectChat = useCallback(
|
||||
async (chat: Chat) => {
|
||||
setTimedoutShow(false);
|
||||
@@ -260,33 +239,28 @@ const ChatAI = memo(
|
||||
chatHistory(response);
|
||||
}
|
||||
},
|
||||
[
|
||||
clearAllChunkData,
|
||||
cancelChat,
|
||||
activeChat,
|
||||
chatClose,
|
||||
openSessionChat,
|
||||
chatHistory,
|
||||
]
|
||||
[cancelChat, activeChat, chatClose, openSessionChat, chatHistory]
|
||||
);
|
||||
|
||||
const deleteChat = useCallback(
|
||||
(chatId: string) => {
|
||||
handleDelete(chatId);
|
||||
|
||||
setChats((prev) => prev.filter((chat) => chat._id !== chatId));
|
||||
setChats((prev) => {
|
||||
const updatedChats = prev.filter((chat) => chat._id !== chatId);
|
||||
|
||||
if (activeChat?._id === chatId) {
|
||||
const remainingChats = chats.filter((chat) => chat._id !== chatId);
|
||||
|
||||
if (remainingChats.length > 0) {
|
||||
setActiveChat(remainingChats[0]);
|
||||
} else {
|
||||
init("");
|
||||
if (activeChat?._id === chatId) {
|
||||
if (updatedChats.length > 0) {
|
||||
setActiveChat(updatedChats[0]);
|
||||
} else {
|
||||
init("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedChats;
|
||||
});
|
||||
},
|
||||
[activeChat, chats, init, setActiveChat]
|
||||
[activeChat?._id, handleDelete, init]
|
||||
);
|
||||
|
||||
const handleOutsideClick = useCallback((e: MouseEvent) => {
|
||||
@@ -317,43 +291,38 @@ const ChatAI = memo(
|
||||
!isSidebarOpenChat && getChatHistory();
|
||||
}, [isSidebarOpenChat, setIsSidebarOpen, getChatHistory]);
|
||||
|
||||
const renameChat = (chatId: string, title: string) => {
|
||||
setChats((prev) => {
|
||||
const updatedChats = prev.map((item) => {
|
||||
if (item._id !== chatId) return item;
|
||||
const renameChat = useCallback(
|
||||
(chatId: string, title: string) => {
|
||||
setChats((prev) => {
|
||||
const chatIndex = prev.findIndex((chat) => chat._id === chatId);
|
||||
if (chatIndex === -1) return prev;
|
||||
|
||||
return { ...item, _source: { ...item._source, title } };
|
||||
const modifiedChat = {
|
||||
...prev[chatIndex],
|
||||
_source: { ...prev[chatIndex]._source, title },
|
||||
};
|
||||
|
||||
const result = [...prev];
|
||||
result.splice(chatIndex, 1);
|
||||
return [modifiedChat, ...result];
|
||||
});
|
||||
|
||||
const modifiedChat = updatedChats.find((item) => {
|
||||
return item._id === chatId;
|
||||
});
|
||||
|
||||
if (!modifiedChat) {
|
||||
return updatedChats;
|
||||
if (activeChat?._id === chatId) {
|
||||
setActiveChat((prev) => {
|
||||
if (!prev) return prev;
|
||||
return { ...prev, _source: { ...prev._source, title } };
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
modifiedChat,
|
||||
...updatedChats.filter((item) => item._id !== chatId),
|
||||
];
|
||||
});
|
||||
|
||||
if (activeChat?._id === chatId) {
|
||||
setActiveChat((prev) => {
|
||||
if (!prev) return prev;
|
||||
|
||||
return { ...prev, _source: { ...prev._source, title } };
|
||||
});
|
||||
}
|
||||
|
||||
handleRename(chatId, title);
|
||||
};
|
||||
handleRename(chatId, title);
|
||||
},
|
||||
[activeChat?._id, handleRename]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className={`h-full flex flex-col rounded-md relative`}
|
||||
className={`flex flex-col rounded-md relative h-full overflow-hidden`}
|
||||
>
|
||||
{showChatHistory && !setIsSidebarOpen && (
|
||||
<ChatSidebar
|
||||
@@ -382,6 +351,7 @@ const ChatAI = memo(
|
||||
showChatHistory={showChatHistory}
|
||||
assistantIDs={assistantIDs}
|
||||
/>
|
||||
|
||||
{isLogin ? (
|
||||
<ChatContent
|
||||
activeChat={activeChat}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useEffect } from "react";
|
||||
import { useRef, useEffect, UIEvent, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ChatMessage } from "@/components/ChatMessage";
|
||||
@@ -11,6 +11,8 @@ import type { Chat, IChunkData } from "./types";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import SessionFile from "./SessionFile";
|
||||
import Splash from "./Splash";
|
||||
import { ArrowDown } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface ChatContentProps {
|
||||
activeChat?: Chat;
|
||||
@@ -52,15 +54,16 @@ export const ChatContent = ({
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentSessionId(activeChat?._id);
|
||||
}, [activeChat]);
|
||||
}, [activeChat?._id]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const uploadFiles = useChatStore((state) => state.uploadFiles);
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { scrollToBottom } = useChatScroll(messagesEndRef);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [isAtBottom, setIsAtBottom] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
@@ -81,10 +84,25 @@ export const ChatContent = ({
|
||||
};
|
||||
}, [scrollToBottom]);
|
||||
|
||||
const allMessages = activeChat?.messages || [];
|
||||
|
||||
const handleScroll = (event: UIEvent<HTMLDivElement>) => {
|
||||
const { scrollHeight, scrollTop, clientHeight } =
|
||||
event.currentTarget as HTMLDivElement;
|
||||
|
||||
const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
|
||||
|
||||
setIsAtBottom(isAtBottom);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col h-full justify-between overflow-hidden">
|
||||
<div className="flex-1 w-full overflow-x-hidden overflow-y-auto border-t border-[rgba(0,0,0,0.1)] dark:border-[rgba(255,255,255,0.15)] custom-scrollbar relative">
|
||||
<Greetings />
|
||||
<div className="flex-1 overflow-hidden flex flex-col justify-between relative">
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="flex-1 w-full overflow-x-hidden overflow-y-auto border-t border-[rgba(0,0,0,0.1)] dark:border-[rgba(255,255,255,0.15)] custom-scrollbar relative"
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{(!activeChat || activeChat?.messages?.length === 0) && <Greetings />}
|
||||
|
||||
{activeChat?.messages?.map((message, index) => (
|
||||
<ChatMessage
|
||||
@@ -94,6 +112,7 @@ export const ChatContent = ({
|
||||
onResend={handleSendMessage}
|
||||
/>
|
||||
))}
|
||||
|
||||
{(!curChatEnd ||
|
||||
query_intent ||
|
||||
tools ||
|
||||
@@ -109,6 +128,8 @@ export const ChatContent = ({
|
||||
_id: "current",
|
||||
_source: {
|
||||
type: "assistant",
|
||||
assistant_id:
|
||||
allMessages[allMessages.length - 1]?._source?.assistant_id,
|
||||
message: "",
|
||||
question: Question,
|
||||
},
|
||||
@@ -125,6 +146,7 @@ export const ChatContent = ({
|
||||
loadingStep={loadingStep}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{timedoutShow ? (
|
||||
<ChatMessage
|
||||
key={"timedout"}
|
||||
@@ -152,6 +174,23 @@ export const ChatContent = ({
|
||||
{sessionId && <SessionFile sessionId={sessionId} />}
|
||||
|
||||
<Splash />
|
||||
|
||||
<button
|
||||
className={clsx(
|
||||
"absolute right-4 bottom-4 flex items-center justify-center size-8 border bg-white rounded-full shadow dark:border-[#272828] dark:bg-black dark:shadow-white/15",
|
||||
{
|
||||
hidden: isAtBottom,
|
||||
}
|
||||
)}
|
||||
onClick={() => {
|
||||
scrollRef.current?.scrollTo({
|
||||
top: scrollRef.current?.scrollHeight,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ArrowDown className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ interface ChatSidebarProps {
|
||||
// onNewChat: () => void;
|
||||
onSelectChat: (chat: any) => void;
|
||||
onDeleteChat: (chatId: string) => void;
|
||||
fetchChatHistory: () => Promise<void>;
|
||||
fetchChatHistory: () => void;
|
||||
onSearch: (keyword: string) => void;
|
||||
onRename: (chat: any, title: string) => void;
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
<div
|
||||
data-sidebar
|
||||
className={`
|
||||
h-[calc(100%+90px)] absolute top-0 left-0 z-10 w-64
|
||||
h-screen fixed top-0 left-0 z-100 w-64
|
||||
transform transition-all duration-300 ease-in-out
|
||||
${isSidebarOpen ? "translate-x-0" : "-translate-x-full"}
|
||||
bg-gray-100 dark:bg-gray-800
|
||||
|
||||
@@ -4,10 +4,11 @@ import { X } from "lucide-react";
|
||||
import { useAsyncEffect } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import { UploadFile, useChatStore } from "@/stores/chatStore";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import FileIcon from "../Common/Icons/FileIcon";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import Tooltip2 from "../Common/Tooltip2";
|
||||
|
||||
interface FileListProps {
|
||||
sessionId: string;
|
||||
@@ -39,29 +40,42 @@ const FileList = (props: FileListProps) => {
|
||||
|
||||
if (uploaded) continue;
|
||||
|
||||
const attachmentIds: any = await platformAdapter.commands(
|
||||
"upload_attachment",
|
||||
{
|
||||
serverId,
|
||||
sessionId,
|
||||
filePaths: [path],
|
||||
try {
|
||||
const attachmentIds: any = await platformAdapter.commands(
|
||||
"upload_attachment",
|
||||
{
|
||||
serverId,
|
||||
sessionId,
|
||||
filePaths: [path],
|
||||
}
|
||||
);
|
||||
|
||||
if (!attachmentIds) {
|
||||
throw new Error("Failed to get attachment id");
|
||||
} else {
|
||||
Object.assign(item, {
|
||||
uploaded: true,
|
||||
attachmentId: attachmentIds[0],
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
if (!attachmentIds) continue;
|
||||
|
||||
Object.assign(item, {
|
||||
uploaded: true,
|
||||
attachmentId: attachmentIds[0],
|
||||
});
|
||||
|
||||
setUploadFiles(uploadFiles);
|
||||
setUploadFiles(uploadFiles);
|
||||
} catch (error) {
|
||||
Object.assign(item, {
|
||||
uploadFailed: true,
|
||||
failedMessage: String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [uploadFiles]);
|
||||
|
||||
const deleteFile = async (id: string, attachmentId: string) => {
|
||||
const deleteFile = async (file: UploadFile) => {
|
||||
const { id, uploadFailed, attachmentId } = file;
|
||||
|
||||
setUploadFiles(uploadFiles.filter((file) => file.id !== id));
|
||||
|
||||
if (uploadFailed) return;
|
||||
|
||||
platformAdapter.commands("delete_attachment", {
|
||||
serverId,
|
||||
id: attachmentId,
|
||||
@@ -71,16 +85,25 @@ const FileList = (props: FileListProps) => {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-y-2 -mx-1 text-sm">
|
||||
{uploadFiles.map((file) => {
|
||||
const { id, name, extname, size, uploaded, attachmentId } = file;
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
extname,
|
||||
size,
|
||||
uploaded,
|
||||
attachmentId,
|
||||
uploadFailed,
|
||||
failedMessage,
|
||||
} = file;
|
||||
|
||||
return (
|
||||
<div key={id} className="w-1/3 px-1">
|
||||
<div className="relative group flex items-center gap-1 p-1 rounded-[4px] bg-[#dedede] dark:bg-[#202126]">
|
||||
{attachmentId && (
|
||||
{(uploadFailed || attachmentId) && (
|
||||
<div
|
||||
className="absolute flex justify-center items-center size-[14px] bg-red-600 top-0 right-0 rounded-full cursor-pointer translate-x-[5px] -translate-y-[5px] transition opacity-0 group-hover:opacity-100 "
|
||||
onClick={() => {
|
||||
deleteFile(id, attachmentId);
|
||||
deleteFile(file);
|
||||
}}
|
||||
>
|
||||
<X className="size-[10px] text-white" />
|
||||
@@ -94,16 +117,24 @@ const FileList = (props: FileListProps) => {
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-[#999999]">
|
||||
{uploaded ? (
|
||||
<div className="flex gap-2">
|
||||
{extname && <span>{extname}</span>}
|
||||
<span>
|
||||
{filesize(size, { standard: "jedec", spacer: "" })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
{uploadFailed && failedMessage ? (
|
||||
<Tooltip2 content={failedMessage}>
|
||||
<span className="text-red-500">Upload Failed</span>
|
||||
</Tooltip2>
|
||||
) : (
|
||||
<span>{t("assistant.fileList.uploading")}</span>
|
||||
<div className="text-[#999]">
|
||||
{uploaded ? (
|
||||
<div className="flex gap-2">
|
||||
{extname && <span>{extname}</span>}
|
||||
<span>
|
||||
{filesize(size, { standard: "jedec", spacer: "" })}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span>{t("assistant.fileList.uploading")}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,7 @@ export function ServerList({
|
||||
fetchServers(true);
|
||||
|
||||
const unlisten = platformAdapter.listenEvent("login_or_logout", (event) => {
|
||||
console.log("Login or Logout:", currentService, event.payload);
|
||||
//console.log("Login or Logout:", currentService, event.payload);
|
||||
if (event.payload !== isLogin) {
|
||||
setIsLogin(!!event.payload);
|
||||
}
|
||||
|
||||
@@ -1,173 +1,174 @@
|
||||
import clsx from "clsx";
|
||||
import { filesize } from "filesize";
|
||||
import { Files, Trash2, X } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {filesize} from "filesize";
|
||||
import {Files, Trash2, X} from "lucide-react";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import {useConnectStore} from "@/stores/connectStore";
|
||||
import Checkbox from "@/components/Common/Checkbox";
|
||||
import FileIcon from "@/components/Common/Icons/FileIcon";
|
||||
import { AttachmentHit } from "@/types/commands";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import {AttachmentHit} from "@/types/commands";
|
||||
import {useAppStore} from "@/stores/appStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
|
||||
interface SessionFileProps {
|
||||
sessionId: string;
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
const SessionFile = (props: SessionFileProps) => {
|
||||
const { sessionId } = props;
|
||||
const { t } = useTranslation();
|
||||
const {sessionId} = props;
|
||||
const {t} = useTranslation();
|
||||
|
||||
const isTauri = useAppStore((state) => state.isTauri);
|
||||
const currentService = useConnectStore((state) => state.currentService);
|
||||
const [uploadedFiles, setUploadedFiles] = useState<AttachmentHit[]>([]);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [checkList, setCheckList] = useState<string[]>([]);
|
||||
const isTauri = useAppStore((state) => state.isTauri);
|
||||
const currentService = useConnectStore((state) => state.currentService);
|
||||
const [uploadedFiles, setUploadedFiles] = useState<AttachmentHit[]>([]);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [checkList, setCheckList] = useState<string[]>([]);
|
||||
|
||||
const serverId = useMemo(() => {
|
||||
return currentService.id;
|
||||
}, [currentService]);
|
||||
const serverId = useMemo(() => {
|
||||
return currentService.id;
|
||||
}, [currentService]);
|
||||
|
||||
useEffect(() => {
|
||||
setUploadedFiles([]);
|
||||
useEffect(() => {
|
||||
setUploadedFiles([]);
|
||||
|
||||
getUploadedFiles();
|
||||
}, [sessionId]);
|
||||
getUploadedFiles();
|
||||
}, [sessionId]);
|
||||
|
||||
const getUploadedFiles = async () => {
|
||||
if (isTauri) {
|
||||
const response: any = await platformAdapter.commands("get_attachment", {
|
||||
serverId,
|
||||
sessionId,
|
||||
});
|
||||
const getUploadedFiles = async () => {
|
||||
if (isTauri) {
|
||||
const response: any = await platformAdapter.commands("get_attachment", {
|
||||
serverId,
|
||||
sessionId,
|
||||
});
|
||||
|
||||
setUploadedFiles(response.hits.hits);
|
||||
} else {
|
||||
}
|
||||
};
|
||||
setUploadedFiles(response?.hits?.hits ?? []);
|
||||
} else {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
let result;
|
||||
if (isTauri) {
|
||||
result = await platformAdapter.commands("delete_attachment", {
|
||||
serverId,
|
||||
id,
|
||||
});
|
||||
} else {
|
||||
}
|
||||
if (!result) return;
|
||||
const handleDelete = async (id: string) => {
|
||||
let result;
|
||||
if (isTauri) {
|
||||
result = await platformAdapter.commands("delete_attachment", {
|
||||
serverId,
|
||||
id,
|
||||
});
|
||||
} else {
|
||||
}
|
||||
if (!result) return;
|
||||
|
||||
getUploadedFiles();
|
||||
};
|
||||
getUploadedFiles();
|
||||
};
|
||||
|
||||
const handleCheckAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
setCheckList(uploadedFiles.map((item) => item._source.id));
|
||||
} else {
|
||||
setCheckList([]);
|
||||
}
|
||||
};
|
||||
const handleCheckAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
setCheckList(uploadedFiles?.map((item) => item?._source?.id));
|
||||
} else {
|
||||
setCheckList([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheck = (checked: boolean, id: string) => {
|
||||
if (checked) {
|
||||
setCheckList([...checkList, id]);
|
||||
} else {
|
||||
setCheckList(checkList.filter((item) => item !== id));
|
||||
}
|
||||
};
|
||||
const handleCheck = (checked: boolean, id: string) => {
|
||||
if (checked) {
|
||||
setCheckList([...checkList, id]);
|
||||
} else {
|
||||
setCheckList(checkList.filter((item) => item !== id));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx("select-none", {
|
||||
hidden: uploadedFiles.length === 0,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className="absolute top-4 right-4 flex items-center justify-center size-8 rounded-lg bg-[#0072FF] cursor-pointer"
|
||||
onClick={() => {
|
||||
setVisible(true);
|
||||
}}
|
||||
>
|
||||
<Files className="size-5 text-white" />
|
||||
return (
|
||||
<div
|
||||
className={clsx("select-none", {
|
||||
hidden: uploadedFiles?.length === 0,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className="absolute top-4 right-4 flex items-center justify-center size-8 rounded-lg bg-[#0072FF] cursor-pointer"
|
||||
onClick={() => {
|
||||
setVisible(true);
|
||||
}}
|
||||
>
|
||||
<Files className="size-5 text-white"/>
|
||||
|
||||
<div className="absolute -top-2 -right-2 flex items-center justify-center min-w-4 h-4 px-1 text-white text-xs rounded-full bg-[#3DB954]">
|
||||
{uploadedFiles.length}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="absolute -top-2 -right-2 flex items-center justify-center min-w-4 h-4 px-1 text-white text-xs rounded-full bg-[#3DB954]">
|
||||
{uploadedFiles?.length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
"absolute inset-0 flex flex-col p-4 bg-white dark:bg-black",
|
||||
{
|
||||
hidden: !visible,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<X
|
||||
className="absolute top-4 right-4 size-5 text-[#999] cursor-pointer"
|
||||
onClick={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
"absolute inset-0 flex flex-col p-4 bg-white dark:bg-black",
|
||||
{
|
||||
hidden: !visible,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<X
|
||||
className="absolute top-4 right-4 size-5 text-[#999] cursor-pointer"
|
||||
onClick={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="mb-2 text-sm text-[#333] dark:text-[#D8D8D8] font-bold">
|
||||
{t("assistant.sessionFile.title")}
|
||||
</div>
|
||||
<div className="flex items-center justify-between pr-2">
|
||||
<div className="mb-2 text-sm text-[#333] dark:text-[#D8D8D8] font-bold">
|
||||
{t("assistant.sessionFile.title")}
|
||||
</div>
|
||||
<div className="flex items-center justify-between pr-2">
|
||||
<span className="text-sm text-[#999]">
|
||||
{t("assistant.sessionFile.description")}
|
||||
</span>
|
||||
|
||||
<Checkbox
|
||||
indeterminate
|
||||
checked={checkList.length === uploadedFiles.length}
|
||||
onChange={handleCheckAll}
|
||||
/>
|
||||
</div>
|
||||
<ul className="flex-1 overflow-auto flex flex-col gap-2 mt-6">
|
||||
{uploadedFiles.map((item) => {
|
||||
const { id, name, icon, size } = item._source;
|
||||
<Checkbox
|
||||
indeterminate
|
||||
checked={checkList?.length === uploadedFiles?.length}
|
||||
onChange={handleCheckAll}
|
||||
/>
|
||||
</div>
|
||||
<ul className="flex-1 overflow-auto flex flex-col gap-2 mt-6 p-0">
|
||||
{uploadedFiles?.map((item) => {
|
||||
const {id, name, icon, size} = item._source;
|
||||
|
||||
return (
|
||||
<li
|
||||
key={id}
|
||||
className="flex items-center justify-between min-h-12 px-2 rounded-[4px] bg-[#ededed] dark:bg-[#202126]"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FileIcon extname={icon} />
|
||||
return (
|
||||
<li
|
||||
key={id}
|
||||
className="flex items-center justify-between min-h-12 px-2 rounded-[4px] bg-[#ededed] dark:bg-[#202126]"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FileIcon extname={icon}/>
|
||||
|
||||
<div>
|
||||
<div className="text-sm leading-4 text-[#333] dark:text-[#D8D8D8]">
|
||||
{name}
|
||||
</div>
|
||||
<div className="text-xs text-[#999]">
|
||||
<span>{icon}</span>
|
||||
<span className="pl-2">
|
||||
{filesize(size, { standard: "jedec", spacer: "" })}
|
||||
<div>
|
||||
<div className="text-sm leading-4 text-[#333] dark:text-[#D8D8D8]">
|
||||
{name}
|
||||
</div>
|
||||
<div className="text-xs text-[#999]">
|
||||
<span>{icon}</span>
|
||||
<span className="pl-2">
|
||||
{filesize(size, {standard: "jedec", spacer: ""})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash2
|
||||
className="size-4 text-[#999] cursor-pointer"
|
||||
onClick={() => handleDelete(id)}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash2
|
||||
className="size-4 text-[#999] cursor-pointer"
|
||||
onClick={() => handleDelete(id)}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
checked={checkList.includes(id)}
|
||||
onChange={(checked) => handleCheck(checked, id)}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<Checkbox
|
||||
checked={checkList.includes(id)}
|
||||
onChange={(checked) => handleCheck(checked, id)}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SessionFile;
|
||||
|
||||
@@ -96,7 +96,7 @@ const Splash = () => {
|
||||
|
||||
return (
|
||||
visibleStartPage && (
|
||||
<div className="absolute inset-0 flex flex-col items-center px-6 pt-6 text-[#333] dark:text-white select-none">
|
||||
<div className="absolute inset-0 flex flex-col items-center px-6 pt-6 text-[#333] dark:text-white select-none overflow-y-auto custom-scrollbar">
|
||||
<CircleX
|
||||
className="absolute top-3 right-3 size-4 text-[#999] cursor-pointer"
|
||||
onClick={() => {
|
||||
|
||||
@@ -15,7 +15,7 @@ export const CallTools = ({ Detail, ChunkData, loading }: CallToolsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [isThinkingExpanded, setIsThinkingExpanded] = useState(false);
|
||||
|
||||
const [Data, setData] = useState("");
|
||||
const [data, setData] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!Detail?.description) return;
|
||||
@@ -25,7 +25,7 @@ export const CallTools = ({ Detail, ChunkData, loading }: CallToolsProps) => {
|
||||
useEffect(() => {
|
||||
if (!ChunkData?.message_chunk) return;
|
||||
setData(ChunkData?.message_chunk);
|
||||
}, [ChunkData?.message_chunk, Data]);
|
||||
}, [ChunkData?.message_chunk, data]);
|
||||
|
||||
// Must be after hooks !!!
|
||||
if (!ChunkData && !Detail) return null;
|
||||
@@ -62,11 +62,11 @@ export const CallTools = ({ Detail, ChunkData, loading }: CallToolsProps) => {
|
||||
<div className="pl-2 border-l-2 border-[#e5e5e5] dark:border-[#4e4e56]">
|
||||
<div className="text-[#8b8b8b] dark:text-[#a6a6a6] space-y-2">
|
||||
<Markdown
|
||||
content={Data || ""}
|
||||
content={data || ""}
|
||||
loading={loading}
|
||||
onDoubleClickCapture={() => {}}
|
||||
/>
|
||||
{/* {Data?.split("\n").map(
|
||||
{/* {data?.split("\n").map(
|
||||
(paragraph, idx) =>
|
||||
paragraph.trim() && (
|
||||
<p key={idx} className="text-sm">
|
||||
|
||||
@@ -20,7 +20,7 @@ export const DeepRead = ({
|
||||
|
||||
const [isThinkingExpanded, setIsThinkingExpanded] = useState(false);
|
||||
|
||||
const [Data, setData] = useState<string[]>([]);
|
||||
const [data, setData] = useState<string[]>([]);
|
||||
const [description, setDescription] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
@@ -71,7 +71,7 @@ export const DeepRead = ({
|
||||
ChunkData?.chunk_type || Detail?.type
|
||||
}`,
|
||||
{
|
||||
count: Number(Data.length),
|
||||
count: Number(data.length),
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
@@ -87,7 +87,7 @@ export const DeepRead = ({
|
||||
<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) => (
|
||||
{data?.map((item) => (
|
||||
<div key={item} className="flex flex-col gap-2">
|
||||
<div className="text-xs text-[#999999] dark:text-[#808080]">
|
||||
- {item}
|
||||
|
||||
@@ -129,17 +129,17 @@ export const FetchSource = ({
|
||||
className="group flex items-center p-2 hover:bg-[#F7F7F7] dark:hover:bg-[#2C2C2C] border-b border-[#E6E6E6] dark:border-[#272626] last:border-b-0 cursor-pointer transition-colors"
|
||||
>
|
||||
<div className="w-full flex items-center gap-2">
|
||||
<div className="w-full md:w-[75%] flex items-center gap-1">
|
||||
<div className="w-[75%] mobile:w-full flex items-center gap-1">
|
||||
<Globe className="w-3 h-3 flex-shrink-0" />
|
||||
<div className="text-xs text-[#333333] dark:text-[#D8D8D8] truncate font-normal group-hover:text-[#0072FF] dark:group-hover:text-[#0072FF]">
|
||||
{item.title || item.category}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`flex mobile:hidden w-[25%] items-center justify-end gap-2`}
|
||||
className={`flex-1 mobile:hidden flex items-center justify-end gap-2`}
|
||||
>
|
||||
<span className="text-xs text-[#999999] dark:text-[#999999] truncate">
|
||||
{item.source?.name}
|
||||
{item.source?.name || item?.category}
|
||||
</span>
|
||||
<SquareArrowOutUpRight className="w-3 h-3 text-[#999999] dark:text-[#999999] flex-shrink-0" />
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@ export const PickSource = ({
|
||||
|
||||
const [isThinkingExpanded, setIsThinkingExpanded] = useState(false);
|
||||
|
||||
const [Data, setData] = useState<IData[]>([]);
|
||||
const [data, setData] = useState<IData[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!Detail?.payload) return;
|
||||
@@ -90,7 +90,7 @@ export const PickSource = ({
|
||||
ChunkData?.chunk_type || Detail.type
|
||||
}`,
|
||||
{
|
||||
count: Data?.length,
|
||||
count: data?.length,
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
@@ -106,7 +106,7 @@ export const PickSource = ({
|
||||
<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) => (
|
||||
{data?.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="p-3 rounded-lg border border-[#E6E6E6] dark:border-[#272626] bg-white dark:bg-[#1E1E1E] hover:bg-gray-50 dark:hover:bg-[#2C2C2C] transition-colors"
|
||||
|
||||
@@ -24,7 +24,7 @@ const PrevSuggestion: FC<PrevSuggestionProps> = (props) => {
|
||||
}, [JSON.stringify(currentAssistant)]);
|
||||
|
||||
return (
|
||||
<ul className="absolute left-2 bottom-2 flex flex-col gap-2">
|
||||
<ul className="absolute left-2 bottom-2 flex flex-col gap-2 p-0">
|
||||
{list.map((item) => {
|
||||
return (
|
||||
<li
|
||||
|
||||
@@ -30,7 +30,7 @@ export const QueryIntent = ({
|
||||
|
||||
const [isThinkingExpanded, setIsThinkingExpanded] = useState(false);
|
||||
|
||||
const [Data, setData] = useState<IQueryData | null>(null);
|
||||
const [data, setData] = useState<IQueryData | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!Detail?.payload) return;
|
||||
@@ -100,13 +100,13 @@ export const QueryIntent = ({
|
||||
<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 ? (
|
||||
{data?.keyword ? (
|
||||
<div className="flex gap-1">
|
||||
<span className="text-[#999999]">
|
||||
- {t("assistant.message.steps.keywords")}:
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{Data?.keyword?.map((keyword, index) => (
|
||||
{data?.keyword?.map((keyword, index) => (
|
||||
<span
|
||||
key={keyword + index}
|
||||
className="text-[#333333] dark:text-[#D8D8D8]"
|
||||
@@ -118,33 +118,33 @@ export const QueryIntent = ({
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{Data?.category ? (
|
||||
{data?.category ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-[#999999]">
|
||||
- {t("assistant.message.steps.questionType")}:
|
||||
</span>
|
||||
<span className="text-[#333333] dark:text-[#D8D8D8]">
|
||||
{Data?.category}
|
||||
{data?.category}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
{Data?.intent ? (
|
||||
{data?.intent ? (
|
||||
<div className="flex items-start gap-1">
|
||||
<span className="text-[#999999]">
|
||||
- {t("assistant.message.steps.userIntent")}:
|
||||
</span>
|
||||
<div className="flex-1 text-[#333333] dark:text-[#D8D8D8]">
|
||||
{Data?.intent}
|
||||
{data?.intent}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{Data?.query ? (
|
||||
{data?.query ? (
|
||||
<div className="flex items-start gap-1">
|
||||
<span className="text-[#999999]">
|
||||
- {t("assistant.message.steps.relatedQuestions")}:
|
||||
</span>
|
||||
<div className="flex-1 flex flex-col text-[#333333] dark:text-[#D8D8D8]">
|
||||
{Data?.query?.map((question, qIndex) => (
|
||||
{data?.query?.map((question, qIndex) => (
|
||||
<span key={question + qIndex}>- {question}</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ export const Think = ({ Detail, ChunkData, loading }: ThinkProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [isThinkingExpanded, setIsThinkingExpanded] = useState(true);
|
||||
|
||||
const [Data, setData] = useState("");
|
||||
const [data, setData] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!Detail?.description) return;
|
||||
@@ -24,7 +24,7 @@ export const Think = ({ Detail, ChunkData, loading }: ThinkProps) => {
|
||||
useEffect(() => {
|
||||
if (!ChunkData?.message_chunk) return;
|
||||
setData(ChunkData?.message_chunk);
|
||||
}, [ChunkData?.message_chunk, Data]);
|
||||
}, [ChunkData?.message_chunk, data]);
|
||||
|
||||
// Must be after hooks !!!
|
||||
if (!ChunkData && !Detail) return null;
|
||||
@@ -59,7 +59,7 @@ export const Think = ({ Detail, ChunkData, loading }: ThinkProps) => {
|
||||
{isThinkingExpanded && (
|
||||
<div className="pl-2 border-l-2 border-[#e5e5e5] dark:border-[#4e4e56]">
|
||||
<div className="text-[#8b8b8b] dark:text-[#a6a6a6] space-y-2">
|
||||
{Data?.split("\n").map(
|
||||
{data?.split("\n").map(
|
||||
(paragraph, idx) =>
|
||||
paragraph.trim() && (
|
||||
<p key={idx} className="text-sm">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { CopyButton } from "@/components/Common/CopyButton";
|
||||
|
||||
@@ -15,7 +16,13 @@ export const UserMessage = ({ messageContent }: UserMessageProps) => {
|
||||
onMouseEnter={() => setShowCopyButton(true)}
|
||||
onMouseLeave={() => setShowCopyButton(false)}
|
||||
>
|
||||
{showCopyButton && <CopyButton textToCopy={messageContent} />}
|
||||
<div
|
||||
className={clsx("size-6 transition", {
|
||||
"opacity-0": !showCopyButton,
|
||||
})}
|
||||
>
|
||||
<CopyButton textToCopy={messageContent} />
|
||||
</div>
|
||||
<div
|
||||
className="px-3 py-2 bg-white dark:bg-[#202126] rounded-xl border border-black/12 dark:border-black/15 font-normal text-sm text-[#333333] dark:text-[#D8D8D8] cursor-pointer select-none"
|
||||
onDoubleClick={(e) => {
|
||||
|
||||
@@ -679,11 +679,14 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.markdown-body p br,
|
||||
.markdown-body table td br,
|
||||
.markdown-body table th br {
|
||||
display: block;
|
||||
content: "";
|
||||
margin-top: 8px;
|
||||
display: block !important;
|
||||
content: "" !important;
|
||||
margin-top: 8px !important;
|
||||
height: 1px !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.markdown-body table tr {
|
||||
|
||||
44
src/components/Common/ErrorNotification/ErrorSearch.tsx
Normal file
44
src/components/Common/ErrorNotification/ErrorSearch.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useState } from "react";
|
||||
import { CircleAlert, Bolt, X, Ellipsis } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import Tooltip from "@/components/Common/Tooltip";
|
||||
|
||||
interface ErrorSearchProps {
|
||||
isError: any[];
|
||||
}
|
||||
|
||||
const ErrorSearch = ({
|
||||
isError,
|
||||
}: ErrorSearchProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [showError, setShowError] = useState<boolean>(isError?.length > 0);
|
||||
|
||||
if (!showError) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-sm text-[#333] dark:text-[#666] p-2">
|
||||
<CircleAlert className="text-[#FF0000] size-3" />
|
||||
{t("search.list.failures")}
|
||||
|
||||
<Tooltip content={isError} position="bottom">
|
||||
<Ellipsis className="dark:text-white size-3 cursor-pointer" />
|
||||
</Tooltip>
|
||||
|
||||
<Bolt
|
||||
className="dark:text-white size-3 cursor-pointer"
|
||||
onClick={() => {
|
||||
platformAdapter.emitEvent("open_settings", "connect");
|
||||
}}
|
||||
/>
|
||||
<X
|
||||
className="text-[#666] size-4 cursor-pointer"
|
||||
onClick={() => setShowError(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorSearch;
|
||||
@@ -14,12 +14,20 @@ import { FC, useEffect, useMemo, useRef, useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
|
||||
import clsx from "clsx";
|
||||
import { Ellipsis, Pencil, RefreshCcw, Search, Trash2 } from "lucide-react";
|
||||
import {
|
||||
Ellipsis,
|
||||
PanelLeftClose,
|
||||
Pencil,
|
||||
RefreshCcw,
|
||||
Search,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import VisibleKey from "../VisibleKey";
|
||||
import { Chat } from "@/components/Assistant/types";
|
||||
import NoDataImage from "../NoDataImage";
|
||||
import { closeHistoryPanel } from "@/utils";
|
||||
|
||||
dayjs.extend(isSameOrAfter);
|
||||
|
||||
@@ -28,7 +36,7 @@ interface HistoryListProps {
|
||||
list: Chat[];
|
||||
active?: Chat;
|
||||
onSearch: (keyword: string) => void;
|
||||
onRefresh: () => Promise<void>;
|
||||
onRefresh: () => void;
|
||||
onSelect: (chat: Chat) => void;
|
||||
onRename: (chatId: string, title: string) => void;
|
||||
onRemove: (chatId: string) => void;
|
||||
@@ -159,10 +167,10 @@ const HistoryList: FC<HistoryListProps> = (props) => {
|
||||
ref={listRef}
|
||||
id={id}
|
||||
className={clsx(
|
||||
"flex flex-col h-full overflow-auto px-3 py-2 text-sm bg-[#F3F4F6] dark:bg-[#1F2937] custom-scrollbar"
|
||||
"flex flex-col h-screen text-sm bg-[#F3F4F6] dark:bg-[#1F2937]"
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-1 children:h-8">
|
||||
<div className="flex gap-1 p-2 border-b dark:border-[#343D4D]">
|
||||
<div className="flex-1 flex items-center gap-2 px-2 rounded-lg border transition border-[#E6E6E6] bg-[#F8F9FA] dark:bg-[#2B3444] dark:border-[#343D4D] focus-within:border-[#0061FF]">
|
||||
<VisibleKey
|
||||
shortcut="F"
|
||||
@@ -198,202 +206,219 @@ const HistoryList: FC<HistoryListProps> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
<div className="flex-1 px-2 overflow-auto custom-scrollbar">
|
||||
{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;
|
||||
<ul className="p-0">
|
||||
{list.map((item) => {
|
||||
const { _id, _source } = item;
|
||||
|
||||
const isActive = _id === active?._id;
|
||||
const title = _source?.title ?? _id;
|
||||
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);
|
||||
|
||||
setIsEdit(false);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span className="truncate">{title}</span>
|
||||
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);
|
||||
}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{isActive && !isEdit && (
|
||||
<VisibleKey
|
||||
shortcut="↑↓"
|
||||
rootClassName="w-6"
|
||||
shortcutClassName="w-6"
|
||||
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);
|
||||
|
||||
setIsEdit(false);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span className="truncate">{title}</span>
|
||||
)}
|
||||
|
||||
<Popover>
|
||||
<div className="flex items-center gap-2">
|
||||
{isActive && !isEdit && (
|
||||
<PopoverButton
|
||||
ref={moreButtonRef}
|
||||
className="flex gap-2"
|
||||
>
|
||||
<VisibleKey
|
||||
shortcut="O"
|
||||
onKeyPress={() => {
|
||||
moreButtonRef.current?.click();
|
||||
}}
|
||||
>
|
||||
<Ellipsis className="size-4 text-[#979797]" />
|
||||
</VisibleKey>
|
||||
</PopoverButton>
|
||||
<VisibleKey
|
||||
shortcut="↑↓"
|
||||
rootClassName="w-6"
|
||||
shortcutClassName="w-6"
|
||||
/>
|
||||
)}
|
||||
|
||||
<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}
|
||||
<Popover>
|
||||
{isActive && !isEdit && (
|
||||
<PopoverButton
|
||||
ref={moreButtonRef}
|
||||
className="flex gap-2"
|
||||
>
|
||||
<VisibleKey
|
||||
shortcut="O"
|
||||
onKeyPress={() => {
|
||||
moreButtonRef.current?.click();
|
||||
}}
|
||||
>
|
||||
<VisibleKey
|
||||
shortcut={shortcut}
|
||||
onKeyPress={onClick}
|
||||
<Ellipsis className="size-4 text-[#979797]" />
|
||||
</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}
|
||||
>
|
||||
<Icon
|
||||
className="size-4"
|
||||
style={{
|
||||
color: iconColor,
|
||||
}}
|
||||
/>
|
||||
</VisibleKey>
|
||||
<VisibleKey
|
||||
shortcut={shortcut}
|
||||
onKeyPress={onClick}
|
||||
>
|
||||
<Icon
|
||||
className="size-4"
|
||||
style={{
|
||||
color: iconColor,
|
||||
}}
|
||||
/>
|
||||
</VisibleKey>
|
||||
|
||||
<span>{t(label)}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
<span>{t(label)}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
</div>
|
||||
</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>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Dialog>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center justify-center flex-1">
|
||||
<NoDataImage />
|
||||
</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?._source?.message || 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>
|
||||
</Dialog>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center justify-center flex-1 pt-8">
|
||||
<NoDataImage />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end p-2 border-t dark:border-[#343D4D]">
|
||||
<VisibleKey shortcut="Esc" shortcutClassName="w-7">
|
||||
<PanelLeftClose
|
||||
className="size-4 text-black/80 dark:text-white/80 cursor-pointer"
|
||||
onClick={closeHistoryPanel}
|
||||
/>
|
||||
</VisibleKey>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import "./style.css";
|
||||
|
||||
interface TooltipProps {
|
||||
content: string;
|
||||
content: string | any[];
|
||||
position?: "top" | "bottom" | "left" | "right";
|
||||
children: React.ReactNode;
|
||||
}
|
||||
@@ -17,6 +18,21 @@ const Tooltip: React.FC<TooltipProps> = ({
|
||||
const handleMouseEnter = () => setVisible(true);
|
||||
const handleMouseLeave = () => setVisible(false);
|
||||
|
||||
const renderContent = () => {
|
||||
if (Array.isArray(content)) {
|
||||
return (
|
||||
<ul className="list-none p-0 m-0">
|
||||
{content.map((item, index) => (
|
||||
<li key={index} className="py-1">
|
||||
{item?.error || item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="tooltip-container"
|
||||
@@ -25,7 +41,9 @@ const Tooltip: React.FC<TooltipProps> = ({
|
||||
>
|
||||
{children}
|
||||
{visible && (
|
||||
<div className={`tooltip-box tooltip-${position}`}>{content}</div>
|
||||
<div className={`tooltip-box tooltip-${position}`}>
|
||||
{renderContent()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
42
src/components/Common/Tooltip2.tsx
Normal file
42
src/components/Common/Tooltip2.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverPanel,
|
||||
PopoverPanelProps,
|
||||
} from "@headlessui/react";
|
||||
import { useBoolean } from "ahooks";
|
||||
import clsx from "clsx";
|
||||
import { FC, ReactNode } from "react";
|
||||
|
||||
interface Tooltip2Props extends PopoverPanelProps {
|
||||
content: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const Tooltip2: FC<Tooltip2Props> = (props) => {
|
||||
const { content, children, anchor = "top", ...rest } = props;
|
||||
const [visible, { setTrue, setFalse }] = useBoolean(false);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverButton onMouseOver={setTrue} onMouseOut={setFalse}>
|
||||
{children}
|
||||
</PopoverButton>
|
||||
<PopoverPanel
|
||||
{...rest}
|
||||
static
|
||||
anchor={anchor}
|
||||
className={clsx(
|
||||
"fixed z-1000 p-2 rounded-md text-xs text-white bg-black/75 hidden",
|
||||
{
|
||||
"!block": visible,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tooltip2;
|
||||
@@ -1,5 +1,11 @@
|
||||
import { useBoolean } from "ahooks";
|
||||
import { useRef, useImperativeHandle, forwardRef, KeyboardEvent } from "react";
|
||||
import {
|
||||
useRef,
|
||||
useImperativeHandle,
|
||||
forwardRef,
|
||||
KeyboardEvent,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface AutoResizeTextareaProps {
|
||||
@@ -8,61 +14,86 @@ interface AutoResizeTextareaProps {
|
||||
handleKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
connected: boolean;
|
||||
chatPlaceholder?: string;
|
||||
onLineCountChange?: (lineCount: number) => void;
|
||||
}
|
||||
|
||||
// Forward ref to allow parent to interact with this component
|
||||
const AutoResizeTextarea = forwardRef<
|
||||
{ reset: () => void; focus: () => void },
|
||||
AutoResizeTextareaProps
|
||||
>(({ input, setInput, handleKeyDown, connected, chatPlaceholder }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [isComposition, { setTrue, setFalse }] = useBoolean();
|
||||
|
||||
// Expose methods to the parent via ref
|
||||
useImperativeHandle(ref, () => ({
|
||||
reset: () => {
|
||||
setInput("");
|
||||
>(
|
||||
(
|
||||
{
|
||||
input,
|
||||
setInput,
|
||||
handleKeyDown,
|
||||
connected,
|
||||
chatPlaceholder,
|
||||
onLineCountChange,
|
||||
},
|
||||
focus: () => {
|
||||
textareaRef.current?.focus();
|
||||
},
|
||||
}));
|
||||
ref
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [isComposition, { setTrue, setFalse }] = useBoolean();
|
||||
|
||||
const handleKeyPress = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (isComposition) return;
|
||||
// Expose methods to the parent via ref
|
||||
useImperativeHandle(ref, () => ({
|
||||
reset: () => {
|
||||
setInput("");
|
||||
},
|
||||
focus: () => {
|
||||
textareaRef.current?.focus();
|
||||
},
|
||||
}));
|
||||
|
||||
handleKeyDown?.(event);
|
||||
};
|
||||
const handleKeyPress = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (isComposition) return;
|
||||
|
||||
return (
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
spellCheck="false"
|
||||
className="text-base flex-1 outline-none min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent"
|
||||
placeholder={
|
||||
connected ? chatPlaceholder || t("search.textarea.placeholder") : ""
|
||||
handleKeyDown?.(event);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = "auto";
|
||||
const newHeight = Math.min(textareaRef.current.scrollHeight, 15 * 16); // 15rem ≈ 15 * 16px
|
||||
textareaRef.current.style.height = `${newHeight}px`;
|
||||
|
||||
const lineHeight = 24; // 1.5rem = 24px
|
||||
const lineCount = Math.ceil(newHeight / lineHeight);
|
||||
onLineCountChange?.(lineCount);
|
||||
}
|
||||
aria-label={t("search.textarea.ariaLabel")}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyPress}
|
||||
onCompositionStart={setTrue}
|
||||
onCompositionEnd={() => {
|
||||
setTimeout(setFalse, 0);
|
||||
}}
|
||||
rows={1}
|
||||
style={{
|
||||
resize: "none", // Prevent manual resize
|
||||
overflow: "auto", // Enable scrollbars when needed
|
||||
maxHeight: "4.5rem", // Limit height to 3 rows (3 * 1.5 line-height)
|
||||
lineHeight: "1.5rem", // Line height to match row height
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}, [input]);
|
||||
|
||||
return (
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
spellCheck="false"
|
||||
className="text-base flex-1 outline-none min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent custom-scrollbar"
|
||||
placeholder={
|
||||
connected ? chatPlaceholder || t("search.textarea.placeholder") : ""
|
||||
}
|
||||
aria-label={t("search.textarea.ariaLabel")}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyPress}
|
||||
onCompositionStart={setTrue}
|
||||
onCompositionEnd={() => {
|
||||
setTimeout(setFalse, 0);
|
||||
}}
|
||||
rows={1}
|
||||
style={{
|
||||
resize: "none", // Prevent manual resize
|
||||
overflow: "auto", // Enable scrollbars when needed
|
||||
maxHeight: "13.5rem", // Limit height to 9 rows (9 * 1.5 line-height)
|
||||
lineHeight: "1.5rem", // Line height to match row height
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default AutoResizeTextarea;
|
||||
|
||||
70
src/components/Search/ChatIcons.tsx
Normal file
70
src/components/Search/ChatIcons.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React from "react";
|
||||
import { Send } from "lucide-react";
|
||||
|
||||
import StopIcon from "@/icons/Stop";
|
||||
|
||||
interface ChatIconsProps {
|
||||
lineCount: number;
|
||||
isChatMode: boolean;
|
||||
curChatEnd: boolean;
|
||||
inputValue: string;
|
||||
onSend: (value: string) => void;
|
||||
disabledChange: () => void;
|
||||
}
|
||||
|
||||
const ChatIcons: React.FC<ChatIconsProps> = ({
|
||||
lineCount,
|
||||
isChatMode,
|
||||
curChatEnd,
|
||||
inputValue,
|
||||
onSend,
|
||||
disabledChange,
|
||||
}) => {
|
||||
const renderSendButton = () => {
|
||||
if (!isChatMode) return null;
|
||||
|
||||
if (curChatEnd) {
|
||||
return (
|
||||
<button
|
||||
className={`ml-1 p-1 ${
|
||||
inputValue ? "bg-[#0072FF]" : "bg-[#E4E5F0] dark:bg-[rgb(84,84,84)]"
|
||||
} rounded-full transition-colors`}
|
||||
type="submit"
|
||||
onClick={() => onSend(inputValue.trim())}
|
||||
>
|
||||
<Send className="w-4 h-4 text-white" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (!curChatEnd) {
|
||||
return (
|
||||
<button
|
||||
className={`ml-1 px-1 bg-[#0072FF] rounded-full transition-colors`}
|
||||
type="submit"
|
||||
onClick={() => disabledChange()}
|
||||
>
|
||||
<StopIcon
|
||||
size={16}
|
||||
className="w-4 h-4 text-white"
|
||||
aria-label="Stop message"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{lineCount === 1 ? (
|
||||
renderSendButton()
|
||||
) : (
|
||||
<div className="w-full flex justify-end mt-1">{renderSendButton()}</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatIcons;
|
||||
58
src/components/Search/ConnectionError.tsx
Normal file
58
src/components/Search/ConnectionError.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import VisibleKey from "@/components/Common/VisibleKey";
|
||||
|
||||
interface ConnectionErrorProps {
|
||||
reconnect: () => void;
|
||||
connected: boolean;
|
||||
}
|
||||
|
||||
export default function ConnectionError({
|
||||
reconnect,
|
||||
connected,
|
||||
}: ConnectionErrorProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [reconnectCountdown, setReconnectCountdown] = useState<number>(0);
|
||||
useEffect(() => {
|
||||
if (!reconnectCountdown || connected) {
|
||||
setReconnectCountdown(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (reconnectCountdown > 0) {
|
||||
const timer = setTimeout(() => {
|
||||
setReconnectCountdown(reconnectCountdown - 1);
|
||||
}, 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [reconnectCountdown, connected]);
|
||||
|
||||
return (
|
||||
<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"
|
||||
onClick={() => {
|
||||
reconnect();
|
||||
setReconnectCountdown(10);
|
||||
}}
|
||||
>
|
||||
{reconnectCountdown > 0 ? (
|
||||
`${t("search.input.connecting")}(${reconnectCountdown}s)`
|
||||
) : (
|
||||
<VisibleKey
|
||||
shortcut="R"
|
||||
onKeyPress={() => {
|
||||
reconnect();
|
||||
setReconnectCountdown(10);
|
||||
}}
|
||||
>
|
||||
{t("search.input.reconnect")}
|
||||
</VisibleKey>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -206,7 +206,7 @@ const ContextMenu = ({ hideCoco }: ContextMenuProps) => {
|
||||
>
|
||||
<div className="text-[#999] dark:text-[#666] truncate">{title}</div>
|
||||
|
||||
<ul className="flex flex-col -mx-2">
|
||||
<ul className="flex flex-col -mx-2 p-0">
|
||||
{searchMenus.map((item, index) => {
|
||||
const { name, icon, keys, clickEvent } = item;
|
||||
|
||||
|
||||
@@ -35,7 +35,9 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
||||
const { t } = useTranslation();
|
||||
const sourceData = useSearchStore((state) => state.sourceData);
|
||||
const isTauri = useAppStore((state) => state.isTauri);
|
||||
const queryTimeout = useConnectStore((state) => state.querySourceTimeout);
|
||||
const querySourceTimeout = useConnectStore((state) => {
|
||||
return state.querySourceTimeout;
|
||||
});
|
||||
|
||||
const [selectedItem, setSelectedItem] = useState<number | null>(null);
|
||||
const [total, setTotal] = useState(0);
|
||||
@@ -43,6 +45,11 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const [isKeyboardMode, setIsKeyboardMode] = useState(false);
|
||||
|
||||
const querySourceTimeoutRef = useRef(querySourceTimeout);
|
||||
useEffect(() => {
|
||||
querySourceTimeoutRef.current = querySourceTimeout;
|
||||
}, [querySourceTimeout]);
|
||||
|
||||
const { data, loading } = useInfiniteScroll(
|
||||
async (d) => {
|
||||
const from = d?.list?.length || 0;
|
||||
@@ -65,7 +72,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
||||
from: from,
|
||||
size: PAGE_SIZE,
|
||||
queryStrings: queryStrings,
|
||||
queryTimeout,
|
||||
queryTimeout: querySourceTimeoutRef.current,
|
||||
});
|
||||
} else {
|
||||
let url = `/query/_search?query=${queryStrings.query}&datasource=${queryStrings.datasource}&from=${from}&size=${PAGE_SIZE}`;
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { useEffect, useRef, useState, useCallback, MouseEvent } from "react";
|
||||
import { CircleAlert, Bolt, X, ArrowBigRight } from "lucide-react";
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useCallback,
|
||||
MouseEvent,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { ArrowBigRight } from "lucide-react";
|
||||
import { isNil } from "lodash-es";
|
||||
import { useDebounceFn, useUnmount } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -14,22 +21,22 @@ import { copyToClipboard, OpenURLWithBrowser } from "@/utils/index";
|
||||
import VisibleKey from "@/components/Common/VisibleKey";
|
||||
import Calculator from "./Calculator";
|
||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import ErrorSearch from "@/components/Common/ErrorNotification/ErrorSearch";
|
||||
|
||||
type ISearchData = Record<string, any[]>;
|
||||
|
||||
interface DropdownListProps {
|
||||
suggests: any[];
|
||||
SearchData: ISearchData;
|
||||
IsError: boolean;
|
||||
searchData: ISearchData;
|
||||
isError: any[];
|
||||
isSearchComplete: boolean;
|
||||
isChatMode: boolean;
|
||||
}
|
||||
|
||||
function DropdownList({
|
||||
suggests,
|
||||
SearchData,
|
||||
IsError,
|
||||
searchData,
|
||||
isError,
|
||||
isChatMode,
|
||||
}: DropdownListProps) {
|
||||
const { t } = useTranslation();
|
||||
@@ -39,7 +46,6 @@ function DropdownList({
|
||||
|
||||
const setSourceData = useSearchStore((state) => state.setSourceData);
|
||||
|
||||
const [showError, setShowError] = useState<boolean>(IsError);
|
||||
const [selectedItem, setSelectedItem] = useState<number | null>(null);
|
||||
const [selectedName, setSelectedName] = useState<string>("");
|
||||
const [showIndex, setShowIndex] = useState<boolean>(false);
|
||||
@@ -83,7 +89,7 @@ function DropdownList({
|
||||
setSelectedItem(null);
|
||||
|
||||
run();
|
||||
}, [SearchData]);
|
||||
}, [searchData]);
|
||||
|
||||
const openPopover = useShortcutsStore((state) => state.openPopover);
|
||||
|
||||
@@ -123,7 +129,7 @@ function DropdownList({
|
||||
goToTwoPage(item);
|
||||
}
|
||||
|
||||
if (e.key === "Enter" && selectedItem !== null) {
|
||||
if (e.key === "Enter" && !e.shiftKey && selectedItem !== null) {
|
||||
// console.log("Enter key pressed", selectedItem);
|
||||
const item = globalItemIndexMap[selectedItem];
|
||||
if (item?.url) {
|
||||
@@ -192,32 +198,35 @@ function DropdownList({
|
||||
setVisibleContextMenu(true);
|
||||
};
|
||||
|
||||
const memoizedCallbacks = useMemo(() => {
|
||||
return {
|
||||
onMouseEnter: (index: number) => () => setSelectedItem(index),
|
||||
onItemClick: (item: any) => () => {
|
||||
if (item?.url) {
|
||||
OpenURLWithBrowser(item.url);
|
||||
}
|
||||
},
|
||||
goToTwoPage: (item: any) => () => setSourceData(item),
|
||||
};
|
||||
}, []);
|
||||
|
||||
const showHeader = useMemo(
|
||||
() => Object.entries(searchData).length < 5,
|
||||
[searchData]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
data-tauri-drag-region
|
||||
className="h-full w-full p-2 flex flex-col overflow-y-auto custom-scrollbar focus:outline-none"
|
||||
tabIndex={0}
|
||||
role="listbox"
|
||||
aria-label={t("search.header.results")}
|
||||
>
|
||||
{showError && (
|
||||
<div className="flex items-center gap-2 text-sm text-[#333] dark:text-[#666] p-2">
|
||||
<CircleAlert className="text-[#FF0000] size-3" />
|
||||
{t("search.list.failures")}
|
||||
<Bolt
|
||||
className="dark:text-white size-3 cursor-pointer"
|
||||
onClick={() => {
|
||||
platformAdapter.emitEvent("open_settings", "connect");
|
||||
}}
|
||||
/>
|
||||
<X
|
||||
className="text-[#666] size-4 cursor-pointer"
|
||||
onClick={() => setShowError(false)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{Object.entries(SearchData).map(([sourceName, items]) => {
|
||||
const showHeader = Object.entries(SearchData).length < 5;
|
||||
<ErrorSearch isError={isError} />
|
||||
|
||||
{Object.entries(searchData).map(([sourceName, items]) => {
|
||||
return (
|
||||
<div key={sourceName}>
|
||||
{showHeader && (
|
||||
@@ -261,7 +270,12 @@ function DropdownList({
|
||||
{hideArrowRight(item) ? (
|
||||
<div
|
||||
ref={(el) => (itemRefs.current[currentIndex] = el)}
|
||||
onMouseEnter={() => setSelectedItem(currentIndex)}
|
||||
onMouseEnter={memoizedCallbacks.onMouseEnter(
|
||||
currentIndex
|
||||
)}
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
id={`search-item-${currentIndex}`}
|
||||
>
|
||||
<Calculator item={item} isSelected={isSelected} />
|
||||
</div>
|
||||
@@ -271,7 +285,9 @@ function DropdownList({
|
||||
isSelected={isSelected}
|
||||
currentIndex={currentIndex}
|
||||
showIndex={showIndex}
|
||||
onMouseEnter={() => setSelectedItem(currentIndex)}
|
||||
onMouseEnter={memoizedCallbacks.onMouseEnter(
|
||||
currentIndex
|
||||
)}
|
||||
onItemClick={() => {
|
||||
if (item?.url) {
|
||||
OpenURLWithBrowser(item?.url);
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { ArrowBigLeft, Search, Send, Brain } from "lucide-react";
|
||||
import { Brain } from "lucide-react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import clsx from "clsx";
|
||||
import { useBoolean, useKeyPress } from "ahooks";
|
||||
import { useKeyPress } from "ahooks";
|
||||
|
||||
import ChatSwitch from "@/components/Common/ChatSwitch";
|
||||
import AutoResizeTextarea from "./AutoResizeTextarea";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import StopIcon from "@/icons/Stop";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { metaOrCtrlKey } from "@/utils/keyboardUtils";
|
||||
@@ -15,11 +14,14 @@ import SearchPopover from "./SearchPopover";
|
||||
import MCPPopover from "./MCPPopover";
|
||||
// import AudioRecording from "../AudioRecording";
|
||||
import { DataSource } from "@/types/commands";
|
||||
// import InputExtra from "./InputExtra";
|
||||
import InputExtra from "./InputExtra";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||
import Copyright from "@/components/Common/Copyright";
|
||||
import VisibleKey from "@/components/Common/VisibleKey";
|
||||
import ConnectionError from "./ConnectionError";
|
||||
import SearchIcons from "./SearchIcons";
|
||||
import ChatIcons from "./ChatIcons";
|
||||
|
||||
interface ChatInputProps {
|
||||
onSend: (message: string) => void;
|
||||
@@ -93,6 +95,15 @@ export default function ChatInput({
|
||||
hasModules = [],
|
||||
searchPlaceholder,
|
||||
chatPlaceholder,
|
||||
checkScreenPermission,
|
||||
requestScreenPermission,
|
||||
getScreenMonitors,
|
||||
getScreenWindows,
|
||||
captureWindowScreenshot,
|
||||
captureMonitorScreenshot,
|
||||
openFileDialog,
|
||||
getFileMetadata,
|
||||
getFileIcon,
|
||||
}: ChatInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -103,43 +114,24 @@ export default function ChatInput({
|
||||
const sourceData = useSearchStore((state) => state.sourceData);
|
||||
const setSourceData = useSearchStore((state) => state.setSourceData);
|
||||
|
||||
// const sessionId = useConnectStore((state) => state.currentSessionId);
|
||||
const sessionId = useConnectStore((state) => state.currentSessionId);
|
||||
const modifierKey = useShortcutsStore((state) => state.modifierKey);
|
||||
const modeSwitch = useShortcutsStore((state) => state.modeSwitch);
|
||||
const returnToInput = useShortcutsStore((state) => state.returnToInput);
|
||||
const deepThinking = useShortcutsStore((state) => state.deepThinking);
|
||||
const [isComposition, { setTrue, setFalse }] = useBoolean();
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
changeInput("");
|
||||
setSourceData(undefined);
|
||||
setIsCommandPressed(false);
|
||||
pressedKeys.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const textareaRef = useRef<{ reset: () => void; focus: () => void }>(null);
|
||||
|
||||
const { curChatEnd, connected } = useChatStore();
|
||||
|
||||
const [reconnectCountdown, setReconnectCountdown] = useState<number>(0);
|
||||
useEffect(() => {
|
||||
if (!reconnectCountdown || connected) {
|
||||
setReconnectCountdown(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (reconnectCountdown > 0) {
|
||||
const timer = setTimeout(() => {
|
||||
setReconnectCountdown(reconnectCountdown - 1);
|
||||
}, 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [reconnectCountdown, connected]);
|
||||
|
||||
const [_isCommandPressed, setIsCommandPressed] = useState(false);
|
||||
const setModifierKeyPressed = useShortcutsStore((state) => {
|
||||
return state.setModifierKeyPressed;
|
||||
});
|
||||
@@ -148,7 +140,6 @@ export default function ChatInput({
|
||||
useEffect(() => {
|
||||
const handleFocus = () => {
|
||||
setBlurred(false);
|
||||
setIsCommandPressed(false);
|
||||
setModifierKeyPressed(false);
|
||||
};
|
||||
|
||||
@@ -160,12 +151,8 @@ export default function ChatInput({
|
||||
}, []);
|
||||
|
||||
const handleToggleFocus = useCallback(() => {
|
||||
if (isChatMode) {
|
||||
textareaRef.current?.focus();
|
||||
} else {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}, [isChatMode, textareaRef, inputRef]);
|
||||
textareaRef.current?.focus();
|
||||
}, [textareaRef]);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
const trimmedValue = inputValue.trim();
|
||||
@@ -188,16 +175,11 @@ export default function ChatInput({
|
||||
(e: KeyboardEvent) => {
|
||||
pressedKeys.add(e.key);
|
||||
|
||||
if (e.key === metaOrCtrlKey()) {
|
||||
setIsCommandPressed(true);
|
||||
}
|
||||
|
||||
if (pressedKeys.has(metaOrCtrlKey())) {
|
||||
// e.preventDefault();
|
||||
switch (e.code) {
|
||||
case "Comma":
|
||||
setIsCommandPressed(false);
|
||||
break;
|
||||
console.log("Comma");
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
setSourceData(undefined);
|
||||
@@ -230,7 +212,6 @@ export default function ChatInput({
|
||||
isChatMode,
|
||||
handleSubmit,
|
||||
setSourceData,
|
||||
setIsCommandPressed,
|
||||
disabledChange,
|
||||
curChatEnd,
|
||||
visibleContextMenu,
|
||||
@@ -240,10 +221,19 @@ export default function ChatInput({
|
||||
const handleKeyUp = useCallback((e: KeyboardEvent) => {
|
||||
pressedKeys.delete(e.key);
|
||||
if (e.key === metaOrCtrlKey()) {
|
||||
setIsCommandPressed(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(value: string) => {
|
||||
changeInput(value);
|
||||
if (!isChatMode) {
|
||||
onSend(value);
|
||||
}
|
||||
},
|
||||
[changeInput, isChatMode, onSend]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
window.addEventListener("keyup", handleKeyUp);
|
||||
@@ -258,11 +248,7 @@ export default function ChatInput({
|
||||
let unlisten: (() => void) | undefined;
|
||||
|
||||
setupWindowFocusListener(() => {
|
||||
if (isChatMode) {
|
||||
textareaRef.current?.focus();
|
||||
} else {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
textareaRef.current?.focus();
|
||||
}).then((unlistener) => {
|
||||
unlisten = unlistener;
|
||||
});
|
||||
@@ -272,11 +258,9 @@ export default function ChatInput({
|
||||
};
|
||||
}, [isChatMode]);
|
||||
|
||||
const DeepThinkClick = () => {
|
||||
setIsDeepThinkActive();
|
||||
};
|
||||
const [lineCount, setLineCount] = useState(1);
|
||||
|
||||
const source = currentAssistant?._source
|
||||
const source = currentAssistant?._source;
|
||||
|
||||
return (
|
||||
<div className={`w-full relative`}>
|
||||
@@ -284,111 +268,91 @@ export default function ChatInput({
|
||||
className={`p-2 flex items-center dark:text-[#D8D8D8] bg-[#ededed] dark:bg-[#202126] rounded-md transition-all relative overflow-hidden`}
|
||||
>
|
||||
<div className="flex flex-wrap gap-2 flex-1 items-center relative">
|
||||
{!isChatMode && !sourceData ? (
|
||||
<Search className="w-4 h-4 text-[#ccc] dark:text-[#d8d8d8]" />
|
||||
) : !isChatMode && sourceData ? (
|
||||
<ArrowBigLeft
|
||||
className="w-4 h-4 text-[#ccc] dark:text-[#d8d8d8] cursor-pointer"
|
||||
onClick={() => setSourceData(undefined)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{isChatMode ? (
|
||||
<AutoResizeTextarea
|
||||
ref={textareaRef}
|
||||
input={inputValue}
|
||||
setInput={(value: string) => {
|
||||
changeInput(value);
|
||||
}}
|
||||
connected={connected}
|
||||
handleKeyDown={(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key !== "Enter") return;
|
||||
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}}
|
||||
chatPlaceholder={chatPlaceholder}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
spellCheck="false"
|
||||
className="text-base font-normal flex-1 outline-none min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent"
|
||||
placeholder={
|
||||
searchPlaceholder || t("search.input.searchPlaceholder")
|
||||
}
|
||||
value={inputValue}
|
||||
onCompositionStart={setTrue}
|
||||
onCompositionEnd={() => {
|
||||
setTimeout(setFalse, 0);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter") return;
|
||||
|
||||
if (isComposition) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}}
|
||||
onChange={(e) => {
|
||||
onSend(e.target.value);
|
||||
}}
|
||||
{lineCount === 1 && (
|
||||
<SearchIcons
|
||||
lineCount={lineCount}
|
||||
isChatMode={isChatMode}
|
||||
sourceData={sourceData}
|
||||
setSourceData={setSourceData}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AutoResizeTextarea
|
||||
ref={textareaRef}
|
||||
input={inputValue}
|
||||
setInput={handleInputChange}
|
||||
connected={connected}
|
||||
handleKeyDown={(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
const { key, shiftKey } = e;
|
||||
|
||||
if (key !== "Enter" || shiftKey) return;
|
||||
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
|
||||
if (!isChatMode) {
|
||||
onSend(inputValue);
|
||||
}
|
||||
}}
|
||||
chatPlaceholder={
|
||||
isChatMode
|
||||
? chatPlaceholder
|
||||
: searchPlaceholder || t("search.input.searchPlaceholder")
|
||||
}
|
||||
onLineCountChange={setLineCount}
|
||||
/>
|
||||
|
||||
{lineCount > 1 && (
|
||||
<SearchIcons
|
||||
lineCount={lineCount}
|
||||
isChatMode={isChatMode}
|
||||
sourceData={sourceData}
|
||||
setSourceData={setSourceData}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ChatIcons
|
||||
lineCount={lineCount}
|
||||
isChatMode={isChatMode}
|
||||
curChatEnd={curChatEnd}
|
||||
inputValue={inputValue}
|
||||
onSend={onSend}
|
||||
disabledChange={disabledChange}
|
||||
/>
|
||||
|
||||
{showTooltip && !isChatMode && sourceData && (
|
||||
<div className="absolute -top-[5px] left-2">
|
||||
<div
|
||||
className={`absolute ${
|
||||
lineCount === 1 ? "-top-[5px]" : "top-[calc(100%-25px)]"
|
||||
} left-2`}
|
||||
>
|
||||
<VisibleKey shortcut="←" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showTooltip && (
|
||||
<div
|
||||
className={clsx("absolute -top-[5px] left-2", {
|
||||
"left-8": !isChatMode && sourceData,
|
||||
})}
|
||||
className={clsx(
|
||||
`absolute ${
|
||||
lineCount === 1 ? "-top-[5px]" : "top-[calc(100%-25px)]"
|
||||
} left-2`,
|
||||
{
|
||||
"left-8": !isChatMode && sourceData,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<VisibleKey shortcut={returnToInput} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* <AudioRecording
|
||||
{/* <AudioRecording
|
||||
key={isChatMode ? "chat" : "search"}
|
||||
onChange={(text) => {
|
||||
changeInput(inputValue + text);
|
||||
}}
|
||||
/> */}
|
||||
|
||||
{isChatMode && curChatEnd ? (
|
||||
<button
|
||||
className={`ml-1 p-1 ${
|
||||
inputValue
|
||||
? "bg-[#0072FF]"
|
||||
: "bg-[#E4E5F0] dark:bg-[rgb(84,84,84)]"
|
||||
} rounded-full transition-colors`}
|
||||
type="submit"
|
||||
onClick={() => onSend(inputValue.trim())}
|
||||
>
|
||||
<Send className="w-4 h-4 text-white" />
|
||||
</button>
|
||||
) : null}
|
||||
{isChatMode && !curChatEnd ? (
|
||||
<button
|
||||
className={`ml-1 px-1 bg-[#0072FF] rounded-full transition-colors`}
|
||||
type="submit"
|
||||
onClick={() => disabledChange()}
|
||||
>
|
||||
<StopIcon
|
||||
size={16}
|
||||
className="w-4 h-4 text-white"
|
||||
aria-label="Stop message"
|
||||
/>
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
{/* {showTooltip && isChatMode && isCommandPressed ? (
|
||||
{/* {showTooltip && isChatMode && isCommandPressed ? (
|
||||
<div
|
||||
className={`absolute right-10 w-4 h-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000]`}
|
||||
>
|
||||
@@ -396,47 +360,29 @@ export default function ChatInput({
|
||||
</div>
|
||||
) : null} */}
|
||||
|
||||
{showTooltip && isChatMode && (
|
||||
<div className="absolute top-[2px] right-[18px]">
|
||||
<VisibleKey shortcut="↩︎" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!connected && isChatMode ? (
|
||||
<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")}
|
||||
{showTooltip && isChatMode && (
|
||||
<div
|
||||
className="px-1 h-[24px] text-[#0061FF] font-normal text-xs flex items-center justify-center cursor-pointer underline"
|
||||
onClick={() => {
|
||||
reconnect();
|
||||
setReconnectCountdown(10);
|
||||
}}
|
||||
className={`absolute ${
|
||||
lineCount === 1 ? "-top-[5px]" : "top-[calc(100%-30px)]"
|
||||
} right-[12px]`}
|
||||
>
|
||||
{reconnectCountdown > 0 ? (
|
||||
`${t("search.input.connecting")}(${reconnectCountdown}s)`
|
||||
) : (
|
||||
<VisibleKey
|
||||
shortcut="R"
|
||||
onKeyPress={() => {
|
||||
reconnect();
|
||||
setReconnectCountdown(10);
|
||||
}}
|
||||
>
|
||||
{t("search.input.reconnect")}
|
||||
</VisibleKey>
|
||||
)}
|
||||
<VisibleKey shortcut="↩︎" />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
|
||||
{!connected && isChatMode ? (
|
||||
<ConnectionError reconnect={reconnect} connected={connected} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="flex justify-between items-center py-2"
|
||||
className="flex justify-between items-center pt-2"
|
||||
>
|
||||
{isChatMode ? (
|
||||
<div className="flex gap-2 text-[12px] leading-3 text-[#333] dark:text-[#d8d8d8]">
|
||||
{/* {sessionId && (
|
||||
{sessionId && (
|
||||
<InputExtra
|
||||
checkScreenPermission={checkScreenPermission}
|
||||
requestScreenPermission={requestScreenPermission}
|
||||
@@ -448,7 +394,7 @@ export default function ChatInput({
|
||||
getFileMetadata={getFileMetadata}
|
||||
getFileIcon={getFileIcon}
|
||||
/>
|
||||
)} */}
|
||||
)}
|
||||
|
||||
{source?.type === "deep_think" && source?.config?.visible && (
|
||||
<button
|
||||
@@ -458,9 +404,12 @@ export default function ChatInput({
|
||||
"!bg-[rgba(0,114,255,0.3)]": isDeepThinkActive,
|
||||
}
|
||||
)}
|
||||
onClick={DeepThinkClick}
|
||||
onClick={setIsDeepThinkActive}
|
||||
>
|
||||
<VisibleKey shortcut={deepThinking} onKeyPress={DeepThinkClick}>
|
||||
<VisibleKey
|
||||
shortcut={deepThinking}
|
||||
onKeyPress={setIsDeepThinkActive}
|
||||
>
|
||||
<Brain
|
||||
className={`size-3 ${
|
||||
isDeepThinkActive
|
||||
@@ -481,7 +430,7 @@ export default function ChatInput({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{source?.datasource?.visible && (
|
||||
{source?.datasource?.enabled && source?.datasource?.visible && (
|
||||
<SearchPopover
|
||||
isSearchActive={isSearchActive}
|
||||
setIsSearchActive={setIsSearchActive}
|
||||
@@ -489,7 +438,7 @@ export default function ChatInput({
|
||||
/>
|
||||
)}
|
||||
|
||||
{source?.mcp_servers?.visible && (
|
||||
{source?.mcp_servers?.enabled && source?.mcp_servers?.visible && (
|
||||
<MCPPopover
|
||||
isMCPActive={isMCPActive}
|
||||
setIsMCPActive={setIsMCPActive}
|
||||
@@ -497,9 +446,9 @@ export default function ChatInput({
|
||||
/>
|
||||
)}
|
||||
|
||||
{!source?.datasource?.visible &&
|
||||
{!(source?.datasource?.enabled && source?.datasource?.visible) &&
|
||||
(source?.type !== "deep_think" || !source?.config?.visible) &&
|
||||
!source?.mcp_servers?.visible ? (
|
||||
!(source?.mcp_servers?.enabled && source?.mcp_servers?.visible) ? (
|
||||
<div className="px-[9px]">
|
||||
<Copyright />
|
||||
</div>
|
||||
|
||||
@@ -73,6 +73,7 @@ const InputExtra = ({
|
||||
const modifierKeyPressed = useShortcutsStore((state) => {
|
||||
return state.modifierKeyPressed;
|
||||
});
|
||||
const addError = useAppStore((state) => state.addError);
|
||||
|
||||
const state = useReactive<State>({
|
||||
screenshotableMonitors: [],
|
||||
@@ -104,6 +105,8 @@ const InputExtra = ({
|
||||
const stat = await getFileMetadata(path);
|
||||
|
||||
if (stat.size / 1024 / 1024 > 100) {
|
||||
addError(t("search.input.uploadFileHints.maxSize"));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -184,8 +187,8 @@ const InputExtra = ({
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton className="size-6">
|
||||
<Tooltip content="支持截图、上传文件,最多 50个,单个文件最大 100 MB。">
|
||||
<MenuButton as="div" className="size-6">
|
||||
<Tooltip content={t("search.input.uploadFileHints.tooltip")}>
|
||||
<div className="size-full flex justify-center items-center rounded-lg transition hover:bg-[#EDEDED] dark:hover:bg-[#202126]">
|
||||
<Plus
|
||||
className={clsx("size-5", {
|
||||
|
||||
@@ -24,7 +24,7 @@ import NoDataImage from "@/components/Common/NoDataImage";
|
||||
import PopoverInput from "../Common/PopoverInput";
|
||||
import FontIcon from "../Common/Icons/FontIcon";
|
||||
|
||||
interface SearchPopoverProps {
|
||||
interface MCPPopoverProps {
|
||||
isMCPActive: boolean;
|
||||
setIsMCPActive: () => void;
|
||||
getMCPByServer: (
|
||||
@@ -37,11 +37,11 @@ interface SearchPopoverProps {
|
||||
) => Promise<DataSource[]>;
|
||||
}
|
||||
|
||||
export default function SearchPopover({
|
||||
export default function MCPPopover({
|
||||
isMCPActive,
|
||||
setIsMCPActive,
|
||||
getMCPByServer,
|
||||
}: SearchPopoverProps) {
|
||||
}: MCPPopoverProps) {
|
||||
const { t } = useTranslation();
|
||||
const { connected } = useChatStore();
|
||||
|
||||
@@ -261,7 +261,7 @@ export default function SearchPopover({
|
||||
</div>
|
||||
|
||||
{visibleList.length > 0 ? (
|
||||
<ul className="flex flex-col gap-2">
|
||||
<ul className="flex flex-col gap-2 p-0">
|
||||
{visibleList?.map((item, index) => {
|
||||
const { id, name } = item;
|
||||
|
||||
|
||||
@@ -45,15 +45,22 @@ function Search({
|
||||
setWindowAlwaysOnTop,
|
||||
}: SearchProps) {
|
||||
const sourceData = useSearchStore((state) => state.sourceData);
|
||||
const queryTimeout = useConnectStore((state) => state.querySourceTimeout);
|
||||
const querySourceTimeout = useConnectStore((state) => {
|
||||
return state.querySourceTimeout;
|
||||
});
|
||||
|
||||
const [IsError, setIsError] = useState<boolean>(false);
|
||||
const [isError, setIsError] = useState<any[]>([]);
|
||||
const [suggests, setSuggests] = useState<any[]>([]);
|
||||
const [SearchData, setSearchData] = useState<any>({});
|
||||
const [searchData, setSearchData] = useState<any>({});
|
||||
const [isSearchComplete, setIsSearchComplete] = useState(false);
|
||||
|
||||
const mainWindowRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const querySourceTimeoutRef = useRef(querySourceTimeout);
|
||||
useEffect(() => {
|
||||
querySourceTimeoutRef.current = querySourceTimeout;
|
||||
}, [querySourceTimeout]);
|
||||
|
||||
const getSuggest = useCallback(
|
||||
async (searchInput: string) => {
|
||||
if (!searchInput) return;
|
||||
@@ -65,11 +72,11 @@ function Search({
|
||||
from: 0,
|
||||
size: 10,
|
||||
queryStrings: { query: searchInput },
|
||||
queryTimeout: queryTimeout,
|
||||
queryTimeout: querySourceTimeoutRef.current,
|
||||
});
|
||||
if (response && typeof response === "object" && "failed" in response) {
|
||||
const failedResult = response as any;
|
||||
setIsError(!!failedResult.failed?.length);
|
||||
setIsError(failedResult.failed || []);
|
||||
}
|
||||
} else {
|
||||
const [error, res]: any = await Get(
|
||||
@@ -130,13 +137,13 @@ function Search({
|
||||
<div ref={mainWindowRef} className={`h-full pb-10 w-full relative`}>
|
||||
{/* Search Results Panel */}
|
||||
{suggests.length > 0 ? (
|
||||
sourceData ? (
|
||||
sourceData ? (
|
||||
<SearchResults input={input} isChatMode={isChatMode} />
|
||||
) : (
|
||||
<DropdownList
|
||||
suggests={suggests}
|
||||
SearchData={SearchData}
|
||||
IsError={IsError}
|
||||
searchData={searchData}
|
||||
isError={isError}
|
||||
isSearchComplete={isSearchComplete}
|
||||
isChatMode={isChatMode}
|
||||
/>
|
||||
|
||||
34
src/components/Search/SearchIcons.tsx
Normal file
34
src/components/Search/SearchIcons.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { ArrowBigLeft, Search } from "lucide-react";
|
||||
|
||||
interface SearchIconsProps {
|
||||
lineCount: number;
|
||||
isChatMode: boolean;
|
||||
sourceData: any;
|
||||
setSourceData: (data: any | undefined) => void;
|
||||
}
|
||||
|
||||
export default function SearchIcons({
|
||||
lineCount,
|
||||
isChatMode,
|
||||
sourceData,
|
||||
setSourceData,
|
||||
}: SearchIconsProps) {
|
||||
if (isChatMode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const iconContent = !sourceData ? (
|
||||
<Search className="w-4 h-4 text-[#ccc] dark:text-[#d8d8d8]" />
|
||||
) : (
|
||||
<ArrowBigLeft
|
||||
className="w-4 h-4 text-[#ccc] dark:text-[#d8d8d8] cursor-pointer"
|
||||
onClick={() => setSourceData(undefined)}
|
||||
/>
|
||||
);
|
||||
|
||||
if (lineCount === 1) {
|
||||
return <>{iconContent}</>;
|
||||
} else {
|
||||
return <div className="w-full flex items-center">{iconContent}</div>;
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,9 @@ const SearchListItem: React.FC<SearchListItemProps> = React.memo(
|
||||
"gap-7 mobile:gap-1": showListRight,
|
||||
}
|
||||
)}
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
id={`search-item-${currentIndex}`}
|
||||
>
|
||||
<div
|
||||
className={`${
|
||||
|
||||
@@ -266,7 +266,7 @@ export default function SearchPopover({
|
||||
</div>
|
||||
|
||||
{visibleList.length > 0 ? (
|
||||
<ul className="flex flex-col gap-2">
|
||||
<ul className="flex flex-col gap-2 p-0">
|
||||
{visibleList?.map((item, index) => {
|
||||
const { id, name } = item;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Suspense,
|
||||
memo,
|
||||
useState,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import clsx from "clsx";
|
||||
import { useMount } from "ahooks";
|
||||
@@ -13,7 +14,6 @@ import { useMount } from "ahooks";
|
||||
import Search from "@/components/Search/Search";
|
||||
import InputBox from "@/components/Search/InputBox";
|
||||
import ChatAI, { ChatAIRef } from "@/components/Assistant/Chat";
|
||||
import UpdateApp from "@/components/UpdateApp";
|
||||
import { isLinux, isWin } from "@/utils/platform";
|
||||
import { appReducer, initialAppState } from "@/reducers/appReducer";
|
||||
import { useWindowEvents } from "@/hooks/useWindowEvents";
|
||||
@@ -25,6 +25,7 @@ import { DataSource } from "@/types/commands";
|
||||
import { useThemeStore } from "@/stores/themeStore";
|
||||
import { Get } from "@/api/axiosRequest";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { useAppearanceStore } from "@/stores/appearanceStore";
|
||||
|
||||
interface SearchChatProps {
|
||||
isTauri?: boolean;
|
||||
@@ -60,11 +61,13 @@ function SearchChat({
|
||||
}: SearchChatProps) {
|
||||
const currentAssistant = useConnectStore((state) => state.currentAssistant);
|
||||
|
||||
const source = currentAssistant?._source;
|
||||
|
||||
const customInitialState = {
|
||||
...initialAppState,
|
||||
isDeepThinkActive: currentAssistant?._source?.type === "deep_think",
|
||||
isSearchActive: currentAssistant?._source?.datasource?.enabled === true,
|
||||
isMCPActive: currentAssistant?._source?.mcp_servers?.enabled === true,
|
||||
isDeepThinkActive: source?.type === "deep_think",
|
||||
isSearchActive: source?.datasource?.enabled_by_default === true,
|
||||
isMCPActive: source?.mcp_servers?.enabled_by_default === true,
|
||||
};
|
||||
|
||||
const [state, dispatch] = useReducer(appReducer, customInitialState);
|
||||
@@ -89,32 +92,46 @@ function SearchChat({
|
||||
|
||||
const setTheme = useThemeStore((state) => state.setTheme);
|
||||
|
||||
const isChatModeRef = useRef(false);
|
||||
useEffect(() => {
|
||||
isChatModeRef.current = isChatMode;
|
||||
}, [isChatMode]);
|
||||
|
||||
useMount(async () => {
|
||||
const isWin10 = await platformAdapter.isWindows10();
|
||||
|
||||
setIsWin10(isWin10);
|
||||
|
||||
const unlisten = platformAdapter.listenEvent("show-coco", () => {
|
||||
console.log("show-coco");
|
||||
|
||||
platformAdapter.invokeBackend("simulate_mouse_click", {
|
||||
isChatMode: isChatModeRef.current,
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
// Cleanup logic if needed
|
||||
unlisten.then((fn) => fn());
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
const init = async () => {
|
||||
if (!mounted) return;
|
||||
await initializeListeners();
|
||||
await initializeListeners_auth();
|
||||
await platformAdapter.invokeBackend("get_app_search_source");
|
||||
if (theme && mounted) {
|
||||
setTheme(theme);
|
||||
}
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!theme) return;
|
||||
|
||||
setTheme(theme);
|
||||
}, [theme]);
|
||||
|
||||
const chatAIRef = useRef<ChatAIRef>(null);
|
||||
|
||||
const changeMode = useCallback(async (value: boolean) => {
|
||||
@@ -173,6 +190,17 @@ function SearchChat({
|
||||
return platformAdapter.setAlwaysOnTop(isPinned);
|
||||
}, []);
|
||||
|
||||
const assistantConfig = useMemo(() => {
|
||||
return {
|
||||
datasourceEnabled: source?.datasource?.enabled,
|
||||
datasourceVisible: source?.datasource?.visible,
|
||||
datasourceIds: source?.datasource?.ids,
|
||||
mcpEnabled: source?.mcp_servers?.enabled,
|
||||
mcpVisible: source?.mcp_servers?.visible,
|
||||
mcpIds: source?.mcp_servers?.ids,
|
||||
};
|
||||
}, [currentAssistant]);
|
||||
|
||||
const getDataSourcesByServer = useCallback(
|
||||
async (
|
||||
serverId: string,
|
||||
@@ -182,6 +210,13 @@ function SearchChat({
|
||||
query?: string;
|
||||
}
|
||||
): Promise<DataSource[]> => {
|
||||
if (
|
||||
!(
|
||||
assistantConfig.datasourceEnabled && assistantConfig.datasourceVisible
|
||||
)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
response = await platformAdapter.invokeBackend("datasource_search", {
|
||||
@@ -202,13 +237,13 @@ function SearchChat({
|
||||
};
|
||||
});
|
||||
}
|
||||
let ids = currentAssistant?._source?.datasource?.ids;
|
||||
let ids = assistantConfig.datasourceIds;
|
||||
if (Array.isArray(ids) && ids.length > 0 && !ids.includes("*")) {
|
||||
response = response?.filter((item: any) => ids.includes(item.id));
|
||||
}
|
||||
return response || [];
|
||||
},
|
||||
[JSON.stringify(currentAssistant)]
|
||||
[assistantConfig]
|
||||
);
|
||||
|
||||
const getMCPByServer = useCallback(
|
||||
@@ -220,6 +255,9 @@ function SearchChat({
|
||||
query?: string;
|
||||
}
|
||||
): Promise<DataSource[]> => {
|
||||
if (!(assistantConfig.mcpEnabled && assistantConfig.mcpVisible)) {
|
||||
return [];
|
||||
}
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
response = await platformAdapter.invokeBackend("mcp_server_search", {
|
||||
@@ -240,13 +278,13 @@ function SearchChat({
|
||||
};
|
||||
});
|
||||
}
|
||||
let ids = currentAssistant?._source?.mcp_servers?.ids;
|
||||
let ids = assistantConfig.mcpIds;
|
||||
if (Array.isArray(ids) && ids.length > 0 && !ids.includes("*")) {
|
||||
response = response?.filter((item: any) => ids.includes(item.id));
|
||||
}
|
||||
return response || [];
|
||||
},
|
||||
[JSON.stringify(currentAssistant)]
|
||||
[assistantConfig]
|
||||
);
|
||||
|
||||
const setupWindowFocusListener = useCallback(async (callback: () => void) => {
|
||||
@@ -289,18 +327,12 @@ function SearchChat({
|
||||
return platformAdapter.getFileIcon(path, size);
|
||||
}, []);
|
||||
|
||||
const checkUpdate = useCallback(async () => {
|
||||
return platformAdapter.checkUpdate();
|
||||
}, []);
|
||||
|
||||
const relaunchApp = useCallback(async () => {
|
||||
return platformAdapter.relaunchApp();
|
||||
}, []);
|
||||
|
||||
const defaultStartupWindow = useStartupStore((state) => {
|
||||
return state.defaultStartupWindow;
|
||||
});
|
||||
|
||||
const opacity = useAppearanceStore((state) => state.opacity);
|
||||
|
||||
useEffect(() => {
|
||||
if (platformAdapter.isTauri()) {
|
||||
changeMode(defaultStartupWindow === "chatMode");
|
||||
@@ -317,7 +349,7 @@ function SearchChat({
|
||||
<div
|
||||
data-tauri-drag-region={isTauri}
|
||||
className={clsx(
|
||||
"m-auto overflow-hidden relative bg-no-repeat bg-cover bg-center bg-white dark:bg-black",
|
||||
"m-auto overflow-hidden relative bg-no-repeat bg-cover bg-center bg-white dark:bg-black flex flex-col",
|
||||
[
|
||||
isTransitioned
|
||||
? "bg-chat_bg_light dark:bg-chat_bg_dark"
|
||||
@@ -329,16 +361,35 @@ function SearchChat({
|
||||
"rounded-xl": !isMobile && !isWin,
|
||||
"border border-[#E6E6E6] dark:border-[#272626]": isTauri && isLinux,
|
||||
"border-t border-t-[#999] dark:border-t-[#333]": isTauri && isWin10,
|
||||
"opacity-30": blurred,
|
||||
}
|
||||
)}
|
||||
style={{ opacity: blurred ? (opacity ?? 30) / 100 : 1 }}
|
||||
>
|
||||
{isTransitioned && (
|
||||
<div
|
||||
data-tauri-drag-region={isTauri}
|
||||
className="flex-1 w-full overflow-hidden"
|
||||
>
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<ChatAI
|
||||
ref={chatAIRef}
|
||||
key="ChatAI"
|
||||
changeInput={setInput}
|
||||
isSearchActive={isSearchActive}
|
||||
isDeepThinkActive={isDeepThinkActive}
|
||||
isMCPActive={isMCPActive}
|
||||
getFileUrl={getFileUrl}
|
||||
showChatHistory={showChatHistory}
|
||||
assistantIDs={assistantIDs}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
data-tauri-drag-region={isTauri}
|
||||
className={`p-2 absolute w-full flex justify-center transition-all duration-500 ${
|
||||
isTransitioned
|
||||
? "top-[calc(100%-82px)] h-[82px] border-t"
|
||||
: "top-0 h-[82px] border-b"
|
||||
className={`p-2 w-full flex justify-center transition-all duration-500 min-h-[82px] ${
|
||||
isTransitioned ? "border-t" : "border-b"
|
||||
} border-[#E6E6E6] dark:border-[#272626]`}
|
||||
>
|
||||
<InputBox
|
||||
@@ -375,50 +426,25 @@ function SearchChat({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-tauri-drag-region={isTauri}
|
||||
className={`absolute w-full transition-opacity duration-500 ${
|
||||
isTransitioned ? "opacity-0 pointer-events-none" : "opacity-100"
|
||||
} bottom-0 h-[calc(100%-82px)] `}
|
||||
>
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<Search
|
||||
key="Search"
|
||||
isTauri={isTauri}
|
||||
input={input}
|
||||
isChatMode={isChatMode}
|
||||
changeInput={setInput}
|
||||
hideCoco={hideCoco}
|
||||
openSetting={openSetting}
|
||||
setWindowAlwaysOnTop={setWindowAlwaysOnTop}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-tauri-drag-region={isTauri}
|
||||
className={`absolute w-full transition-all duration-500 select-auto ${
|
||||
isTransitioned
|
||||
? "top-0 opacity-100 pointer-events-auto"
|
||||
: "-top-[506px] opacity-0 pointer-events-none"
|
||||
} h-[calc(100%-90px)]`}
|
||||
>
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<ChatAI
|
||||
ref={chatAIRef}
|
||||
key="ChatAI"
|
||||
changeInput={setInput}
|
||||
isSearchActive={isSearchActive}
|
||||
isDeepThinkActive={isDeepThinkActive}
|
||||
isMCPActive={isMCPActive}
|
||||
getFileUrl={getFileUrl}
|
||||
showChatHistory={showChatHistory}
|
||||
assistantIDs={assistantIDs}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
<UpdateApp checkUpdate={checkUpdate} relaunchApp={relaunchApp} />
|
||||
{!isTransitioned && (
|
||||
<div
|
||||
data-tauri-drag-region={isTauri}
|
||||
className="flex-1 w-full overflow-auto"
|
||||
>
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<Search
|
||||
key="Search"
|
||||
isTauri={isTauri}
|
||||
input={input}
|
||||
isChatMode={isChatMode}
|
||||
changeInput={setInput}
|
||||
hideCoco={hideCoco}
|
||||
openSetting={openSetting}
|
||||
setWindowAlwaysOnTop={setWindowAlwaysOnTop}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import SettingsInput from "@/components/Settings/SettingsInput";
|
||||
import SettingsItem from "@/components/Settings/SettingsItem";
|
||||
import { useAppearanceStore } from "@/stores/appearanceStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { AppWindowMac } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Appearance = () => {
|
||||
const { t } = useTranslation();
|
||||
const opacity = useAppearanceStore((state) => state.opacity);
|
||||
const setOpacity = useAppearanceStore((state) => state.setOpacity);
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = useAppearanceStore.subscribe((state) => {
|
||||
platformAdapter.emitEvent("change-appearance-store", state);
|
||||
});
|
||||
|
||||
return unlisten;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
{t("settings.advanced.appearance.title")}
|
||||
</h2>
|
||||
|
||||
<SettingsItem
|
||||
icon={AppWindowMac}
|
||||
title={t("settings.advanced.appearance.opacity.title")}
|
||||
description={t("settings.advanced.appearance.opacity.description")}
|
||||
>
|
||||
<SettingsInput
|
||||
type="number"
|
||||
min={10}
|
||||
max={100}
|
||||
value={opacity}
|
||||
onChange={(value) => {
|
||||
return setOpacity(!value ? void 0 : Number(value));
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Appearance;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Command, RotateCcw } from "lucide-react";
|
||||
import { ChangeEvent, useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { formatKey } from "@/utils/keyboardUtils";
|
||||
import SettingsItem from "@/components/Settings/SettingsItem";
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { ModifierKey } from "@/types/index";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import SettingsInput from "@/components/Settings/SettingsInput";
|
||||
|
||||
export const modifierKeys: ModifierKey[] = isMac
|
||||
? ["meta", "ctrl"]
|
||||
@@ -234,12 +235,7 @@ const Shortcuts = () => {
|
||||
},
|
||||
];
|
||||
|
||||
const handleChange = (
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
setValue: (value: string) => void
|
||||
) => {
|
||||
const value = event.target.value.toUpperCase();
|
||||
|
||||
const handleChange = (value: string, setValue: (value: string) => void) => {
|
||||
if (value.length > 1) return;
|
||||
|
||||
const systemKeys = ["C", "V", "X", "Z", "Q", "H"];
|
||||
@@ -284,7 +280,11 @@ const Shortcuts = () => {
|
||||
}}
|
||||
>
|
||||
{modifierKeys.map((item) => {
|
||||
return <option value={item}>{formatKey(item)}</option>;
|
||||
return (
|
||||
<option key={item} value={item}>
|
||||
{formatKey(item)}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</SettingsItem>
|
||||
@@ -302,12 +302,11 @@ const Shortcuts = () => {
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{formatKey(modifierKey)}</span>
|
||||
<span>+</span>
|
||||
<input
|
||||
className="w-20 h-8 px-2 rounded-md border bg-transparent border-black/5 dark:border-white/10 hover:border-[#0072FF] focus:border-[#0072FF] transition"
|
||||
<SettingsInput
|
||||
value={value}
|
||||
maxLength={1}
|
||||
onChange={(event) => {
|
||||
handleChange(event, setValue);
|
||||
max={1}
|
||||
onChange={(value) => {
|
||||
handleChange(String(value).toUpperCase(), setValue);
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import SettingsItem from "@/components/Settings/SettingsItem";
|
||||
import SettingsToggle from "@/components/Settings/SettingsToggle";
|
||||
import { useAppearanceStore } from "@/stores/appearanceStore";
|
||||
import { FlaskConical } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const UpdateSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const snapshotUpdate = useAppearanceStore((state) => state.snapshotUpdate);
|
||||
const setSnapshotUpdate = useAppearanceStore((state) => {
|
||||
return state.setSnapshotUpdate;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
{t("settings.advanced.updateVersion.title")}
|
||||
</h2>
|
||||
|
||||
<SettingsItem
|
||||
icon={FlaskConical}
|
||||
title={t("settings.advanced.updateVersion.snapshotUpdate.title")}
|
||||
description={t(
|
||||
"settings.advanced.updateVersion.snapshotUpdate.description"
|
||||
)}
|
||||
>
|
||||
<SettingsToggle
|
||||
label={t("settings.advanced.updateVersion.snapshotUpdate.title")}
|
||||
checked={snapshotUpdate}
|
||||
onChange={() => {
|
||||
setSnapshotUpdate(!snapshotUpdate);
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateSettings;
|
||||
@@ -1,11 +1,23 @@
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
AppWindowMac,
|
||||
MessageSquareMore,
|
||||
Search,
|
||||
ShieldCheck,
|
||||
Unplug,
|
||||
} from "lucide-react";
|
||||
import { useMount } from "ahooks";
|
||||
|
||||
import Shortcuts from "./components/Shortcuts";
|
||||
import SettingsItem from "../SettingsItem";
|
||||
import { AppWindowMac, MessageSquareMore, Search, Unplug } from "lucide-react";
|
||||
import { useStartupStore } from "@/stores/startupStore";
|
||||
import { useEffect } from "react";
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import Appearance from "./components/Appearance";
|
||||
import SettingsInput from "../SettingsInput";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import UpdateSettings from "./components/UpdateSettings";
|
||||
import SettingsToggle from "../SettingsToggle";
|
||||
|
||||
const Advanced = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -39,14 +51,28 @@ const Advanced = () => {
|
||||
const setQueryTimeout = useConnectStore((state) => {
|
||||
return state.setQuerySourceTimeout;
|
||||
});
|
||||
const allowSelfSignature = useConnectStore((state) => {
|
||||
return state.allowSelfSignature;
|
||||
});
|
||||
const setAllowSelfSignature = useConnectStore((state) => {
|
||||
return state.setAllowSelfSignature;
|
||||
});
|
||||
|
||||
useMount(async () => {
|
||||
const allowSelfSignature = await platformAdapter.invokeBackend<boolean>(
|
||||
"get_allow_self_signature"
|
||||
);
|
||||
|
||||
setAllowSelfSignature(allowSelfSignature);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribeStartup = useStartupStore.subscribe((state) => {
|
||||
emit("change-startup-store", state);
|
||||
platformAdapter.emitEvent("change-startup-store", state);
|
||||
});
|
||||
|
||||
const unsubscribeConnect = useConnectStore.subscribe((state) => {
|
||||
emit("change-connect-store", state);
|
||||
platformAdapter.emitEvent("change-connect-store", state);
|
||||
});
|
||||
|
||||
return () => {
|
||||
@@ -165,13 +191,12 @@ const Advanced = () => {
|
||||
"settings.advanced.connect.connectionTimeout.description"
|
||||
)}
|
||||
>
|
||||
<input
|
||||
<SettingsInput
|
||||
type="number"
|
||||
min={10}
|
||||
value={connectionTimeout}
|
||||
className="w-20 h-8 px-2 rounded-md border bg-transparent border-black/5 dark:border-white/10"
|
||||
onChange={(event) => {
|
||||
setConnectionTimeout(Number(event.target.value) || 120);
|
||||
onChange={(value) => {
|
||||
setConnectionTimeout(!value ? void 0 : Number(value));
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
@@ -181,17 +206,40 @@ const Advanced = () => {
|
||||
title={t("settings.advanced.connect.queryTimeout.title")}
|
||||
description={t("settings.advanced.connect.queryTimeout.description")}
|
||||
>
|
||||
<input
|
||||
<SettingsInput
|
||||
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) || 500);
|
||||
onChange={(value) => {
|
||||
setQueryTimeout(!value ? void 0 : Number(value));
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
icon={ShieldCheck}
|
||||
title={t("settings.advanced.connect.allowSelfSignature.title")}
|
||||
description={t(
|
||||
"settings.advanced.connect.allowSelfSignature.description"
|
||||
)}
|
||||
>
|
||||
<SettingsToggle
|
||||
label={t("settings.advanced.connect.allowSelfSignature.title")}
|
||||
checked={allowSelfSignature}
|
||||
onChange={(value) => {
|
||||
setAllowSelfSignature(value);
|
||||
|
||||
platformAdapter.invokeBackend("set_allow_self_signature", {
|
||||
value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
|
||||
<Appearance />
|
||||
|
||||
<UpdateSettings />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import { cloneElement, FC, useContext, useState } from "react";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import SettingsToggle from "@/components/Settings/SettingsToggle";
|
||||
import { ExtensionsContext, Plugin } from "../..";
|
||||
|
||||
interface AccordionProps extends Plugin {}
|
||||
|
||||
const Accordion: FC<AccordionProps> = (props) => {
|
||||
const {
|
||||
id,
|
||||
icon,
|
||||
title,
|
||||
type = "Extension",
|
||||
alias = "-",
|
||||
hotKey = "-",
|
||||
enabled = true,
|
||||
content,
|
||||
} = props;
|
||||
const { activeId, setActiveId } = useContext(ExtensionsContext);
|
||||
|
||||
const [expand, setExpand] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={clsx("flex items-center h-8 -mx-2 px-2 text-sm rounded-md", {
|
||||
"bg-[#f0f6fe] dark:bg-gray-700": id === activeId,
|
||||
})}
|
||||
onClick={() => {
|
||||
setActiveId(id);
|
||||
}}
|
||||
>
|
||||
<div className="w-[220px] flex items-center gap-1">
|
||||
<div className="size-4">
|
||||
{content && (
|
||||
<ChevronRight
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
setExpand((prev) => !prev);
|
||||
}}
|
||||
className={clsx("size-full transition cursor-pointer", {
|
||||
"rotate-90": expand,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{cloneElement(icon, { className: "size-4" })}
|
||||
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex items-center text-[#999]">
|
||||
<div className="flex-1">{type}</div>
|
||||
<div className="flex-1">{alias}</div>
|
||||
<div className="flex-1">{hotKey}</div>
|
||||
<div className="flex-1 flex items-center justify-end">
|
||||
<SettingsToggle
|
||||
label=""
|
||||
checked={enabled}
|
||||
className="scale-75"
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{expand && <div className="text-sm">{content}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Accordion;
|
||||
@@ -1,68 +0,0 @@
|
||||
import SettingsToggle from "@/components/Settings/SettingsToggle";
|
||||
import { useApplicationsStore } from "@/stores/applicationsStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { useContext } from "react";
|
||||
import { ExtensionsContext } from "../../..";
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Applications = () => {
|
||||
const { t } = useTranslation();
|
||||
const { activeId, setActiveId } = useContext(ExtensionsContext);
|
||||
|
||||
const allApps = useApplicationsStore((state) => state.allApps);
|
||||
const disabledApps = useApplicationsStore((state) => state.disabledApps);
|
||||
const setDisabledApps = useApplicationsStore((state) => {
|
||||
return state.setDisabledApps;
|
||||
});
|
||||
|
||||
return allApps.map((app) => {
|
||||
const { name, icon } = app;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
className={clsx("flex items-center h-8 -mx-2 pl-10 pr-2 rounded-md", {
|
||||
"bg-[#f0f6fe] dark:bg-gray-700": name === activeId,
|
||||
})}
|
||||
onClick={() => {
|
||||
setActiveId(name);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-1 w-[180px] pr-2 overflow-hidden">
|
||||
<img src={platformAdapter.convertFileSrc(icon)} className="size-5" />
|
||||
|
||||
<span className="text-sm truncate">{name}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex items-center text-[#999] ">
|
||||
<div className="flex-1">
|
||||
{t("settings.extensions.application.title")}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
{t("settings.extensions.application.hits.addAlias")}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
{t("settings.extensions.application.hits.recordHotkey")}
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-end">
|
||||
<SettingsToggle
|
||||
label=""
|
||||
checked={!disabledApps.includes(name)}
|
||||
className="scale-75"
|
||||
onChange={() => {
|
||||
if (disabledApps.includes(name)) {
|
||||
setDisabledApps(disabledApps.filter((app) => app !== name));
|
||||
} else {
|
||||
setDisabledApps([...disabledApps, name]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default Applications;
|
||||
241
src/components/Settings/Extensions/components/Content/index.tsx
Normal file
241
src/components/Settings/Extensions/components/Content/index.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
import {
|
||||
cloneElement,
|
||||
FC,
|
||||
Fragment,
|
||||
MouseEvent,
|
||||
useContext,
|
||||
useState,
|
||||
} from "react";
|
||||
import { ExtensionsContext, Plugin } from "../..";
|
||||
import { useMount } from "ahooks";
|
||||
import { ChevronRight, LoaderCircle } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import { isArray, isFunction } from "lodash-es";
|
||||
import SettingsToggle from "@/components/Settings/SettingsToggle";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import Shortcut from "../Shortcut";
|
||||
import SettingsInput from "@/components/Settings/SettingsInput";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Content = () => {
|
||||
const { plugins } = useContext(ExtensionsContext);
|
||||
|
||||
return plugins.map((item) => {
|
||||
return <Item key={item.id} {...item} level={1} />;
|
||||
});
|
||||
};
|
||||
|
||||
const Item: FC<Plugin & { level: number }> = (props) => {
|
||||
const {
|
||||
id,
|
||||
icon,
|
||||
name,
|
||||
children,
|
||||
type = "Extension",
|
||||
manualLoad,
|
||||
level = 1,
|
||||
} = props;
|
||||
const { activeId, setActiveId, setPlugins } = useContext(ExtensionsContext);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const hasChildren = isArray(children);
|
||||
|
||||
const handleLoadChildren = async () => {
|
||||
setLoading(true);
|
||||
|
||||
await props.loadChildren?.();
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useMount(async () => {
|
||||
if (!manualLoad) {
|
||||
handleLoadChildren();
|
||||
}
|
||||
});
|
||||
|
||||
const handleExpand = async (event: MouseEvent) => {
|
||||
event?.stopPropagation();
|
||||
|
||||
if (expanded) {
|
||||
setExpanded(false);
|
||||
} else {
|
||||
if (manualLoad) {
|
||||
await handleLoadChildren();
|
||||
}
|
||||
|
||||
setExpanded(true);
|
||||
}
|
||||
};
|
||||
|
||||
const renderAlias = () => {
|
||||
const { alias, onAliasChange } = props;
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
if (isFunction(onAliasChange)) {
|
||||
return onAliasChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
if (isFunction(onAliasChange)) {
|
||||
return (
|
||||
<div
|
||||
className="-translate-x-2"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<SettingsInput
|
||||
defaultValue={alias}
|
||||
placeholder={t("settings.extensions.hits.addAlias")}
|
||||
className="!w-[90%] !h-6 !border-transparent rounded-[4px]"
|
||||
onChange={(value) => {
|
||||
handleChange(String(value));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>--</>;
|
||||
};
|
||||
|
||||
const renderHotkey = () => {
|
||||
const { hotkey, onHotkeyChange } = props;
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
if (isFunction(onHotkeyChange)) {
|
||||
return onHotkeyChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
if (isFunction(onHotkeyChange)) {
|
||||
return (
|
||||
<div
|
||||
className="-translate-x-2"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Shortcut
|
||||
value={hotkey}
|
||||
placeholder={t("settings.extensions.hits.recordHotkey")}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>--</>;
|
||||
};
|
||||
|
||||
const renderSwitch = () => {
|
||||
const { enabled = true, onEnabledChange } = props;
|
||||
|
||||
const handleChange = (value: boolean) => {
|
||||
if (isFunction(onEnabledChange)) {
|
||||
return onEnabledChange(value);
|
||||
}
|
||||
|
||||
const command = `${value ? "enable" : "disable"}_local_query_source`;
|
||||
|
||||
platformAdapter.invokeBackend(command, {
|
||||
querySourceId: id,
|
||||
});
|
||||
|
||||
setPlugins((prevPlugins) => {
|
||||
return prevPlugins.map((item) => {
|
||||
if (item.id === id) {
|
||||
return { ...item, enabled: value };
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<SettingsToggle
|
||||
label={id}
|
||||
checked={Boolean(enabled)}
|
||||
className="scale-75"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment key={id}>
|
||||
<div
|
||||
className={clsx("-mx-2 px-2 text-sm rounded-md", {
|
||||
"bg-[#f0f6fe] dark:bg-gray-700": id === activeId,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className="flex items-center justify-between gap-2 h-8"
|
||||
onClick={() => {
|
||||
setActiveId(id);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex-1 flex items-center gap-1 overflow-hidden"
|
||||
style={{ paddingLeft: (level - 1) * 20 }}
|
||||
>
|
||||
<div className="min-w-4 h-4">
|
||||
{hasChildren && (
|
||||
<>
|
||||
{loading ? (
|
||||
<LoaderCircle className="size-4 animate-spin" />
|
||||
) : (
|
||||
<ChevronRight
|
||||
onClick={handleExpand}
|
||||
className={clsx("size-4 transition cursor-pointer", {
|
||||
"rotate-90": expanded,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{cloneElement(icon, {
|
||||
className: clsx("size-4", icon.props.className),
|
||||
})}
|
||||
|
||||
<div className="truncate">{name}</div>
|
||||
</div>
|
||||
|
||||
<div className="w-3/5 flex items-center text-[#999]">
|
||||
<div className="flex-1">{type}</div>
|
||||
<div className="flex-1">{renderAlias()}</div>
|
||||
<div className="flex-1">{renderHotkey()}</div>
|
||||
<div className="flex-1 flex items-center justify-end">
|
||||
{renderSwitch()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasChildren && (
|
||||
<div
|
||||
className={clsx({
|
||||
hidden: !expanded,
|
||||
})}
|
||||
>
|
||||
{children.map((item) => {
|
||||
return <Item key={item.id} {...item} level={level + 1} />;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Content;
|
||||
@@ -1,50 +1,77 @@
|
||||
import { FC } from "react";
|
||||
import { Application } from "@/stores/applicationsStore";
|
||||
import { useContext, useMemo, useState } from "react";
|
||||
import { filesize } from "filesize";
|
||||
import dayjs from "dayjs";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAsyncEffect } from "ahooks";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { ExtensionsContext } from "../../..";
|
||||
|
||||
interface AppProps {
|
||||
current: Application;
|
||||
interface Metadata {
|
||||
name: string;
|
||||
where: string;
|
||||
size: number;
|
||||
icon: string;
|
||||
created: number;
|
||||
modified: number;
|
||||
lastOpened: number;
|
||||
}
|
||||
|
||||
const App: FC<AppProps> = (props) => {
|
||||
const { name, where, size, created, modified, lastOpened } = props.current;
|
||||
const App = () => {
|
||||
const { t } = useTranslation();
|
||||
const { activeId } = useContext(ExtensionsContext);
|
||||
|
||||
const metadata = [
|
||||
{
|
||||
label: t("settings.extensions.application.details.name"),
|
||||
value: name,
|
||||
},
|
||||
{
|
||||
label: t("settings.extensions.application.details.where"),
|
||||
value: where,
|
||||
},
|
||||
{
|
||||
label: t("settings.extensions.application.details.type"),
|
||||
value: t("settings.extensions.application.details.typeValue"),
|
||||
},
|
||||
{
|
||||
label: t("settings.extensions.application.details.size"),
|
||||
value: filesize(size, { standard: "jedec", spacer: "" }),
|
||||
},
|
||||
{
|
||||
label: t("settings.extensions.application.details.created"),
|
||||
value: dayjs(created).format("YYYY/MM/DD HH:mm:ss"),
|
||||
},
|
||||
{
|
||||
label: t("settings.extensions.application.details.modified"),
|
||||
value: dayjs(modified).format("YYYY/MM/DD HH:mm:ss"),
|
||||
},
|
||||
{
|
||||
label: t("settings.extensions.application.details.lastOpened"),
|
||||
value: dayjs(lastOpened).format("YYYY/MM/DD HH:mm:ss"),
|
||||
},
|
||||
];
|
||||
const [appMetadata, setAppMetadata] = useState<Metadata>();
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
const appMetadata = await platformAdapter.invokeBackend<Metadata>(
|
||||
"get_app_metadata",
|
||||
{
|
||||
appPath: activeId,
|
||||
}
|
||||
);
|
||||
|
||||
setAppMetadata(appMetadata);
|
||||
}, [activeId]);
|
||||
|
||||
const metadata = useMemo(() => {
|
||||
if (!appMetadata) return [];
|
||||
|
||||
const { name, where, size, created, modified, lastOpened } = appMetadata;
|
||||
|
||||
return [
|
||||
{
|
||||
label: t("settings.extensions.application.details.name"),
|
||||
value: name,
|
||||
},
|
||||
{
|
||||
label: t("settings.extensions.application.details.where"),
|
||||
value: where,
|
||||
},
|
||||
{
|
||||
label: t("settings.extensions.application.details.type"),
|
||||
value: t("settings.extensions.application.details.typeValue"),
|
||||
},
|
||||
{
|
||||
label: t("settings.extensions.application.details.size"),
|
||||
value: filesize(size, { standard: "jedec", spacer: "" }),
|
||||
},
|
||||
{
|
||||
label: t("settings.extensions.application.details.created"),
|
||||
value: dayjs(created).format("YYYY/MM/DD HH:mm:ss"),
|
||||
},
|
||||
{
|
||||
label: t("settings.extensions.application.details.modified"),
|
||||
value: dayjs(modified).format("YYYY/MM/DD HH:mm:ss"),
|
||||
},
|
||||
{
|
||||
label: t("settings.extensions.application.details.lastOpened"),
|
||||
value: dayjs(lastOpened).format("YYYY/MM/DD HH:mm:ss"),
|
||||
},
|
||||
];
|
||||
}, [appMetadata]);
|
||||
|
||||
return (
|
||||
<ul className="flex flex-col gap-2">
|
||||
<ul className="flex flex-col gap-2 p-0">
|
||||
{metadata.map((item) => {
|
||||
const { label, value } = item;
|
||||
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import { useApplicationsStore } from "@/stores/applicationsStore";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { Button } from "@headlessui/react";
|
||||
import { castArray, union } from "lodash-es";
|
||||
import { useMount } from "ahooks";
|
||||
import { castArray } from "lodash-es";
|
||||
import { Folder, SquareArrowOutUpRight, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Applications = () => {
|
||||
const { t } = useTranslation();
|
||||
const searchPaths = useApplicationsStore((state) => state.searchPaths);
|
||||
const setSearchPaths = useApplicationsStore((state) => state.setSearchPaths);
|
||||
const addError = useAppStore((state) => state.addError);
|
||||
const [paths, setPaths] = useState<string[]>([]);
|
||||
|
||||
const selectDirectory = async () => {
|
||||
useMount(async () => {
|
||||
const paths = await platformAdapter.invokeBackend<string[]>(
|
||||
"get_app_search_path"
|
||||
);
|
||||
|
||||
setPaths(paths);
|
||||
});
|
||||
|
||||
const handleAdd = async () => {
|
||||
const selected = await platformAdapter.openFileDialog({
|
||||
directory: true,
|
||||
multiple: true,
|
||||
@@ -18,7 +28,49 @@ const Applications = () => {
|
||||
|
||||
if (!selected) return;
|
||||
|
||||
setSearchPaths(union(searchPaths, castArray(selected)));
|
||||
const selectedPaths = castArray(selected).filter((selectedPath) => {
|
||||
if (paths.includes(selectedPath)) {
|
||||
addError(
|
||||
t("settings.extensions.application.hits.pathDuplication", {
|
||||
replace: [selectedPath],
|
||||
})
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const isChildPath = paths.some((item) => {
|
||||
return selectedPath.startsWith(item);
|
||||
});
|
||||
|
||||
if (isChildPath) {
|
||||
addError(
|
||||
t("settings.extensions.application.hits.pathIncluded", {
|
||||
replace: [selectedPath],
|
||||
})
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
setPaths((prev) => prev.concat(selectedPaths));
|
||||
|
||||
for await (const path of selectedPaths) {
|
||||
await platformAdapter.invokeBackend("add_app_search_path", {
|
||||
searchPath: path,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (path: string) => {
|
||||
setPaths((prev) => prev.filter((item) => item !== path));
|
||||
|
||||
platformAdapter.invokeBackend("remove_app_search_path", {
|
||||
searchPath: path,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -35,13 +87,13 @@ const Applications = () => {
|
||||
|
||||
<Button
|
||||
className="w-full h-8 my-4 text-[#0087FF] border border-[#EEF0F3] hover:border-[#0087FF] dark:border-gray-700 rounded-md transition"
|
||||
onClick={selectDirectory}
|
||||
onClick={handleAdd}
|
||||
>
|
||||
{t("settings.extensions.application.button.addDirectories")}
|
||||
</Button>
|
||||
|
||||
<ul className="flex flex-col gap-2">
|
||||
{searchPaths.map((item) => {
|
||||
<ul className="flex flex-col gap-2 p-0">
|
||||
{paths.map((item) => {
|
||||
return (
|
||||
<li key={item} className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-1 flex-1 overflow-hidden">
|
||||
@@ -60,9 +112,7 @@ const Applications = () => {
|
||||
|
||||
<X
|
||||
className="size-4 cursor-pointer"
|
||||
onClick={() => {
|
||||
setSearchPaths(searchPaths.filter((path) => path !== item));
|
||||
}}
|
||||
onClick={() => handleRemove(item)}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useContext, useMemo } from "react";
|
||||
import { ExtensionsContext, Plugin } from "../..";
|
||||
|
||||
const Details = () => {
|
||||
const { plugins, activeId } = useContext(ExtensionsContext);
|
||||
|
||||
const findPlugin = (plugins: Plugin[], id: string) => {
|
||||
for (const plugin of plugins) {
|
||||
const { children = [] } = plugin;
|
||||
|
||||
if (plugin.id === id) {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
if (children.length > 0) {
|
||||
const matched = findPlugin(children, id) as Plugin;
|
||||
|
||||
if (!matched) continue;
|
||||
|
||||
return matched;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const currentPlugin = useMemo(() => {
|
||||
if (!activeId) return;
|
||||
|
||||
return findPlugin(plugins, activeId);
|
||||
}, [activeId, plugins]);
|
||||
|
||||
return (
|
||||
<div className="flex-1 h-full overflow-auto">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
{currentPlugin?.name}
|
||||
</h2>
|
||||
|
||||
{currentPlugin?.detail}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Details;
|
||||
143
src/components/Settings/Extensions/components/Shortcut/index.tsx
Normal file
143
src/components/Settings/Extensions/components/Shortcut/index.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { find, isEmpty, map, remove, some, split } from "lodash-es";
|
||||
import { useRef, type FC, type KeyboardEvent, type MouseEvent } from "react";
|
||||
import { type Key, keys, modifierKeys, standardKeys } from "./keyboard";
|
||||
import { CircleX } from "lucide-react";
|
||||
import { useFocusWithin, useHover, useReactive } from "ahooks";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface ShortcutProps {
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
isSystem?: boolean;
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
value: Key[];
|
||||
}
|
||||
|
||||
const Shortcut: FC<ShortcutProps> = (props) => {
|
||||
const { value = "", placeholder, isSystem = true, onChange } = props;
|
||||
|
||||
const separator = isSystem ? "+" : ".";
|
||||
const keyFiled = isSystem ? "tauriKey" : "hookKey";
|
||||
|
||||
const parseValue = () => {
|
||||
if (!value) return [];
|
||||
|
||||
return split(value, separator).map((key) => {
|
||||
return find(keys, { [keyFiled]: key })!;
|
||||
});
|
||||
};
|
||||
|
||||
const state = useReactive<State>({
|
||||
value: parseValue(),
|
||||
});
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isHovering = useHover(containerRef);
|
||||
|
||||
const isFocusing = useFocusWithin(containerRef, {
|
||||
onFocus: () => {
|
||||
state.value = [];
|
||||
},
|
||||
onBlur: () => {
|
||||
if (!isValidShortcut()) {
|
||||
state.value = parseValue();
|
||||
}
|
||||
|
||||
handleChange();
|
||||
},
|
||||
});
|
||||
|
||||
const isValidShortcut = () => {
|
||||
if (state.value?.[0]?.eventKey?.startsWith("F")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const hasModifierKey = some(state.value, ({ eventKey }) => {
|
||||
return some(modifierKeys, { eventKey });
|
||||
});
|
||||
const hasStandardKey = some(state.value, ({ eventKey }) => {
|
||||
return some(standardKeys, { eventKey });
|
||||
});
|
||||
|
||||
return hasModifierKey && hasStandardKey;
|
||||
};
|
||||
|
||||
const getEventKey = (event: KeyboardEvent) => {
|
||||
let { key, code } = event;
|
||||
|
||||
key = key.replace("Meta", "Command");
|
||||
|
||||
const isModifierKey = some(modifierKeys, { eventKey: key });
|
||||
|
||||
return isModifierKey ? key : code;
|
||||
};
|
||||
|
||||
const handleChange = () => {
|
||||
const nextValue = map(state.value, keyFiled).join(separator);
|
||||
|
||||
onChange?.(nextValue);
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const eventKey = getEventKey(event);
|
||||
|
||||
const matched = find(keys, { eventKey });
|
||||
const isInvalid = !matched;
|
||||
const isDuplicate = some(state.value, { eventKey });
|
||||
|
||||
if (isInvalid || isDuplicate) return;
|
||||
|
||||
state.value.push(matched);
|
||||
|
||||
if (isValidShortcut()) {
|
||||
containerRef.current?.blur();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyUp = (event: KeyboardEvent) => {
|
||||
remove(state.value, { eventKey: getEventKey(event) });
|
||||
};
|
||||
|
||||
const handleClear = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
state.value = [];
|
||||
|
||||
handleChange();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
tabIndex={0}
|
||||
className="relative flex items-center h-6 px-2 rounded-[4px] border border-transparent hover:border-[#0072FF] focus:border-[#0072FF] transition"
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={handleKeyUp}
|
||||
>
|
||||
{isEmpty(state.value) ? (
|
||||
<div className="whitespace-nowrap">{placeholder}</div>
|
||||
) : (
|
||||
<div className="font-bold text-primary">
|
||||
{map(state.value, "symbol").join(" ")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CircleX
|
||||
size={16}
|
||||
className={clsx(
|
||||
"absolute right-2 hover:text-[#0072FF] cursor-pointer transition",
|
||||
{
|
||||
hidden: isFocusing || !isHovering || isEmpty(state.value),
|
||||
}
|
||||
)}
|
||||
onMouseDown={handleClear}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Shortcut;
|
||||
@@ -0,0 +1,314 @@
|
||||
import { isMac } from "@/utils/platform";
|
||||
import { defaults } from "lodash-es";
|
||||
|
||||
export interface Key {
|
||||
eventKey: string;
|
||||
hookKey?: string;
|
||||
tauriKey?: string;
|
||||
symbol?: string;
|
||||
}
|
||||
|
||||
export const modifierKeys: Key[] = [
|
||||
{
|
||||
eventKey: "Shift",
|
||||
symbol: isMac ? "⇧" : "Shift",
|
||||
},
|
||||
{
|
||||
eventKey: "Control",
|
||||
hookKey: "ctrl",
|
||||
symbol: isMac ? "⌃" : "Ctrl",
|
||||
},
|
||||
{
|
||||
eventKey: "Alt",
|
||||
symbol: isMac ? "⌥" : "Alt",
|
||||
},
|
||||
{
|
||||
eventKey: "Command",
|
||||
hookKey: "meta",
|
||||
symbol: isMac ? "⌘" : "Super",
|
||||
},
|
||||
].map((item) => {
|
||||
const { eventKey } = item;
|
||||
|
||||
defaults<Key, Partial<Key>>(item, {
|
||||
hookKey: eventKey.toLowerCase(),
|
||||
tauriKey: eventKey,
|
||||
});
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
export const standardKeys: Key[] = [
|
||||
// 第一排
|
||||
{
|
||||
eventKey: "Escape",
|
||||
hookKey: "esc",
|
||||
symbol: isMac ? "⎋" : "Esc",
|
||||
},
|
||||
{
|
||||
eventKey: "F1",
|
||||
},
|
||||
{
|
||||
eventKey: "F2",
|
||||
},
|
||||
{
|
||||
eventKey: "F3",
|
||||
},
|
||||
{
|
||||
eventKey: "F4",
|
||||
},
|
||||
{
|
||||
eventKey: "F5",
|
||||
},
|
||||
{
|
||||
eventKey: "F6",
|
||||
},
|
||||
{
|
||||
eventKey: "F7",
|
||||
},
|
||||
{
|
||||
eventKey: "F8",
|
||||
},
|
||||
{
|
||||
eventKey: "F9",
|
||||
},
|
||||
{
|
||||
eventKey: "F10",
|
||||
},
|
||||
{
|
||||
eventKey: "F11",
|
||||
},
|
||||
{
|
||||
eventKey: "F12",
|
||||
}, // 第二排
|
||||
{
|
||||
eventKey: "Backquote",
|
||||
hookKey: "graveaccent",
|
||||
symbol: "`",
|
||||
},
|
||||
{
|
||||
eventKey: "Digit1",
|
||||
},
|
||||
{
|
||||
eventKey: "Digit2",
|
||||
},
|
||||
{
|
||||
eventKey: "Digit3",
|
||||
},
|
||||
{
|
||||
eventKey: "Digit4",
|
||||
},
|
||||
{
|
||||
eventKey: "Digit5",
|
||||
},
|
||||
{
|
||||
eventKey: "Digit6",
|
||||
},
|
||||
{
|
||||
eventKey: "Digit7",
|
||||
},
|
||||
{
|
||||
eventKey: "Digit8",
|
||||
},
|
||||
{
|
||||
eventKey: "Digit9",
|
||||
},
|
||||
{
|
||||
eventKey: "Digit0",
|
||||
},
|
||||
{
|
||||
eventKey: "Minus",
|
||||
hookKey: "dash",
|
||||
tauriKey: "-",
|
||||
symbol: "-",
|
||||
},
|
||||
{
|
||||
eventKey: "Equal",
|
||||
hookKey: "equalsign",
|
||||
tauriKey: "=",
|
||||
symbol: "=",
|
||||
},
|
||||
{
|
||||
eventKey: "Backspace",
|
||||
symbol: isMac ? "⌫" : void 0,
|
||||
},
|
||||
// 第三排
|
||||
{
|
||||
eventKey: "Tab",
|
||||
symbol: isMac ? "⇥" : void 0,
|
||||
},
|
||||
{
|
||||
eventKey: "KeyQ",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyW",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyE",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyR",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyT",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyY",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyU",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyI",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyO",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyP",
|
||||
},
|
||||
{
|
||||
eventKey: "BracketLeft",
|
||||
hookKey: "openbracket",
|
||||
symbol: "[",
|
||||
},
|
||||
{
|
||||
eventKey: "BracketRight",
|
||||
hookKey: "closebracket",
|
||||
symbol: "]",
|
||||
},
|
||||
{
|
||||
eventKey: "Backslash",
|
||||
symbol: "\\",
|
||||
},
|
||||
// 第四排
|
||||
{
|
||||
eventKey: "KeyA",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyS",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyD",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyF",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyG",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyH",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyJ",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyK",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyL",
|
||||
},
|
||||
{
|
||||
eventKey: "Semicolon",
|
||||
symbol: ";",
|
||||
},
|
||||
{
|
||||
eventKey: "Quote",
|
||||
hookKey: "singlequote",
|
||||
symbol: "'",
|
||||
},
|
||||
{
|
||||
eventKey: "Enter",
|
||||
symbol: isMac ? "↩︎" : void 0,
|
||||
},
|
||||
// 第五排
|
||||
{
|
||||
eventKey: "KeyZ",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyX",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyC",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyV",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyB",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyN",
|
||||
},
|
||||
{
|
||||
eventKey: "KeyM",
|
||||
},
|
||||
{
|
||||
eventKey: "Comma",
|
||||
symbol: ",",
|
||||
},
|
||||
{
|
||||
eventKey: "Period",
|
||||
symbol: ".",
|
||||
},
|
||||
{
|
||||
eventKey: "Slash",
|
||||
hookKey: "forwardslash",
|
||||
symbol: "/",
|
||||
},
|
||||
// 第六排
|
||||
{
|
||||
eventKey: "Space",
|
||||
symbol: isMac ? "␣" : void 0,
|
||||
},
|
||||
// 方向键
|
||||
{
|
||||
eventKey: "ArrowUp",
|
||||
hookKey: "uparrow",
|
||||
symbol: "↑",
|
||||
},
|
||||
{
|
||||
eventKey: "ArrowDown",
|
||||
hookKey: "downarrow",
|
||||
symbol: "↓",
|
||||
},
|
||||
{
|
||||
eventKey: "ArrowLeft",
|
||||
hookKey: "leftarrow",
|
||||
symbol: "←",
|
||||
},
|
||||
{
|
||||
eventKey: "ArrowRight",
|
||||
hookKey: "rightarrow",
|
||||
symbol: "→",
|
||||
},
|
||||
].map((item) => {
|
||||
const { eventKey } = item;
|
||||
|
||||
defaults<Key, Partial<Key>>(item, {
|
||||
hookKey: eventKey.toLowerCase(),
|
||||
symbol: eventKey,
|
||||
tauriKey: eventKey,
|
||||
});
|
||||
|
||||
if (eventKey.startsWith("Digit") || eventKey.startsWith("Key")) {
|
||||
item.tauriKey = item.symbol = eventKey.slice(-1);
|
||||
|
||||
item.hookKey = item.tauriKey.toLowerCase();
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
export const keys = modifierKeys.concat(standardKeys);
|
||||
|
||||
export const getKeySymbol = (key: string) => {
|
||||
const fields = ["tauriKey", "hookKey"] as const;
|
||||
|
||||
const matched = keys.find((entry) => {
|
||||
return fields.some((field) => entry[field] === key);
|
||||
});
|
||||
|
||||
return matched?.symbol ?? key;
|
||||
};
|
||||
@@ -1,81 +1,201 @@
|
||||
import {
|
||||
createContext,
|
||||
Dispatch,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Folder } from "lucide-react";
|
||||
import { Calculator, Folder } from "lucide-react";
|
||||
import { noop } from "lodash-es";
|
||||
|
||||
import Accordion from "./components/Accordion";
|
||||
import ApplicationsContent from "./components/Content/Applications";
|
||||
import ApplicationsDetail from "./components/Details/Applications";
|
||||
import { useApplicationsStore } from "@/stores/applicationsStore";
|
||||
import Application from "./components/Details/Application";
|
||||
import { useMount } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import ApplicationsDetail from "./components/Details/Applications";
|
||||
import Application from "./components/Details/Application";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import Content from "./components/Content";
|
||||
import Details from "./components/Details";
|
||||
|
||||
export interface IApplication {
|
||||
path: string;
|
||||
name: string;
|
||||
iconPath: string;
|
||||
alias: string;
|
||||
hotkey: string;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
|
||||
export interface Plugin {
|
||||
id: string;
|
||||
icon: ReactElement;
|
||||
title: ReactNode;
|
||||
type?: "Group" | "Extension";
|
||||
name: ReactNode;
|
||||
type?: "Group" | "Extension" | "Application";
|
||||
alias?: string;
|
||||
hotKey?: string;
|
||||
hotkey?: string;
|
||||
enabled?: boolean;
|
||||
content?: ReactNode;
|
||||
detail?: ReactNode;
|
||||
children?: Plugin[];
|
||||
manualLoad?: boolean;
|
||||
loadChildren?: () => Promise<void>;
|
||||
onAliasChange?: (alias: string) => void;
|
||||
onHotkeyChange?: (hotkey: string) => void;
|
||||
onEnabledChange?: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
interface ExtensionsContextType {
|
||||
plugins: Plugin[];
|
||||
setPlugins: Dispatch<SetStateAction<Plugin[]>>;
|
||||
activeId?: string;
|
||||
setActiveId: (id: string) => void;
|
||||
}
|
||||
|
||||
export const ExtensionsContext = createContext<ExtensionsContextType>({
|
||||
plugins: [],
|
||||
setPlugins: noop,
|
||||
setActiveId: noop,
|
||||
});
|
||||
|
||||
const Extensions = () => {
|
||||
const { t } = useTranslation();
|
||||
const [apps, setApps] = useState<IApplication[]>([]);
|
||||
const [disabled, setDisabled] = useState<string[]>([]);
|
||||
const [activeId, setActiveId] = useState<string>();
|
||||
|
||||
const allApps = useApplicationsStore((state) => {
|
||||
return state.allApps;
|
||||
useMount(async () => {
|
||||
const disabled = await platformAdapter.invokeBackend<string[]>(
|
||||
"get_disabled_local_query_sources"
|
||||
);
|
||||
|
||||
setDisabled(disabled);
|
||||
});
|
||||
|
||||
const presetPlugins: Plugin[] = [
|
||||
{
|
||||
id: "1",
|
||||
icon: <Folder />,
|
||||
title: t("settings.extensions.application.title"),
|
||||
type: "Group",
|
||||
content: <ApplicationsContent />,
|
||||
detail: <ApplicationsDetail />,
|
||||
},
|
||||
// {
|
||||
// id: "2",
|
||||
// icon: <File />,
|
||||
// title: "File Search",
|
||||
// },
|
||||
];
|
||||
const loadApps = async () => {
|
||||
const apps = await platformAdapter.invokeBackend<IApplication[]>(
|
||||
"get_app_list"
|
||||
);
|
||||
|
||||
const plugins: Plugin[] = [...presetPlugins];
|
||||
|
||||
const [activeId, setActiveId] = useState(plugins[0].id);
|
||||
|
||||
const currentPlugin = useMemo(() => {
|
||||
return plugins.find((plugin) => plugin.id === activeId);
|
||||
}, [activeId, plugins]);
|
||||
|
||||
const currentApp = useMemo(() => {
|
||||
return allApps.find((app) => {
|
||||
return app.name === activeId;
|
||||
const sortedApps = apps.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name, undefined, {
|
||||
sensitivity: "base",
|
||||
});
|
||||
});
|
||||
}, [activeId, allApps]);
|
||||
|
||||
setApps(sortedApps);
|
||||
};
|
||||
|
||||
const presetPlugins = useMemo<Plugin[]>(() => {
|
||||
const plugins: Plugin[] = [
|
||||
{
|
||||
id: "Applications",
|
||||
icon: <Folder />,
|
||||
name: t("settings.extensions.application.title"),
|
||||
type: "Group",
|
||||
detail: <ApplicationsDetail />,
|
||||
children: [],
|
||||
manualLoad: true,
|
||||
loadChildren: loadApps,
|
||||
},
|
||||
{
|
||||
id: "Calculator",
|
||||
icon: <Calculator />,
|
||||
name: t("settings.extensions.calculator.title"),
|
||||
},
|
||||
];
|
||||
|
||||
if (apps.length > 0) {
|
||||
for (const app of apps) {
|
||||
const { path, iconPath, isDisabled } = app;
|
||||
|
||||
plugins[0].children?.push({
|
||||
...app,
|
||||
id: path,
|
||||
type: "Application",
|
||||
icon: (
|
||||
<img
|
||||
src={platformAdapter.convertFileSrc(iconPath)}
|
||||
className="size-5"
|
||||
/>
|
||||
),
|
||||
enabled: !isDisabled,
|
||||
detail: <Application />,
|
||||
onAliasChange(alias) {
|
||||
platformAdapter.invokeBackend("set_app_alias", {
|
||||
appPath: path,
|
||||
alias,
|
||||
});
|
||||
|
||||
const nextApps = apps.map((item) => {
|
||||
if (item.path !== path) return item;
|
||||
|
||||
return { ...item, alias };
|
||||
});
|
||||
|
||||
setApps(nextApps);
|
||||
},
|
||||
onHotkeyChange(hotkey) {
|
||||
const command = `${hotkey ? "register" : "unregister"}_app_hotkey`;
|
||||
|
||||
platformAdapter.invokeBackend(command, {
|
||||
appPath: path,
|
||||
hotkey,
|
||||
});
|
||||
|
||||
const nextApps = apps.map((item) => {
|
||||
if (item.path !== path) return item;
|
||||
|
||||
return { ...item, hotkey };
|
||||
});
|
||||
|
||||
setApps(nextApps);
|
||||
},
|
||||
onEnabledChange(enabled) {
|
||||
const command = `${enabled ? "enable" : "disable"}_app_search`;
|
||||
|
||||
platformAdapter.invokeBackend(command, {
|
||||
appPath: path,
|
||||
});
|
||||
|
||||
const nextApps = apps.map((item) => {
|
||||
if (item.path !== path) return item;
|
||||
|
||||
return { ...item, isDisabled: !enabled };
|
||||
});
|
||||
|
||||
setApps(nextApps);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}, [apps]);
|
||||
|
||||
const [plugins, setPlugins] = useState<Plugin[]>(presetPlugins);
|
||||
|
||||
useEffect(() => {
|
||||
setPlugins(presetPlugins);
|
||||
}, [presetPlugins]);
|
||||
|
||||
useEffect(() => {
|
||||
setPlugins((prevPlugins) => {
|
||||
return prevPlugins.map((item) => {
|
||||
if (disabled.includes(item.id)) {
|
||||
return { ...item, enabled: false };
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
});
|
||||
}, [disabled]);
|
||||
|
||||
return (
|
||||
<ExtensionsContext.Provider
|
||||
value={{
|
||||
plugins,
|
||||
setPlugins,
|
||||
activeId,
|
||||
setActiveId,
|
||||
}}
|
||||
@@ -88,11 +208,9 @@ const Extensions = () => {
|
||||
|
||||
<div>
|
||||
<div className="flex">
|
||||
<div className="w-[220px]">
|
||||
{t("settings.extensions.list.name")}
|
||||
</div>
|
||||
<div className="flex-1">{t("settings.extensions.list.name")}</div>
|
||||
|
||||
<div className="flex flex-1">
|
||||
<div className="w-3/5 flex">
|
||||
<div className="flex-1">
|
||||
{t("settings.extensions.list.type")}
|
||||
</div>
|
||||
@@ -108,23 +226,11 @@ const Extensions = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{plugins.map((item) => {
|
||||
return <Accordion {...item} key={item.id} />;
|
||||
})}
|
||||
<Content />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 h-full overflow-auto">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
{currentPlugin?.title}
|
||||
|
||||
{currentApp?.name}
|
||||
</h2>
|
||||
|
||||
{currentPlugin?.detail}
|
||||
|
||||
{currentApp && <Application current={currentApp} />}
|
||||
</div>
|
||||
<Details />
|
||||
</div>
|
||||
</ExtensionsContext.Provider>
|
||||
);
|
||||
|
||||
47
src/components/Settings/SettingsInput.tsx
Normal file
47
src/components/Settings/SettingsInput.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Input, InputProps } from "@headlessui/react";
|
||||
import clsx from "clsx";
|
||||
import { isNumber } from "lodash-es";
|
||||
import { FC, FocusEvent } from "react";
|
||||
|
||||
interface SettingsInputProps extends Omit<InputProps, "onChange"> {
|
||||
onChange: (value?: string | number) => void;
|
||||
}
|
||||
|
||||
const SettingsInput: FC<SettingsInputProps> = (props) => {
|
||||
const { className, onBlur, onChange, ...rest } = props;
|
||||
const { type, min, max } = rest;
|
||||
|
||||
const handleBlur = (event: FocusEvent<HTMLInputElement>) => {
|
||||
onBlur?.(event);
|
||||
|
||||
if (type !== "number") return;
|
||||
|
||||
if (event.target instanceof HTMLInputElement) {
|
||||
const value = Number(event.target.value);
|
||||
|
||||
if (isNumber(min) && value < min) {
|
||||
onChange?.(min);
|
||||
}
|
||||
|
||||
if (isNumber(max) && value > max) {
|
||||
onChange?.(max);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Input
|
||||
{...rest}
|
||||
className={clsx(
|
||||
"w-20 h-8 px-2 rounded-md border bg-transparent border-black/5 dark:border-white/10 hover:!border-[#0072FF] focus:!border-[#0072FF] transition",
|
||||
className
|
||||
)}
|
||||
onBlur={handleBlur}
|
||||
onChange={(event) => {
|
||||
onChange?.(event.target.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsInput;
|
||||
@@ -16,7 +16,7 @@ export default function SettingsItem({
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Icon className="h-5 w-5 text-gray-400 dark:text-gray-500" />
|
||||
<Icon className="h-5 min-w-5 text-gray-400 dark:text-gray-500" />
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{title}
|
||||
|
||||
@@ -45,7 +45,7 @@ const UpdateApp = ({ checkUpdate, relaunchApp }: UpdateAppProps) => {
|
||||
const checkUpdateStatus = useCallback(async () => {
|
||||
const update = await checkUpdate();
|
||||
|
||||
if (update?.available) {
|
||||
if (update) {
|
||||
setUpdateInfo(update);
|
||||
|
||||
if (skipVersion === update.version) return;
|
||||
|
||||
@@ -39,182 +39,152 @@ export function useChatActions(
|
||||
const chatClose = useCallback(
|
||||
async (activeChat?: Chat) => {
|
||||
if (!activeChat?._id) return;
|
||||
try {
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
if (!currentServiceId) return;
|
||||
response = await platformAdapter.commands("close_session_chat", {
|
||||
serverId: currentServiceId,
|
||||
sessionId: activeChat?._id,
|
||||
});
|
||||
response = response ? JSON.parse(response) : null;
|
||||
} else {
|
||||
const [error, res] = await Post(
|
||||
`/chat/${activeChat?._id}/_close`,
|
||||
{}
|
||||
);
|
||||
if (error) {
|
||||
console.error("_close", error);
|
||||
return;
|
||||
}
|
||||
response = res;
|
||||
}
|
||||
console.log("_close", response);
|
||||
} catch (error) {
|
||||
console.error("chatClose:", error);
|
||||
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
if (!currentServiceId) return;
|
||||
response = await platformAdapter.commands("close_session_chat", {
|
||||
serverId: currentServiceId,
|
||||
sessionId: activeChat?._id,
|
||||
});
|
||||
response = response ? JSON.parse(response) : null;
|
||||
} else {
|
||||
const [_error, res] = await Post(
|
||||
`/chat/${activeChat?._id}/_close`,
|
||||
{}
|
||||
);
|
||||
response = res;
|
||||
}
|
||||
console.log("_close", response);
|
||||
|
||||
},
|
||||
[currentServiceId]
|
||||
[currentServiceId, isTauri]
|
||||
);
|
||||
|
||||
const cancelChat = useCallback(
|
||||
async (activeChat?: Chat) => {
|
||||
setCurChatEnd(true);
|
||||
if (!activeChat?._id) return;
|
||||
try {
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
if (!currentServiceId) return;
|
||||
response = await platformAdapter.commands("cancel_session_chat", {
|
||||
serverId: currentServiceId,
|
||||
sessionId: activeChat?._id,
|
||||
});
|
||||
response = response ? JSON.parse(response) : null;
|
||||
} else {
|
||||
const [error, res] = await Post(
|
||||
`/chat/${activeChat?._id}/_cancel`,
|
||||
{}
|
||||
);
|
||||
if (error) {
|
||||
console.error("_cancel", error);
|
||||
return;
|
||||
}
|
||||
response = res;
|
||||
}
|
||||
console.log("_cancel", response);
|
||||
} catch (error) {
|
||||
console.error("cancelChat:", error);
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
if (!currentServiceId) return;
|
||||
response = await platformAdapter.commands("cancel_session_chat", {
|
||||
serverId: currentServiceId,
|
||||
sessionId: activeChat?._id,
|
||||
});
|
||||
response = response ? JSON.parse(response) : null;
|
||||
} else {
|
||||
const [_error, res] = await Post(
|
||||
`/chat/${activeChat?._id}/_cancel`,
|
||||
{}
|
||||
);
|
||||
response = res;
|
||||
}
|
||||
console.log("_cancel", response);
|
||||
},
|
||||
[currentServiceId, setCurChatEnd]
|
||||
[currentServiceId, isTauri]
|
||||
);
|
||||
|
||||
const chatHistory = useCallback(
|
||||
async (chat: Chat, callback?: (chat: Chat) => void) => {
|
||||
if (!chat?._id) return;
|
||||
try {
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
if (!currentServiceId) return;
|
||||
response = await platformAdapter.commands("session_chat_history", {
|
||||
serverId: currentServiceId,
|
||||
sessionId: chat?._id,
|
||||
from: 0,
|
||||
size: 20,
|
||||
});
|
||||
response = response ? JSON.parse(response) : null;
|
||||
} else {
|
||||
const [error, res] = await Get(`/chat/${chat?._id}/_history`, {
|
||||
from: 0,
|
||||
size: 20,
|
||||
});
|
||||
if (error) {
|
||||
console.error("_cancel", error);
|
||||
return;
|
||||
}
|
||||
response = res;
|
||||
}
|
||||
const hits = response?.hits?.hits || [];
|
||||
const updatedChat: Chat = {
|
||||
...chat,
|
||||
messages: hits,
|
||||
};
|
||||
console.log("id_history", response, updatedChat);
|
||||
setActiveChat(updatedChat);
|
||||
callback && callback(updatedChat);
|
||||
} catch (error) {
|
||||
console.error("chatHistory:", error);
|
||||
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
if (!currentServiceId) return;
|
||||
response = await platformAdapter.commands("session_chat_history", {
|
||||
serverId: currentServiceId,
|
||||
sessionId: chat?._id,
|
||||
from: 0,
|
||||
size: 100,
|
||||
});
|
||||
response = response ? JSON.parse(response) : null;
|
||||
} else {
|
||||
const [_error, res] = await Get(`/chat/${chat?._id}/_history`, {
|
||||
from: 0,
|
||||
size: 100,
|
||||
});
|
||||
response = res;
|
||||
}
|
||||
|
||||
const hits = response?.hits?.hits || [];
|
||||
const updatedChat: Chat = {
|
||||
...chat,
|
||||
messages: hits,
|
||||
};
|
||||
console.log("id_history", updatedChat);
|
||||
setActiveChat(updatedChat);
|
||||
callback && callback(updatedChat);
|
||||
setVisibleStartPage(false);
|
||||
},
|
||||
[currentServiceId, setActiveChat]
|
||||
[currentServiceId, isTauri]
|
||||
);
|
||||
|
||||
const createNewChat = useCallback(
|
||||
async (value: string = "", activeChat?: Chat, id?: string) => {
|
||||
try {
|
||||
setTimedoutShow(false);
|
||||
await chatClose(activeChat);
|
||||
clearAllChunkData();
|
||||
setQuestion(value);
|
||||
if (!(websocketSessionId || id)) {
|
||||
addError("websocketSessionId not found");
|
||||
console.error("websocketSessionId", websocketSessionId, id);
|
||||
return;
|
||||
}
|
||||
console.log("sourceDataIds", sourceDataIds, MCPIds, websocketSessionId, id);
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
if (!currentServiceId) return;
|
||||
response = await platformAdapter.commands("new_chat", {
|
||||
serverId: currentServiceId,
|
||||
websocketId: websocketSessionId || id,
|
||||
message: value,
|
||||
queryParams: {
|
||||
search: isSearchActive,
|
||||
deep_thinking: isDeepThinkActive,
|
||||
mcp: isMCPActive,
|
||||
datasource: sourceDataIds?.join(",") || "",
|
||||
mcp_servers: MCPIds?.join(",") || "",
|
||||
assistant_id: currentAssistant?._id || '',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.log("websocketSessionId", websocketSessionId, id);
|
||||
const [error, res] = await Post(
|
||||
"/chat/_new",
|
||||
{
|
||||
message: value,
|
||||
},
|
||||
{
|
||||
search: isSearchActive,
|
||||
deep_thinking: isDeepThinkActive,
|
||||
mcp: isMCPActive,
|
||||
datasource: sourceDataIds?.join(",") || "",
|
||||
mcp_servers: MCPIds?.join(",") || "",
|
||||
assistant_id: currentAssistant?._id || '',
|
||||
},
|
||||
{
|
||||
"WEBSOCKET-SESSION-ID": websocketSessionId || id,
|
||||
}
|
||||
);
|
||||
if (error) {
|
||||
console.error("_new", error);
|
||||
return;
|
||||
}
|
||||
response = res;
|
||||
}
|
||||
console.log("_new", response);
|
||||
const newChat: Chat = response;
|
||||
curIdRef.current = response?.payload?.id;
|
||||
setTimedoutShow(false);
|
||||
await chatClose(activeChat);
|
||||
clearAllChunkData();
|
||||
setQuestion(value);
|
||||
|
||||
newChat._source = {
|
||||
message: value,
|
||||
};
|
||||
const updatedChat: Chat = {
|
||||
...newChat,
|
||||
messages: [newChat],
|
||||
};
|
||||
|
||||
changeInput && changeInput("");
|
||||
setActiveChat(updatedChat);
|
||||
setCurChatEnd(false);
|
||||
} catch (error) {
|
||||
console.error("createNewChat:", error);
|
||||
const sessionId = websocketSessionId || id;
|
||||
if (!sessionId) {
|
||||
addError("websocketSessionId not found");
|
||||
console.error("websocketSessionId", websocketSessionId, id);
|
||||
return;
|
||||
}
|
||||
|
||||
//console.log("sourceDataIds", sourceDataIds, MCPIds, websocketSessionId, id);
|
||||
const queryParams = {
|
||||
search: isSearchActive,
|
||||
deep_thinking: isDeepThinkActive,
|
||||
mcp: isMCPActive,
|
||||
datasource: sourceDataIds?.join(",") || "",
|
||||
mcp_servers: MCPIds?.join(",") || "",
|
||||
assistant_id: currentAssistant?._id || '',
|
||||
};
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
if (!currentServiceId) return;
|
||||
response = await platformAdapter.commands("new_chat", {
|
||||
serverId: currentServiceId,
|
||||
websocketId: sessionId,
|
||||
message: value,
|
||||
queryParams,
|
||||
});
|
||||
} else {
|
||||
const [_error, res] = await Post(
|
||||
"/chat/_new",
|
||||
{
|
||||
message: value,
|
||||
},
|
||||
queryParams,
|
||||
{
|
||||
"WEBSOCKET-SESSION-ID": sessionId,
|
||||
}
|
||||
);
|
||||
response = res;
|
||||
}
|
||||
|
||||
console.log("_new", response);
|
||||
const newChat: Chat = response;
|
||||
curIdRef.current = response?.payload?.id;
|
||||
|
||||
newChat._source = {
|
||||
message: value,
|
||||
};
|
||||
const updatedChat: Chat = {
|
||||
...newChat,
|
||||
messages: [newChat],
|
||||
};
|
||||
|
||||
changeInput && changeInput("");
|
||||
setActiveChat(updatedChat);
|
||||
setCurChatEnd(false);
|
||||
setVisibleStartPage(false);
|
||||
},
|
||||
[
|
||||
isTauri,
|
||||
currentServiceId,
|
||||
sourceDataIds,
|
||||
MCPIds,
|
||||
@@ -224,6 +194,7 @@ export function useChatActions(
|
||||
curIdRef,
|
||||
websocketSessionId,
|
||||
currentAssistant,
|
||||
chatClose,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -232,73 +203,62 @@ export function useChatActions(
|
||||
if (!newChat?._id || !content) return;
|
||||
|
||||
clearAllChunkData();
|
||||
try {
|
||||
if (!(websocketSessionId || id)) {
|
||||
addError("websocketSessionId not found");
|
||||
console.error("websocketSessionId", websocketSessionId, id);
|
||||
return;
|
||||
}
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
if (!currentServiceId) return;
|
||||
response = await platformAdapter.commands("send_message", {
|
||||
serverId: currentServiceId,
|
||||
websocketId: websocketSessionId || id,
|
||||
sessionId: newChat?._id,
|
||||
queryParams: {
|
||||
search: isSearchActive,
|
||||
deep_thinking: isDeepThinkActive,
|
||||
mcp: isMCPActive,
|
||||
datasource: sourceDataIds?.join(",") || "",
|
||||
mcp_servers: MCPIds?.join(",") || "",
|
||||
assistant_id: currentAssistant?._id || '',
|
||||
},
|
||||
message: content,
|
||||
});
|
||||
response = response ? JSON.parse(response) : null;
|
||||
} else {
|
||||
console.log("websocketSessionId", websocketSessionId, id);
|
||||
const [error, res] = await Post(
|
||||
`/chat/${newChat?._id}/_send`,
|
||||
{
|
||||
message: content,
|
||||
},
|
||||
{
|
||||
search: isSearchActive,
|
||||
deep_thinking: isDeepThinkActive,
|
||||
mcp: isMCPActive,
|
||||
datasource: sourceDataIds?.join(",") || "",
|
||||
mcp_servers: MCPIds?.join(",") || "",
|
||||
assistant_id: currentAssistant?._id || '',
|
||||
},
|
||||
{
|
||||
"WEBSOCKET-SESSION-ID": websocketSessionId || id,
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
console.error("_cancel", error);
|
||||
return;
|
||||
}
|
||||
response = res;
|
||||
}
|
||||
console.log("_send", response);
|
||||
curIdRef.current = response[0]?._id;
|
||||
|
||||
const updatedChat: Chat = {
|
||||
...newChat,
|
||||
messages: [...(newChat?.messages || []), ...(response || [])],
|
||||
};
|
||||
|
||||
changeInput && changeInput("");
|
||||
setActiveChat(updatedChat);
|
||||
setCurChatEnd(false);
|
||||
} catch (error) {
|
||||
console.error("sendMessage:", error);
|
||||
const sessionId = websocketSessionId || id;
|
||||
if (!sessionId) {
|
||||
addError("websocketSessionId not found");
|
||||
console.error("websocketSessionId", websocketSessionId, id);
|
||||
return;
|
||||
}
|
||||
|
||||
const queryParams = {
|
||||
search: isSearchActive,
|
||||
deep_thinking: isDeepThinkActive,
|
||||
mcp: isMCPActive,
|
||||
datasource: sourceDataIds?.join(",") || "",
|
||||
mcp_servers: MCPIds?.join(",") || "",
|
||||
assistant_id: currentAssistant?._id || '',
|
||||
}
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
if (!currentServiceId) return;
|
||||
response = await platformAdapter.commands("send_message", {
|
||||
serverId: currentServiceId,
|
||||
websocketId: sessionId,
|
||||
sessionId: newChat?._id,
|
||||
queryParams,
|
||||
message: content,
|
||||
});
|
||||
response = response ? JSON.parse(response) : null;
|
||||
} else {
|
||||
const [_error, res] = await Post(
|
||||
`/chat/${newChat?._id}/_send`,
|
||||
{
|
||||
message: content,
|
||||
},
|
||||
queryParams,
|
||||
{
|
||||
"WEBSOCKET-SESSION-ID": sessionId,
|
||||
}
|
||||
);
|
||||
response = res;
|
||||
}
|
||||
|
||||
console.log("_send", response);
|
||||
curIdRef.current = response[0]?._id;
|
||||
|
||||
const updatedChat: Chat = {
|
||||
...newChat,
|
||||
messages: [...(newChat?.messages || []), ...(response || [])],
|
||||
};
|
||||
|
||||
changeInput && changeInput("");
|
||||
setActiveChat(updatedChat);
|
||||
setCurChatEnd(false);
|
||||
setVisibleStartPage(false);
|
||||
},
|
||||
[
|
||||
isTauri,
|
||||
currentServiceId,
|
||||
sourceDataIds,
|
||||
MCPIds,
|
||||
@@ -306,8 +266,6 @@ export function useChatActions(
|
||||
isDeepThinkActive,
|
||||
isMCPActive,
|
||||
curIdRef,
|
||||
setActiveChat,
|
||||
setCurChatEnd,
|
||||
changeInput,
|
||||
websocketSessionId,
|
||||
currentAssistant,
|
||||
@@ -326,9 +284,6 @@ export function useChatActions(
|
||||
[
|
||||
chatHistory,
|
||||
sendMessage,
|
||||
setQuestion,
|
||||
setTimedoutShow,
|
||||
clearAllChunkData,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -336,66 +291,52 @@ export function useChatActions(
|
||||
async (chat: Chat) => {
|
||||
if (!chat?._id) return;
|
||||
setVisibleStartPage(false);
|
||||
try {
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
if (!currentServiceId) return;
|
||||
response = await platformAdapter.commands("open_session_chat", {
|
||||
serverId: currentServiceId,
|
||||
sessionId: chat?._id,
|
||||
});
|
||||
response = response ? JSON.parse(response) : null;
|
||||
} else {
|
||||
const [error, res] = await Post(`/chat/${chat?._id}/_open`, {});
|
||||
if (error) {
|
||||
console.error("_open", error);
|
||||
return null;
|
||||
}
|
||||
response = res;
|
||||
}
|
||||
|
||||
console.log("_open", response);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("open_session_chat:", error);
|
||||
return null;
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
if (!currentServiceId) return;
|
||||
response = await platformAdapter.commands("open_session_chat", {
|
||||
serverId: currentServiceId,
|
||||
sessionId: chat?._id,
|
||||
});
|
||||
response = response ? JSON.parse(response) : null;
|
||||
} else {
|
||||
const [_error, res] = await Post(`/chat/${chat?._id}/_open`, {});
|
||||
response = res;
|
||||
}
|
||||
|
||||
console.log("_open", response);
|
||||
return response;
|
||||
|
||||
},
|
||||
[currentServiceId]
|
||||
[currentServiceId, isTauri]
|
||||
);
|
||||
|
||||
const getChatHistory = useCallback(async () => {
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
try {
|
||||
if (!currentServiceId) return [];
|
||||
response = await platformAdapter.commands("chat_history", {
|
||||
serverId: currentServiceId,
|
||||
from: 0,
|
||||
size: 20,
|
||||
query: keyword,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("chat_history", error);
|
||||
}
|
||||
|
||||
if (!currentServiceId) return [];
|
||||
response = await platformAdapter.commands("chat_history", {
|
||||
serverId: currentServiceId,
|
||||
from: 0,
|
||||
size: 100,
|
||||
query: keyword,
|
||||
});
|
||||
|
||||
response = response ? JSON.parse(response) : null;
|
||||
} else {
|
||||
const [error, res] = await Get(`/chat/_history`, {
|
||||
const [_error, res] = await Get(`/chat/_history`, {
|
||||
from: 0,
|
||||
size: 20,
|
||||
size: 100,
|
||||
});
|
||||
if (error) {
|
||||
console.error("_history", error);
|
||||
return [];
|
||||
}
|
||||
response = res;
|
||||
}
|
||||
console.log("_history", response);
|
||||
const hits = response?.hits?.hits || [];
|
||||
|
||||
setChats(hits);
|
||||
return hits;
|
||||
}, [currentServiceId, keyword]);
|
||||
}, [currentServiceId, keyword, isTauri]);
|
||||
|
||||
useEffect(() => {
|
||||
showChatHistory && connected && getChatHistory();
|
||||
@@ -420,13 +361,13 @@ export function useChatActions(
|
||||
url: "/ui/chat",
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
}, [isTauri]);
|
||||
|
||||
const handleSearch = (keyword: string) => {
|
||||
setKeyword(keyword);
|
||||
};
|
||||
|
||||
const handleRename = async (chatId: string, title: string) => {
|
||||
const handleRename = useCallback(async (chatId: string, title: string) => {
|
||||
if (!currentServiceId) return;
|
||||
|
||||
await platformAdapter.commands("update_session_chat", {
|
||||
@@ -434,13 +375,13 @@ export function useChatActions(
|
||||
sessionId: chatId,
|
||||
title,
|
||||
});
|
||||
};
|
||||
}, [currentServiceId]);
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
const handleDelete = useCallback(async (chatId: string) => {
|
||||
if (!currentServiceId) return;
|
||||
|
||||
await platformAdapter.commands("delete_session_chat", currentServiceId, id);
|
||||
};
|
||||
await platformAdapter.commands("delete_session_chat", currentServiceId, chatId);
|
||||
}, [currentServiceId]);
|
||||
|
||||
return {
|
||||
chatClose,
|
||||
|
||||
@@ -2,6 +2,7 @@ import platformAdapter from "@/utils/platformAdapter";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { useKeyPress } from "ahooks";
|
||||
import { HISTORY_PANEL_ID } from "@/constants";
|
||||
import { closeHistoryPanel } from "@/utils";
|
||||
|
||||
const useEscape = () => {
|
||||
const visibleContextMenu = useSearchStore((state) => {
|
||||
@@ -29,11 +30,7 @@ const useEscape = () => {
|
||||
const historyPanel = document.getElementById(HISTORY_PANEL_ID);
|
||||
|
||||
if (historyPanel) {
|
||||
const button = document.querySelector(
|
||||
`[aria-controls="${HISTORY_PANEL_ID}"]`
|
||||
);
|
||||
|
||||
return (button as HTMLElement).click();
|
||||
return closeHistoryPanel();
|
||||
}
|
||||
|
||||
platformAdapter.hideWindow();
|
||||
|
||||
@@ -77,7 +77,7 @@ export default function useMessageChunkData() {
|
||||
}, []),
|
||||
};
|
||||
|
||||
const clearAllChunkData = useCallback(() => {
|
||||
const clearAllChunkData = () => {
|
||||
setQuery_intent(undefined);
|
||||
setTools(undefined);
|
||||
setFetch_source(undefined);
|
||||
@@ -85,7 +85,7 @@ export default function useMessageChunkData() {
|
||||
setDeep_read(undefined);
|
||||
setThink(undefined);
|
||||
setResponse(undefined);
|
||||
}, []);
|
||||
};
|
||||
|
||||
return {
|
||||
data: { query_intent, tools, fetch_source, pick_source, deep_read, think, response },
|
||||
|
||||
@@ -95,6 +95,5 @@ export function useMessageHandler(
|
||||
|
||||
return {
|
||||
dealMsg,
|
||||
messageTimeoutRef,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useAppearanceStore } from "@/stores/appearanceStore";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||
import { useStartupStore } from "@/stores/startupStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { isNumber } from "lodash-es";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const useSyncStore = () => {
|
||||
@@ -77,6 +79,13 @@ export const useSyncStore = () => {
|
||||
const setQueryTimeout = useConnectStore((state) => {
|
||||
return state.setQuerySourceTimeout;
|
||||
});
|
||||
const setOpacity = useAppearanceStore((state) => state.setOpacity);
|
||||
const setSnapshotUpdate = useAppearanceStore((state) => {
|
||||
return state.setSnapshotUpdate;
|
||||
});
|
||||
const setAllowSelfSignature = useConnectStore((state) => {
|
||||
return state.setAllowSelfSignature;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!resetFixedWindow) {
|
||||
@@ -137,9 +146,24 @@ export const useSyncStore = () => {
|
||||
}),
|
||||
|
||||
platformAdapter.listenEvent("change-connect-store", ({ payload }) => {
|
||||
const { connectionTimeout, querySourceTimeout } = payload;
|
||||
setConnectionTimeout(connectionTimeout);
|
||||
setQueryTimeout(querySourceTimeout);
|
||||
const { connectionTimeout, querySourceTimeout, allowSelfSignature } =
|
||||
payload;
|
||||
if (isNumber(connectionTimeout)) {
|
||||
setConnectionTimeout(connectionTimeout);
|
||||
}
|
||||
if (isNumber(querySourceTimeout)) {
|
||||
setQueryTimeout(querySourceTimeout);
|
||||
}
|
||||
setAllowSelfSignature(allowSelfSignature);
|
||||
}),
|
||||
|
||||
platformAdapter.listenEvent("change-appearance-store", ({ payload }) => {
|
||||
const { opacity, snapshotUpdate } = payload;
|
||||
|
||||
if (isNumber(opacity)) {
|
||||
setOpacity(opacity);
|
||||
}
|
||||
setSnapshotUpdate(snapshotUpdate);
|
||||
}),
|
||||
]);
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function useWebSocket({
|
||||
|
||||
// web
|
||||
const { readyState, connect, disconnect } = useWebSocketAHook(
|
||||
// "wss://coco.infini.cloud/ws",
|
||||
//"wss://coco.infini.cloud/ws",
|
||||
//"ws://localhost:9000/ws",
|
||||
isTauri ? "" : endpoint_websocket,
|
||||
{
|
||||
@@ -118,7 +118,7 @@ export default function useWebSocket({
|
||||
},
|
||||
[currentService]
|
||||
);
|
||||
const disconnectWS = async () => {
|
||||
const disconnectWS = useCallback(async () => {
|
||||
if (!connected) return;
|
||||
if (isTauri) {
|
||||
try {
|
||||
@@ -131,7 +131,8 @@ export default function useWebSocket({
|
||||
} else {
|
||||
disconnect();
|
||||
}
|
||||
};
|
||||
}, [connected]);
|
||||
|
||||
const updateDealMsg = useCallback(
|
||||
(newDealMsg: (msg: string) => void) => {
|
||||
dealMsgRef.current = newDealMsg;
|
||||
@@ -145,16 +146,10 @@ export default function useWebSocket({
|
||||
let unlisten_message = null;
|
||||
|
||||
if (!isTauri) return;
|
||||
|
||||
|
||||
unlisten_error = platformAdapter.listenEvent(`ws-error-${clientId}`, (event) => {
|
||||
// {
|
||||
// "error": {
|
||||
// "reason": "invalid login"
|
||||
// },
|
||||
// "status": 401
|
||||
// }
|
||||
console.error(`ws-error-${clientId}`, event, connected);
|
||||
if(connected) {
|
||||
if (connected) {
|
||||
addError("WebSocket connection failed.");
|
||||
}
|
||||
setConnected(false); // error
|
||||
|
||||
@@ -161,6 +161,24 @@
|
||||
"queryTimeout": {
|
||||
"title": "Query Timeout",
|
||||
"description": "Terminates the query if no search results are returned within this time. Default: 500ms."
|
||||
},
|
||||
"allowSelfSignature": {
|
||||
"title": "Allow Self-Signed Certificates",
|
||||
"description": "Allow connections to servers using self-signed certificates. Enable only if you trust the source."
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Appearance Settings",
|
||||
"opacity": {
|
||||
"title": "Pinned Window Dimness Setting",
|
||||
"description": "Adjusts the opacity level of the Coco AI window when it’s pinned and not in focus. Set a value between 10% and 100%, where 100% means fully opaque (no dimming), and lower values increase transparency, allowing underlying content to show through."
|
||||
}
|
||||
},
|
||||
"updateVersion": {
|
||||
"title": "Version & Updates",
|
||||
"snapshotUpdate": {
|
||||
"title": "Snapshot Updates",
|
||||
"description": "Get early access to new features. May be unstable."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -182,11 +200,15 @@
|
||||
"hotkey": "Hotkey",
|
||||
"enabled": "Enabled"
|
||||
},
|
||||
"hits": {
|
||||
"addAlias": "Add Alias",
|
||||
"recordHotkey": "Record Hotkey"
|
||||
},
|
||||
"application": {
|
||||
"title": "Applications",
|
||||
"hits": {
|
||||
"addAlias": "Add Alias",
|
||||
"recordHotkey": "Record Hotkey"
|
||||
"pathDuplication": "Path \"{{0}}\" is already in search scope.",
|
||||
"pathIncluded": "Path \"{{0}}\" is already covered by another search directory."
|
||||
},
|
||||
"button": {
|
||||
"addDirectories": "Add Directories"
|
||||
@@ -203,6 +225,9 @@
|
||||
"modified": "Modified",
|
||||
"lastOpened": "Last Opened"
|
||||
}
|
||||
},
|
||||
"calculator": {
|
||||
"title": "Calculator"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -262,6 +287,10 @@
|
||||
"searchPopover": {
|
||||
"title": "Search Scope",
|
||||
"allScope": "All Scope"
|
||||
},
|
||||
"uploadFileHints": {
|
||||
"tooltip": "Support screenshots, upload files, up to 50, single file up to 100 MB.",
|
||||
"maxSize": "The file size cannot exceed 100 MB."
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
|
||||
@@ -161,6 +161,24 @@
|
||||
"queryTimeout": {
|
||||
"title": "查询超时",
|
||||
"description": "在此时间内未返回搜索结果,则终止查询。默认值:500 毫秒。"
|
||||
},
|
||||
"allowSelfSignature": {
|
||||
"title": "允许自签名证书",
|
||||
"description": "允许连接使用自签名证书的服务器。仅在信任来源的情况下启用。"
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
"title": "外观设置",
|
||||
"opacity": {
|
||||
"title": "置顶时失焦透明度",
|
||||
"description": "设置 Coco AI 窗口在置顶且失去焦点时的不透明度(10%–100%,100% 表示完全不透明)。"
|
||||
}
|
||||
},
|
||||
"updateVersion": {
|
||||
"title": "版本与更新",
|
||||
"snapshotUpdate": {
|
||||
"title": "快照版更新",
|
||||
"description": "抢先体验新功能,可能不稳定。"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -182,11 +200,15 @@
|
||||
"hotkey": "热键",
|
||||
"enabled": "启用状态"
|
||||
},
|
||||
"hits": {
|
||||
"addAlias": "添加别名",
|
||||
"recordHotkey": "录制热键"
|
||||
},
|
||||
"application": {
|
||||
"title": "应用程序",
|
||||
"hits": {
|
||||
"addAlias": "添加别名",
|
||||
"recordHotkey": "录制热键"
|
||||
"pathDuplication": "路径 \"{{0}}\" 已存在于搜索范围中。",
|
||||
"pathIncluded": "路径 \"{{0}}\" 已被其他搜索目录包含。"
|
||||
},
|
||||
"button": {
|
||||
"addDirectories": "添加目录"
|
||||
@@ -203,6 +225,9 @@
|
||||
"modified": "修改时间",
|
||||
"lastOpened": "上次打开时间"
|
||||
}
|
||||
},
|
||||
"calculator": {
|
||||
"title": "计算器"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -262,6 +287,10 @@
|
||||
"searchPopover": {
|
||||
"title": "搜索范围",
|
||||
"allScope": "所有范围"
|
||||
},
|
||||
"uploadFileHints": {
|
||||
"tooltip": "支持截图、上传文件,最多 50个,单个文件最大 100 MB。",
|
||||
"maxSize": "文件大小不能超过 100 MB。"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { RouterProvider } from "react-router-dom";
|
||||
|
||||
@@ -8,7 +7,5 @@ import "./i18n";
|
||||
import "./main.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
//<React.StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
// </React.StrictMode>
|
||||
);
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function Chat({}: ChatProps) {
|
||||
let response: any = await chat_history({
|
||||
serverId: currentService?.id,
|
||||
from: 0,
|
||||
size: 20,
|
||||
size: 100,
|
||||
query: keyword,
|
||||
});
|
||||
response = response ? JSON.parse(response) : null;
|
||||
@@ -109,7 +109,7 @@ export default function Chat({}: ChatProps) {
|
||||
serverId: currentService?.id,
|
||||
sessionId: chat?._id,
|
||||
from: 0,
|
||||
size: 20,
|
||||
size: 100,
|
||||
});
|
||||
response = response ? JSON.parse(response) : null;
|
||||
console.log("id_history", response);
|
||||
|
||||
@@ -4,8 +4,12 @@ import SearchChat from "@/components/SearchChat";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useSyncStore } from "@/hooks/useSyncStore";
|
||||
import UpdateApp from "@/components/UpdateApp";
|
||||
import { useAppearanceStore } from "@/stores/appearanceStore";
|
||||
|
||||
function MainApp() {
|
||||
const addError = useAppStore((state) => state.addError);
|
||||
|
||||
const setIsTauri = useAppStore((state) => state.setIsTauri);
|
||||
useEffect(() => {
|
||||
setIsTauri(true);
|
||||
@@ -17,12 +21,33 @@ function MainApp() {
|
||||
|
||||
useSyncStore();
|
||||
|
||||
const snapshotUpdate = useAppearanceStore((state) => state.snapshotUpdate);
|
||||
|
||||
const checkUpdate = useCallback(async () => {
|
||||
return platformAdapter.checkUpdate();
|
||||
}, []);
|
||||
|
||||
const relaunchApp = useCallback(async () => {
|
||||
return platformAdapter.relaunchApp();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!snapshotUpdate) return;
|
||||
|
||||
checkUpdate().catch((error) => {
|
||||
addError("Update failed:" + error, "error");
|
||||
});
|
||||
}, [snapshotUpdate]);
|
||||
|
||||
return (
|
||||
<SearchChat
|
||||
isTauri={true}
|
||||
hideCoco={hideCoco}
|
||||
hasModules={["search", "chat"]}
|
||||
/>
|
||||
<>
|
||||
<SearchChat
|
||||
isTauri={true}
|
||||
hideCoco={hideCoco}
|
||||
hasModules={["search", "chat"]}
|
||||
/>
|
||||
<UpdateApp checkUpdate={checkUpdate} relaunchApp={relaunchApp} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,6 @@ import Footer from "@/components/Common/UI/SettingsFooter";
|
||||
import { useTray } from "@/hooks/useTray";
|
||||
import Advanced from "@/components/Settings/Advanced";
|
||||
import Extensions from "@/components/Settings/Extensions";
|
||||
import { useAsyncEffect, useMount } from "ahooks";
|
||||
import { useApplicationsStore } from "@/stores/applicationsStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
|
||||
const tabIndexMap: { [key: string]: number } = {
|
||||
general: 0,
|
||||
@@ -26,9 +23,6 @@ const tabIndexMap: { [key: string]: number } = {
|
||||
|
||||
function SettingsPage() {
|
||||
const { t } = useTranslation();
|
||||
const searchPaths = useApplicationsStore((state) => state.searchPaths);
|
||||
const setSearchPaths = useApplicationsStore((state) => state.setSearchPaths);
|
||||
const setAllApps = useApplicationsStore((state) => state.setAllApps);
|
||||
|
||||
useTray();
|
||||
|
||||
@@ -57,34 +51,9 @@ function SettingsPage() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = defaultIndex !== 1 ? "auto" : "hidden";
|
||||
document.body.style.overflow = defaultIndex === 1 ? "hidden" : "auto";
|
||||
}, [defaultIndex]);
|
||||
|
||||
useMount(async () => {
|
||||
if (searchPaths.length > 0) return;
|
||||
|
||||
const paths = await platformAdapter.invokeBackend<string[]>(
|
||||
"get_default_search_paths"
|
||||
);
|
||||
|
||||
setSearchPaths(paths);
|
||||
});
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
if (searchPaths.length === 0) {
|
||||
return setAllApps([]);
|
||||
}
|
||||
|
||||
const apps = await platformAdapter.invokeBackend<any[]>(
|
||||
"list_app_with_metadata_in",
|
||||
{
|
||||
searchPath: searchPaths,
|
||||
}
|
||||
);
|
||||
|
||||
setAllApps(apps);
|
||||
}, [searchPaths]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="min-h-screen pb-8 bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
||||
|
||||
@@ -33,7 +33,7 @@ function WebApp({
|
||||
height = 590,
|
||||
headers = {
|
||||
"X-API-TOKEN":
|
||||
"cvqt6r02sdb2v3bkgip0x3ixv01f3r2lhnxoz1efbn160wm9og58wtv8t6wrv1ebvnvypuc23dx9pb33aemh",
|
||||
"d0erda62a89cir2p1rdgbdkjynbtwxa93e86op8fwyujsht11ckbcugw2zlp1lrvb87cnalv90p22jqbam21",
|
||||
"APP-INTEGRATION-ID": "cvkm9hmhpcemufsg3vug",
|
||||
},
|
||||
// token = "cva1j5ehpcenic3ir7k0h8fb8qtv35iwtywze248oscrej8yoivhb5b1hyovp24xejjk27jy9ddt69ewfi3n", // https://coco.infini.cloud
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useStartupStore } from "@/stores/startupStore";
|
||||
|
||||
export type AppState = {
|
||||
isChatMode: boolean;
|
||||
input: string;
|
||||
@@ -10,21 +12,18 @@ export type AppState = {
|
||||
};
|
||||
|
||||
export type AppAction =
|
||||
| { type: 'SET_CHAT_MODE'; payload: boolean }
|
||||
| { type: 'SET_INPUT'; payload: string }
|
||||
| { type: 'TOGGLE_SEARCH_ACTIVE' }
|
||||
| { type: 'TOGGLE_DEEP_THINK_ACTIVE' }
|
||||
| { type: 'TOGGLE_MCP_ACTIVE' }
|
||||
| { type: 'SET_TYPING'; payload: boolean }
|
||||
| { type: 'SET_LOADING'; payload: boolean };
|
||||
| { type: "SET_CHAT_MODE"; payload: boolean }
|
||||
| { type: "SET_INPUT"; payload: string }
|
||||
| { type: "TOGGLE_SEARCH_ACTIVE" }
|
||||
| { type: "TOGGLE_DEEP_THINK_ACTIVE" }
|
||||
| { type: "TOGGLE_MCP_ACTIVE" }
|
||||
| { type: "SET_TYPING"; payload: boolean }
|
||||
| { type: "SET_LOADING"; payload: boolean };
|
||||
|
||||
const getCachedChatMode = (): boolean => {
|
||||
try {
|
||||
const cached = localStorage.getItem('coco-chat-mode');
|
||||
return cached === 'true';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
const { defaultStartupWindow } = useStartupStore.getState();
|
||||
|
||||
return defaultStartupWindow === "chatMode";
|
||||
};
|
||||
|
||||
export const initialAppState: AppState = {
|
||||
@@ -35,26 +34,30 @@ export const initialAppState: AppState = {
|
||||
isDeepThinkActive: false,
|
||||
isMCPActive: false,
|
||||
isTyping: false,
|
||||
isLoading: false
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
export function appReducer(state: AppState, action: AppAction): AppState {
|
||||
switch (action.type) {
|
||||
case 'SET_CHAT_MODE':
|
||||
return { ...state, isChatMode: action.payload, isTransitioned: action.payload };
|
||||
case 'SET_INPUT':
|
||||
case "SET_CHAT_MODE":
|
||||
return {
|
||||
...state,
|
||||
isChatMode: action.payload,
|
||||
isTransitioned: action.payload,
|
||||
};
|
||||
case "SET_INPUT":
|
||||
return { ...state, input: action.payload };
|
||||
case 'TOGGLE_SEARCH_ACTIVE':
|
||||
case "TOGGLE_SEARCH_ACTIVE":
|
||||
return { ...state, isSearchActive: !state.isSearchActive };
|
||||
case 'TOGGLE_DEEP_THINK_ACTIVE':
|
||||
case "TOGGLE_DEEP_THINK_ACTIVE":
|
||||
return { ...state, isDeepThinkActive: !state.isDeepThinkActive };
|
||||
case 'TOGGLE_MCP_ACTIVE':
|
||||
case "TOGGLE_MCP_ACTIVE":
|
||||
return { ...state, isMCPActive: !state.isMCPActive };
|
||||
case 'SET_TYPING':
|
||||
case "SET_TYPING":
|
||||
return { ...state, isTyping: action.payload };
|
||||
case 'SET_LOADING':
|
||||
case "SET_LOADING":
|
||||
return { ...state, isLoading: action.payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useEffect } from "react";
|
||||
import { Outlet, useLocation } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAsyncEffect, useEventListener, useMount } from "ahooks";
|
||||
import { isString } from "lodash-es";
|
||||
import { error } from "@tauri-apps/plugin-log";
|
||||
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import useEscape from "@/hooks/useEscape";
|
||||
@@ -108,6 +110,12 @@ export default function Layout() {
|
||||
|
||||
useModifierKeyPress();
|
||||
|
||||
useEventListener("unhandledrejection", ({ reason }) => {
|
||||
const message = isString(reason) ? reason : JSON.stringify(reason);
|
||||
|
||||
error(message);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Outlet />
|
||||
|
||||
@@ -56,7 +56,7 @@ export type IAppStore = {
|
||||
setLanguage: (language: string) => void;
|
||||
isPinned: boolean;
|
||||
setIsPinned: (isPinned: boolean) => void;
|
||||
initializeListeners: () => void;
|
||||
initializeListeners: () => Promise<() => void>;
|
||||
|
||||
showCocoShortcuts: string[];
|
||||
setShowCocoShortcuts: (showCocoShortcuts: string[]) => void;
|
||||
@@ -130,10 +130,14 @@ export const useAppStore = create<IAppStore>()(
|
||||
isPinned: false,
|
||||
setIsPinned: (isPinned: boolean) => set({ isPinned }),
|
||||
initializeListeners: () => {
|
||||
platformAdapter.listenEvent(ENDPOINT_CHANGE_EVENT, (event: any) => {
|
||||
const { endpoint, endpoint_http, endpoint_websocket } = event.payload;
|
||||
set({ endpoint, endpoint_http, endpoint_websocket });
|
||||
});
|
||||
return platformAdapter.listenEvent(
|
||||
ENDPOINT_CHANGE_EVENT,
|
||||
(event: any) => {
|
||||
const { endpoint, endpoint_http, endpoint_websocket } =
|
||||
event.payload;
|
||||
set({ endpoint, endpoint_http, endpoint_websocket });
|
||||
}
|
||||
);
|
||||
},
|
||||
showCocoShortcuts: [],
|
||||
setShowCocoShortcuts: (showCocoShortcuts: string[]) => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user