mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-21 05:49:24 +01:00
Compare commits
45 Commits
convert_to
...
v0.7.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f992bfa92 | ||
|
|
e7dd27c744 | ||
|
|
7914836c3e | ||
|
|
b37bf1f7c7 | ||
|
|
419d9d55c5 | ||
|
|
d3ed54c771 | ||
|
|
8f26dbcbe6 | ||
|
|
663873ae14 | ||
|
|
286b1be212 | ||
|
|
37221782b0 | ||
|
|
644e291105 | ||
|
|
aae6984aa7 | ||
|
|
dbd296d399 | ||
|
|
e2ad25967d | ||
|
|
21b61d80d8 | ||
|
|
9f4c693ac4 | ||
|
|
45c27cac56 | ||
|
|
e46035afd4 | ||
|
|
1004bb73f4 | ||
|
|
d664fa7271 | ||
|
|
067fb7144f | ||
|
|
579f91f3aa | ||
|
|
abe2aecedf | ||
|
|
e8f9a4e627 | ||
|
|
22b1558e8b | ||
|
|
ca3b514a65 | ||
|
|
c694c4eda9 | ||
|
|
ac835c76aa | ||
|
|
25bbab7432 | ||
|
|
cca00e944e | ||
|
|
e78fe4ac89 | ||
|
|
60fd79f1fa | ||
|
|
5c0a865822 | ||
|
|
5b50e4b51b | ||
|
|
b97386a827 | ||
|
|
29aa26af94 | ||
|
|
3650d9914c | ||
|
|
f26031047c | ||
|
|
c8719926be | ||
|
|
f1dfc5c730 | ||
|
|
74ed642a42 | ||
|
|
5a17173620 | ||
|
|
29d14ff931 | ||
|
|
ad01504766 | ||
|
|
57ab08fb6d |
@@ -1,4 +1,4 @@
|
||||
name: Rust Code Compile Check
|
||||
name: Rust Code Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
- 'src-tauri/**'
|
||||
|
||||
jobs:
|
||||
compile-check:
|
||||
check:
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [ubuntu-latest, windows-latest, macos-latest]
|
||||
@@ -37,6 +37,13 @@ jobs:
|
||||
shell: bash
|
||||
run: cargo add --path ../pizza/lib/engine --features query_string_parser,persistence
|
||||
|
||||
- name: Format check
|
||||
working-directory: src-tauri
|
||||
shell: bash
|
||||
run: |
|
||||
rustup component add rustfmt
|
||||
cargo fmt --all --check
|
||||
|
||||
- name: Check compilation (Without Pizza engine enabled)
|
||||
working-directory: ./src-tauri
|
||||
run: cargo check
|
||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -62,6 +62,7 @@
|
||||
"traptitech",
|
||||
"unlisten",
|
||||
"unlistener",
|
||||
"unlisteners",
|
||||
"unminimize",
|
||||
"uuidv",
|
||||
"VITE",
|
||||
|
||||
@@ -13,6 +13,20 @@ Information about release notes of Coco Server is provided here.
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
### 🐛 Bug fix
|
||||
|
||||
- fix: correct enter key behavior #828
|
||||
|
||||
### ✈️ Improvements
|
||||
|
||||
- chore: web component add notification component #825
|
||||
|
||||
## 0.7.0 (2025-07-25)
|
||||
|
||||
### ❌ Breaking changes
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- feat: file search using spotlight #705
|
||||
- feat: voice input support in both search and chat modes #732
|
||||
- feat: text to speech now powered by LLM #750
|
||||
@@ -30,6 +44,17 @@ Information about release notes of Coco Server is provided here.
|
||||
- fix: resolved minor issues with voice playback #780
|
||||
- fix: fixed incorrect taskbar icon display on linux #783
|
||||
- fix: fix data inconsistency issue on secondary pages #784
|
||||
- fix: incorrect status when installing extension #789
|
||||
- fix: increase read_timeout for HTTP streaming stability #798
|
||||
- fix: enter key problem #794
|
||||
- fix: fix selection issue after renaming #800
|
||||
- fix: fix shortcut issue in windows context menu #804
|
||||
- fix: panic caused by "state() called before manage()" #806
|
||||
- fix: fix multiline input issue #808
|
||||
- fix: fix ctrl+k not working #815
|
||||
- fix: fix update window config sync #818
|
||||
- fix: fix enter key on subpages #819
|
||||
- fix: panic on Ubuntu (GNOME) when opening apps #821
|
||||
|
||||
### ✈️ Improvements
|
||||
|
||||
@@ -49,6 +74,9 @@ Information about release notes of Coco Server is provided here.
|
||||
- refactor: do status code check before deserializing response #767
|
||||
- style: splash adapts to the width of mobile phones #768
|
||||
- chore: search-chat add language and formatUrl parameters #775
|
||||
- chore: not request the interface if not logged in #795
|
||||
- refactor: clean up unsupported characters from query string in Win Search #802
|
||||
- chore: display backtrace in panic log #805
|
||||
|
||||
## 0.6.0 (2025-06-29)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "coco",
|
||||
"private": true,
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
84
src-tauri/Cargo.lock
generated
84
src-tauri/Cargo.lock
generated
@@ -840,7 +840,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "coco"
|
||||
version = "0.6.0"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"applications",
|
||||
@@ -852,6 +852,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"chinese-number",
|
||||
"chrono",
|
||||
"cocoa 0.24.1",
|
||||
"derive_more 2.0.1",
|
||||
"dirs 5.0.1",
|
||||
"enigo",
|
||||
@@ -872,6 +873,7 @@ dependencies = [
|
||||
"plist",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_plain",
|
||||
@@ -913,6 +915,22 @@ dependencies = [
|
||||
"zip 4.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cocoa"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"block",
|
||||
"cocoa-foundation 0.1.2",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics 0.22.3",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cocoa"
|
||||
version = "0.26.0"
|
||||
@@ -921,14 +939,28 @@ checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"block",
|
||||
"cocoa-foundation",
|
||||
"cocoa-foundation 0.2.0",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics",
|
||||
"core-graphics 0.24.0",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cocoa-foundation"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"block",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics-types 0.1.3",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cocoa-foundation"
|
||||
version = "0.2.0"
|
||||
@@ -938,7 +970,7 @@ dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"block",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics-types",
|
||||
"core-graphics-types 0.2.0",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
@@ -1055,6 +1087,19 @@ version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics"
|
||||
version = "0.22.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics-types 0.1.3",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics"
|
||||
version = "0.24.0"
|
||||
@@ -1063,11 +1108,22 @@ checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics-types",
|
||||
"core-graphics-types 0.2.0",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics-types"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation 0.9.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics-types"
|
||||
version = "0.2.0"
|
||||
@@ -1471,8 +1527,8 @@ version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67fd9ae1736d6ebb2e472740fbee86fb2178b8d56feb98a6751411d4c95b7e72"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"core-graphics",
|
||||
"cocoa 0.26.0",
|
||||
"core-graphics 0.24.0",
|
||||
"dunce",
|
||||
"gdk",
|
||||
"gdkx11",
|
||||
@@ -1561,7 +1617,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cf6f550bbbdd5fe66f39d429cb2604bcdacbf00dca0f5bbe2e9306a0009b7c6"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics",
|
||||
"core-graphics 0.24.0",
|
||||
"foreign-types-shared 0.3.1",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -2713,7 +2769,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.59.0",
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5720,7 +5776,7 @@ checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"cfg_aliases",
|
||||
"core-graphics",
|
||||
"core-graphics 0.24.0",
|
||||
"foreign-types 0.5.0",
|
||||
"js-sys",
|
||||
"log",
|
||||
@@ -5888,7 +5944,7 @@ dependencies = [
|
||||
"ntapi",
|
||||
"objc2-core-foundation",
|
||||
"objc2-io-kit",
|
||||
"windows 0.59.0",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5946,7 +6002,7 @@ checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics",
|
||||
"core-graphics 0.24.0",
|
||||
"crossbeam-channel",
|
||||
"dispatch",
|
||||
"dlopen2",
|
||||
@@ -6144,9 +6200,9 @@ source = "git+https://github.com/ahkohd/tauri-nspanel?branch=v2#d4b9df797959f8fa
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"block",
|
||||
"cocoa",
|
||||
"cocoa 0.26.0",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics",
|
||||
"core-graphics 0.24.0",
|
||||
"objc",
|
||||
"objc-foundation",
|
||||
"objc_id",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "coco"
|
||||
version = "0.6.0"
|
||||
version = "0.7.1"
|
||||
description = "Search, connect, collaborate – all in one place."
|
||||
authors = ["INFINI Labs"]
|
||||
edition = "2024"
|
||||
@@ -109,6 +109,7 @@ sysinfo = "0.35.2"
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }
|
||||
cocoa = "0.24"
|
||||
|
||||
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
|
||||
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
|
||||
@@ -127,6 +128,8 @@ strip = true # Ensures debug symbols are removed.
|
||||
tauri-plugin-autostart = "^2.2"
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
tauri-plugin-updater = { git = "https://github.com/infinilabs/plugins-workspace", branch = "v2" }
|
||||
# This should be compatible with the semver used by `tauri-plugin-updater`
|
||||
semver = { version = "1", features = ["serde"] }
|
||||
|
||||
[target."cfg(target_os = \"windows\")".dependencies]
|
||||
enigo="0.3"
|
||||
|
||||
@@ -99,10 +99,12 @@ pub async fn cancel_session_chat<R: Runtime>(
|
||||
_app_handle: AppHandle<R>,
|
||||
server_id: String,
|
||||
session_id: String,
|
||||
query_params: Option<HashMap<String, Value>>,
|
||||
) -> Result<String, String> {
|
||||
let path = format!("/chat/{}/_cancel", session_id);
|
||||
let query_params = convert_query_params_to_strings(query_params);
|
||||
|
||||
let response = HttpClient::post(&server_id, path.as_str(), None, None)
|
||||
let response = HttpClient::post(&server_id, path.as_str(), query_params, None)
|
||||
.await
|
||||
.map_err(|e| format!("Error cancel session: {}", e))?;
|
||||
|
||||
@@ -163,6 +165,7 @@ pub async fn chat_create<R: Runtime>(
|
||||
server_id: String,
|
||||
message: String,
|
||||
query_params: Option<HashMap<String, Value>>,
|
||||
client_id: String,
|
||||
) -> Result<(), String> {
|
||||
let body = if !message.is_empty() {
|
||||
let message = ChatRequestMessage {
|
||||
@@ -202,10 +205,12 @@ pub async fn chat_create<R: Runtime>(
|
||||
);
|
||||
let mut lines = tokio::io::BufReader::new(reader).lines();
|
||||
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
log::debug!("Received chat stream line: {}", &line);
|
||||
log::info!("client_id_create: {}", &client_id);
|
||||
|
||||
if let Err(err) = app_handle.emit("chat-create-stream", line) {
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
log::info!("Received chat stream line: {}", &line);
|
||||
|
||||
if let Err(err) = app_handle.emit(&client_id, line) {
|
||||
log::error!("Emit failed: {:?}", err);
|
||||
|
||||
print!("Error sending message: {:?}", err);
|
||||
@@ -255,6 +260,7 @@ pub async fn chat_chat<R: Runtime>(
|
||||
session_id: String,
|
||||
message: String,
|
||||
query_params: Option<HashMap<String, Value>>, //search,deep_thinking
|
||||
client_id: String,
|
||||
) -> Result<(), String> {
|
||||
let body = if !message.is_empty() {
|
||||
let message = ChatRequestMessage {
|
||||
@@ -295,11 +301,18 @@ pub async fn chat_chat<R: Runtime>(
|
||||
stream.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)),
|
||||
);
|
||||
let mut lines = tokio::io::BufReader::new(reader).lines();
|
||||
let mut first_log = true;
|
||||
|
||||
log::info!("client_id: {}", &client_id);
|
||||
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
log::debug!("Received chat stream line: {}", &line);
|
||||
log::info!("Received chat stream line: {}", &line);
|
||||
if first_log {
|
||||
log::info!("first stream line: {}", &line);
|
||||
first_log = false;
|
||||
}
|
||||
|
||||
if let Err(err) = app_handle.emit("chat-create-stream", line) {
|
||||
if let Err(err) = app_handle.emit(&client_id, line) {
|
||||
log::error!("Emit failed: {:?}", err);
|
||||
let _ = app_handle.emit("chat-create-error", format!("Emit failed: {:?}", err));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug,Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Connector {
|
||||
pub id: String,
|
||||
pub created: Option<String>,
|
||||
@@ -13,7 +13,7 @@ pub struct Connector {
|
||||
pub url: Option<String>,
|
||||
pub assets: Option<ConnectorAssets>,
|
||||
}
|
||||
#[derive(Debug,Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConnectorAssets {
|
||||
pub icons: Option<std::collections::HashMap<String, String>>,
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use tauri::AppHandle;
|
||||
use tauri::Runtime;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RichLabel {
|
||||
@@ -62,23 +64,21 @@ impl OnOpened {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn open(on_opened: OnOpened) -> Result<(), String> {
|
||||
pub(crate) async fn open<R: Runtime>(
|
||||
tauri_app_handle: AppHandle<R>,
|
||||
on_opened: OnOpened,
|
||||
) -> Result<(), String> {
|
||||
log::debug!("open({})", on_opened.url());
|
||||
|
||||
use crate::util::open as homemade_tauri_shell_open;
|
||||
use crate::GLOBAL_TAURI_APP_HANDLE;
|
||||
use std::process::Command;
|
||||
|
||||
let global_tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set");
|
||||
|
||||
match on_opened {
|
||||
OnOpened::Application { app_path } => {
|
||||
homemade_tauri_shell_open(global_tauri_app_handle.clone(), app_path).await?
|
||||
homemade_tauri_shell_open(tauri_app_handle.clone(), app_path).await?
|
||||
}
|
||||
OnOpened::Document { url } => {
|
||||
homemade_tauri_shell_open(global_tauri_app_handle.clone(), url).await?
|
||||
homemade_tauri_shell_open(tauri_app_handle.clone(), url).await?
|
||||
}
|
||||
OnOpened::Command { action } => {
|
||||
let mut cmd = Command::new(action.exec);
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
use thiserror::Error;
|
||||
|
||||
fn serialize_optional_status_code<S>(
|
||||
status_code: &Option<StatusCode>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match status_code {
|
||||
Some(code) => serializer.serialize_str(&format!("{:?}", code)),
|
||||
None => serializer.serialize_none(),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct ErrorCause {
|
||||
#[serde(default)]
|
||||
pub r#type: Option<String>,
|
||||
@@ -11,7 +25,7 @@ pub struct ErrorCause {
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
#[allow(unused)]
|
||||
pub struct ErrorDetail {
|
||||
#[serde(default)]
|
||||
pub root_cause: Option<Vec<ErrorCause>>,
|
||||
@@ -24,18 +38,22 @@ pub struct ErrorDetail {
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct ErrorResponse {
|
||||
#[serde(default)]
|
||||
pub error: Option<ErrorDetail>,
|
||||
#[serde(default)]
|
||||
#[allow(unused)]
|
||||
pub status: Option<u16>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, Serialize)]
|
||||
pub enum SearchError {
|
||||
#[error("HttpError: {0}")]
|
||||
HttpError(String),
|
||||
#[error("HttpError: status code [{status_code:?}], msg [{msg}]")]
|
||||
HttpError {
|
||||
#[serde(serialize_with = "serialize_optional_status_code")]
|
||||
status_code: Option<StatusCode>,
|
||||
msg: String,
|
||||
},
|
||||
|
||||
#[error("ParseError: {0}")]
|
||||
ParseError(String),
|
||||
@@ -43,12 +61,7 @@ pub enum SearchError {
|
||||
#[error("Timeout occurred")]
|
||||
Timeout,
|
||||
|
||||
#[error("UnknownError: {0}")]
|
||||
#[allow(dead_code)]
|
||||
Unknown(String),
|
||||
|
||||
#[error("InternalError: {0}")]
|
||||
#[allow(dead_code)]
|
||||
InternalError(String),
|
||||
}
|
||||
|
||||
@@ -59,7 +72,10 @@ impl From<reqwest::Error> for SearchError {
|
||||
} else if err.is_decode() {
|
||||
SearchError::ParseError(err.to_string())
|
||||
} else {
|
||||
SearchError::HttpError(err.to_string())
|
||||
SearchError::HttpError {
|
||||
status_code: err.status(),
|
||||
msg: err.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ pub async fn get_response_body_text(response: Response) -> Result<String, String
|
||||
return Err(fallback_error);
|
||||
}
|
||||
|
||||
|
||||
match serde_json::from_str::<common::error::ErrorResponse>(&body) {
|
||||
Ok(parsed_error) => {
|
||||
dbg!(&parsed_error);
|
||||
@@ -57,7 +56,6 @@ pub async fn get_response_body_text(response: Response) -> Result<String, String
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn convert_query_params_to_strings(
|
||||
query_params: Option<HashMap<String, JsonValue>>,
|
||||
) -> Option<Vec<String>> {
|
||||
@@ -68,10 +66,7 @@ pub fn convert_query_params_to_strings(
|
||||
JsonValue::Number(n) => Some(format!("{}={}", k, n)),
|
||||
JsonValue::Bool(b) => Some(format!("{}={}", k, b)),
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Skipping unsupported query value for key '{}': {:?}",
|
||||
k, v
|
||||
);
|
||||
eprintln!("Skipping unsupported query value for key '{}': {:?}", k, v);
|
||||
None
|
||||
}
|
||||
})
|
||||
|
||||
@@ -83,20 +83,6 @@ where
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn parse_search_results_with_score<T>(
|
||||
response: Response,
|
||||
) -> Result<Vec<(T, Option<f64>)>, Box<dyn Error>>
|
||||
where
|
||||
T: for<'de> Deserialize<'de> + std::fmt::Debug,
|
||||
{
|
||||
Ok(parse_search_hits(response)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|hit| (hit._source, hit._score))
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SearchQuery {
|
||||
pub from: u64,
|
||||
|
||||
@@ -50,9 +50,17 @@ pub struct Server {
|
||||
pub updated: String,
|
||||
#[serde(default = "default_enabled_type")]
|
||||
pub enabled: bool,
|
||||
/// Public Coco servers can be used without signing in.
|
||||
#[serde(default = "default_bool_type")]
|
||||
pub public: bool,
|
||||
|
||||
/// A coco server is available if:
|
||||
///
|
||||
/// 1. It is still online, we check this via the `GET /base_url/provider/_info`
|
||||
/// interface.
|
||||
/// 2. A user is logged in to this Coco server, i.e., a token is stored in the
|
||||
/// `SERVER_TOKEN_LIST_CACHE`.
|
||||
/// For public Coco servers, requirement 2 is not needed.
|
||||
#[serde(default = "default_available_type")]
|
||||
pub available: bool,
|
||||
|
||||
@@ -84,7 +92,10 @@ pub struct ServerAccessToken {
|
||||
#[serde(default = "default_empty_string")] // Custom default function for empty string
|
||||
pub id: String,
|
||||
pub access_token: String,
|
||||
pub expired_at: u32, //unix timestamp in seconds
|
||||
/// Unix timestamp in seconds
|
||||
///
|
||||
/// Currently, this is UNUSED.
|
||||
pub expired_at: u32,
|
||||
}
|
||||
|
||||
impl ServerAccessToken {
|
||||
|
||||
@@ -2,10 +2,15 @@ use crate::common::error::SearchError;
|
||||
use crate::common::search::SearchQuery;
|
||||
use crate::common::search::{QueryResponse, QuerySource};
|
||||
use async_trait::async_trait;
|
||||
use tauri::AppHandle;
|
||||
|
||||
#[async_trait]
|
||||
pub trait SearchSource: Send + Sync {
|
||||
fn get_type(&self) -> QuerySource;
|
||||
|
||||
async fn search(&self, query: SearchQuery) -> Result<QueryResponse, SearchError>;
|
||||
async fn search(
|
||||
&self,
|
||||
tauri_app_handle: AppHandle,
|
||||
query: SearchQuery,
|
||||
) -> Result<QueryResponse, SearchError>;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use super::super::Extension;
|
||||
use super::super::pizza_engine_runtime::RUNTIME_TX;
|
||||
use super::super::pizza_engine_runtime::SearchSourceState;
|
||||
use super::super::pizza_engine_runtime::Task;
|
||||
use super::super::pizza_engine_runtime::RUNTIME_TX;
|
||||
use super::super::Extension;
|
||||
use super::AppMetadata;
|
||||
use crate::GLOBAL_TAURI_APP_HANDLE;
|
||||
use crate::common::document::{DataSourceReference, Document, OnOpened};
|
||||
use crate::common::error::SearchError;
|
||||
use crate::common::search::{QueryResponse, QuerySource, SearchQuery};
|
||||
@@ -10,7 +11,6 @@ use crate::common::traits::SearchSource;
|
||||
use crate::extension::ExtensionType;
|
||||
use crate::extension::LOCAL_QUERY_SOURCE_TYPE;
|
||||
use crate::util::open;
|
||||
use crate::GLOBAL_TAURI_APP_HANDLE;
|
||||
use applications::{App, AppTrait};
|
||||
use async_trait::async_trait;
|
||||
use log::{error, warn};
|
||||
@@ -23,12 +23,12 @@ use pizza_engine::error::PizzaEngineError;
|
||||
use pizza_engine::search::{OriginalQuery, QueryContext, SearchResult, Searcher};
|
||||
use pizza_engine::store::{DiskStore, DiskStoreSnapshot};
|
||||
use pizza_engine::writer::Writer;
|
||||
use pizza_engine::{doc, Engine, EngineBuilder};
|
||||
use pizza_engine::{Engine, EngineBuilder, doc};
|
||||
use serde_json::Value as Json;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use tauri::{async_runtime, AppHandle, Manager, Runtime};
|
||||
use tauri_plugin_fs_pro::{icon, metadata, name, IconOptions};
|
||||
use tauri::{AppHandle, Manager, Runtime, async_runtime};
|
||||
use tauri_plugin_fs_pro::{IconOptions, icon, metadata, name};
|
||||
use tauri_plugin_global_shortcut::GlobalShortcutExt;
|
||||
use tauri_plugin_global_shortcut::Shortcut;
|
||||
use tauri_plugin_global_shortcut::ShortcutEvent;
|
||||
@@ -246,8 +246,8 @@ async fn index_applications_if_not_indexed<R: Runtime>(
|
||||
|
||||
if !index_exists {
|
||||
let search_path = {
|
||||
let disabled_app_list_and_search_path_store = tauri_app_handle
|
||||
.store(TAURI_STORE_DISABLED_APP_LIST_AND_SEARCH_PATH)?;
|
||||
let disabled_app_list_and_search_path_store =
|
||||
tauri_app_handle.store(TAURI_STORE_DISABLED_APP_LIST_AND_SEARCH_PATH)?;
|
||||
let search_path_json = disabled_app_list_and_search_path_store
|
||||
.get(TAURI_STORE_KEY_SEARCH_PATH)
|
||||
.unwrap_or_else(|| {
|
||||
@@ -294,7 +294,8 @@ async fn index_applications_if_not_indexed<R: Runtime>(
|
||||
// We don't error out because one failure won't break the whole thing
|
||||
if let Err(e) = writer.create_document(document).await {
|
||||
warn!(
|
||||
"failed to index application [app name: '{}', app path: '{}'] due to error [{}]", app_name, app_path, e
|
||||
"failed to index application [app name: '{}', app path: '{}'] due to error [{}]",
|
||||
app_name, app_path, e
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -401,7 +402,9 @@ impl<R: Runtime> Task for SearchApplicationsTask<R> {
|
||||
|
||||
let rx_dropped_error = callback.send(Ok(empty_hits)).is_err();
|
||||
if rx_dropped_error {
|
||||
warn!("failed to send local app search result back because the corresponding channel receiver end has been unexpected dropped, which could happen due to a low query timeout")
|
||||
warn!(
|
||||
"failed to send local app search result back because the corresponding channel receiver end has been unexpected dropped, which could happen due to a low query timeout"
|
||||
)
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -422,7 +425,9 @@ impl<R: Runtime> Task for SearchApplicationsTask<R> {
|
||||
// It will be passed to Pizza like "Google\nChrome". Using Display impl would result
|
||||
// in an invalid query DSL and serde will complain.
|
||||
let dsl = format!(
|
||||
"{{ \"query\": {{ \"bool\": {{ \"should\": [ {{ \"match\": {{ \"{FIELD_APP_NAME}\": {:?} }} }}, {{ \"prefix\": {{ \"{FIELD_APP_NAME}\": {:?} }} }} ] }} }} }}", self.query_string, self.query_string);
|
||||
"{{ \"query\": {{ \"bool\": {{ \"should\": [ {{ \"match\": {{ \"{FIELD_APP_NAME}\": {:?} }} }}, {{ \"prefix\": {{ \"{FIELD_APP_NAME}\": {:?} }} }} ] }} }} }}",
|
||||
self.query_string, self.query_string
|
||||
);
|
||||
|
||||
let state = state
|
||||
.as_mut_any()
|
||||
@@ -453,7 +458,9 @@ impl<R: Runtime> Task for SearchApplicationsTask<R> {
|
||||
|
||||
let rx_dropped_error = callback.send(Ok(search_result)).is_err();
|
||||
if rx_dropped_error {
|
||||
warn!("failed to send local app search result back because the corresponding channel receiver end has been unexpected dropped, which could happen due to a low query timeout")
|
||||
warn!(
|
||||
"failed to send local app search result back because the corresponding channel receiver end has been unexpected dropped, which could happen due to a low query timeout"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -536,7 +543,6 @@ impl ApplicationSearchSource {
|
||||
.set(TAURI_STORE_KEY_SEARCH_PATH, default_search_path);
|
||||
}
|
||||
|
||||
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
let index_applications_task = IndexAllApplicationsTask {
|
||||
tauri_app_handle: app_handle.clone(),
|
||||
@@ -557,7 +563,6 @@ impl ApplicationSearchSource {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -575,7 +580,11 @@ impl SearchSource for ApplicationSearchSource {
|
||||
}
|
||||
}
|
||||
|
||||
async fn search(&self, query: SearchQuery) -> Result<QueryResponse, SearchError> {
|
||||
async fn search(
|
||||
&self,
|
||||
_tauri_app_handle: AppHandle,
|
||||
query: SearchQuery,
|
||||
) -> Result<QueryResponse, SearchError> {
|
||||
let query_string = query
|
||||
.query_strings
|
||||
.get("query")
|
||||
@@ -833,7 +842,9 @@ pub fn unregister_app_hotkey<R: Runtime>(
|
||||
.global_shortcut()
|
||||
.is_registered(hotkey.as_str())
|
||||
{
|
||||
panic!("inconsistent state, tauri store a hotkey is stored in the tauri store but it is not registered");
|
||||
panic!(
|
||||
"inconsistent state, tauri store a hotkey is stored in the tauri store but it is not registered"
|
||||
);
|
||||
}
|
||||
|
||||
tauri_app_handle
|
||||
|
||||
@@ -32,7 +32,11 @@ impl SearchSource for ApplicationSearchSource {
|
||||
}
|
||||
}
|
||||
|
||||
async fn search(&self, _query: SearchQuery) -> Result<QueryResponse, SearchError> {
|
||||
async fn search(
|
||||
&self,
|
||||
_tauri_app_handle: AppHandle,
|
||||
_query: SearchQuery,
|
||||
) -> Result<QueryResponse, SearchError> {
|
||||
Ok(QueryResponse {
|
||||
source: self.get_type(),
|
||||
hits: Vec::new(),
|
||||
|
||||
@@ -10,6 +10,7 @@ use chinese_number::{ChineseCase, ChineseCountMethod, ChineseVariant, NumberToCh
|
||||
use num2words::Num2Words;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub(crate) const DATA_SOURCE_ID: &str = "Calculator";
|
||||
|
||||
@@ -120,7 +121,11 @@ impl SearchSource for CalculatorSource {
|
||||
}
|
||||
}
|
||||
|
||||
async fn search(&self, query: SearchQuery) -> Result<QueryResponse, SearchError> {
|
||||
async fn search(
|
||||
&self,
|
||||
_tauri_app_handle: AppHandle,
|
||||
query: SearchQuery,
|
||||
) -> Result<QueryResponse, SearchError> {
|
||||
let Some(query_string) = query.query_strings.get("query") else {
|
||||
return Ok(QueryResponse {
|
||||
source: self.get_type(),
|
||||
|
||||
@@ -4,6 +4,8 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use std::sync::LazyLock;
|
||||
use tauri::AppHandle;
|
||||
use tauri::Runtime;
|
||||
use tauri_plugin_store::StoreExt;
|
||||
|
||||
// Tauri store keys for file system configuration
|
||||
@@ -52,11 +54,7 @@ impl Default for FileSearchConfig {
|
||||
}
|
||||
|
||||
impl FileSearchConfig {
|
||||
pub(crate) fn get() -> Self {
|
||||
let tauri_app_handle = crate::GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set");
|
||||
|
||||
pub(crate) fn get<R: Runtime>(tauri_app_handle: &AppHandle<R>) -> Self {
|
||||
let store = tauri_app_handle
|
||||
.store(TAURI_STORE_FILE_SYSTEM_CONFIG)
|
||||
.unwrap_or_else(|e| {
|
||||
@@ -187,16 +185,17 @@ impl FileSearchConfig {
|
||||
|
||||
// Tauri commands for managing file system configuration
|
||||
#[tauri::command]
|
||||
pub async fn get_file_system_config() -> FileSearchConfig {
|
||||
FileSearchConfig::get()
|
||||
pub async fn get_file_system_config<R: Runtime>(
|
||||
tauri_app_handle: AppHandle<R>,
|
||||
) -> FileSearchConfig {
|
||||
FileSearchConfig::get(&tauri_app_handle)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_file_system_config(config: FileSearchConfig) -> Result<(), String> {
|
||||
let tauri_app_handle = crate::GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set");
|
||||
|
||||
pub async fn set_file_system_config<R: Runtime>(
|
||||
tauri_app_handle: AppHandle<R>,
|
||||
config: FileSearchConfig,
|
||||
) -> Result<(), String> {
|
||||
let store = tauri_app_handle
|
||||
.store(TAURI_STORE_FILE_SYSTEM_CONFIG)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use super::super::EXTENSION_ID;
|
||||
use super::super::config::FileSearchConfig;
|
||||
use super::super::config::SearchBy;
|
||||
use super::super::EXTENSION_ID;
|
||||
use crate::common::{
|
||||
document::{DataSourceReference, Document},
|
||||
};
|
||||
use crate::extension::OnOpened;
|
||||
use crate::common::document::{DataSourceReference, Document};
|
||||
use crate::extension::LOCAL_QUERY_SOURCE_TYPE;
|
||||
use crate::extension::OnOpened;
|
||||
use crate::util::file::get_file_icon;
|
||||
use futures::stream::Stream;
|
||||
use futures::stream::StreamExt;
|
||||
@@ -32,8 +30,7 @@ pub(crate) async fn hits(
|
||||
// Convert results to documents
|
||||
let mut hits: Vec<(Document, f64)> = Vec::new();
|
||||
while let Some(res_file_path) = iter.next().await {
|
||||
let file_path =
|
||||
res_file_path.map_err(|io_err| io_err.to_string())?;
|
||||
let file_path = res_file_path.map_err(|io_err| io_err.to_string())?;
|
||||
|
||||
let icon = get_file_icon(file_path.clone()).await;
|
||||
let file_path_of_type_path = camino::Utf8Path::new(&file_path);
|
||||
|
||||
@@ -3,25 +3,25 @@
|
||||
//! https://github.com/IRONAGE-Park/rag-sample/blob/3f0ad8c8012026cd3a7e453d08f041609426cb91/src/native/windows.rs
|
||||
//! is the starting point of this implementation.
|
||||
|
||||
use super::super::EXTENSION_ID;
|
||||
use super::super::config::FileSearchConfig;
|
||||
use super::super::config::SearchBy;
|
||||
use super::super::EXTENSION_ID;
|
||||
use crate::common::document::{DataSourceReference, Document};
|
||||
use crate::extension::OnOpened;
|
||||
use crate::extension::LOCAL_QUERY_SOURCE_TYPE;
|
||||
use crate::extension::OnOpened;
|
||||
use crate::util::file::get_file_icon;
|
||||
use windows::{
|
||||
core::{w, IUnknown, Interface, GUID, PWSTR},
|
||||
Win32::System::{
|
||||
Com::{CoCreateInstance, CLSCTX_INPROC_SERVER},
|
||||
Com::{CLSCTX_INPROC_SERVER, CoCreateInstance},
|
||||
Ole::{OleInitialize, OleUninitialize},
|
||||
Search::{
|
||||
IAccessor, ICommand, ICommandText, IDBCreateCommand, IDBCreateSession, IDBInitialize,
|
||||
IDataInitialize, IRowset, DBACCESSOR_ROWDATA, DBBINDING, DBMEMOWNER_CLIENTOWNED,
|
||||
DBPARAMIO_NOTPARAM, DBPART_VALUE, DBTYPE_WSTR, DB_NULL_HCHAPTER, HACCESSOR,
|
||||
MSDAINITIALIZE,
|
||||
DB_NULL_HCHAPTER, DBACCESSOR_ROWDATA, DBBINDING, DBMEMOWNER_CLIENTOWNED,
|
||||
DBPARAMIO_NOTPARAM, DBPART_VALUE, DBTYPE_WSTR, HACCESSOR, IAccessor, ICommand,
|
||||
ICommandText, IDBCreateCommand, IDBCreateSession, IDBInitialize, IDataInitialize,
|
||||
IRowset, MSDAINITIALIZE,
|
||||
},
|
||||
},
|
||||
core::{GUID, IUnknown, Interface, PWSTR, w},
|
||||
};
|
||||
|
||||
/// Owned version of `PWSTR` that holds the heap memory.
|
||||
@@ -50,6 +50,28 @@ impl<S: AsRef<str> + ?Sized> From<&S> for PwStrOwned {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to replace unsupported characters with whitespace.
|
||||
///
|
||||
/// Windows search will error out if it encounters these characters.
|
||||
///
|
||||
/// The complete list of unsupported characters is unknown and we don't know how
|
||||
/// to escape them, so let's replace them.
|
||||
fn query_string_cleanup(old: &str) -> String {
|
||||
const UNSUPPORTED_CHAR: [char; 2] = ['\'', '\n'];
|
||||
|
||||
// Using len in bytes is ok
|
||||
let mut chars = Vec::with_capacity(old.len());
|
||||
for char in old.chars() {
|
||||
if UNSUPPORTED_CHAR.contains(&char) {
|
||||
chars.push(' ');
|
||||
} else {
|
||||
chars.push(char);
|
||||
}
|
||||
}
|
||||
|
||||
chars.into_iter().collect()
|
||||
}
|
||||
|
||||
/// Helper function to construct the Windows Search SQL.
|
||||
///
|
||||
/// Paging is not natively supported by windows Search SQL, it only supports `size`
|
||||
@@ -69,11 +91,8 @@ fn query_sql(query_string: &str, from: usize, size: usize, config: &FileSearchCo
|
||||
"SELECT TOP {} System.ItemUrl, System.Search.Rank FROM SystemIndex WHERE",
|
||||
top_n
|
||||
);
|
||||
// Use debug print to escape the newline character, which cannot be handled by Windows Search.
|
||||
let query_string_debug_print = format!("{:?}", query_string);
|
||||
// Debug print will be double quoted, we need to trim them.
|
||||
let query_string_debug_print_len = query_string_debug_print.len();
|
||||
let query_string = &query_string_debug_print[1..(query_string_debug_print_len - 1)];
|
||||
|
||||
let query_string = query_string_cleanup(query_string);
|
||||
|
||||
let search_by_predicate = match config.search_by {
|
||||
SearchBy::Name => {
|
||||
@@ -454,7 +473,7 @@ pub(crate) async fn hits(
|
||||
//
|
||||
// I have no idea about the underlying root cause
|
||||
#[cfg(all(test, not(ci)))]
|
||||
mod test {
|
||||
mod test_windows_search {
|
||||
use super::*;
|
||||
|
||||
/// Helper function for ensuring `sql` is valid SQL by actually executing it.
|
||||
@@ -491,7 +510,10 @@ mod test {
|
||||
};
|
||||
let sql = query_sql("coco", 0, 10, &config);
|
||||
|
||||
assert_eq!(sql, "SELECT TOP 10 System.ItemUrl, System.Search.Rank FROM SystemIndex WHERE ((System.FileName LIKE '%coco%') OR CONTAINS('coco'))");
|
||||
assert_eq!(
|
||||
sql,
|
||||
"SELECT TOP 10 System.ItemUrl, System.Search.Rank FROM SystemIndex WHERE ((System.FileName LIKE '%coco%') OR CONTAINS('coco'))"
|
||||
);
|
||||
ensure_it_is_valid_sql(&sql);
|
||||
}
|
||||
|
||||
@@ -505,7 +527,10 @@ mod test {
|
||||
};
|
||||
let sql = query_sql("coco", 0, 10, &config);
|
||||
|
||||
assert_eq!(sql, "SELECT TOP 10 System.ItemUrl, System.Search.Rank FROM SystemIndex WHERE (System.FileName LIKE '%coco%') AND (SCOPE = 'file:C:/Users/')");
|
||||
assert_eq!(
|
||||
sql,
|
||||
"SELECT TOP 10 System.ItemUrl, System.Search.Rank FROM SystemIndex WHERE (System.FileName LIKE '%coco%') AND (SCOPE = 'file:C:/Users/')"
|
||||
);
|
||||
ensure_it_is_valid_sql(&sql);
|
||||
}
|
||||
|
||||
@@ -523,7 +548,10 @@ mod test {
|
||||
};
|
||||
let sql = query_sql("test", 0, 5, &config);
|
||||
|
||||
assert_eq!(sql, "SELECT TOP 5 System.ItemUrl, System.Search.Rank FROM SystemIndex WHERE (System.FileName LIKE '%test%') AND (SCOPE = 'file:C:/Users/' OR SCOPE = 'file:D:/Projects/' OR SCOPE = 'file:E:/Documents/')");
|
||||
assert_eq!(
|
||||
sql,
|
||||
"SELECT TOP 5 System.ItemUrl, System.Search.Rank FROM SystemIndex WHERE (System.FileName LIKE '%test%') AND (SCOPE = 'file:C:/Users/' OR SCOPE = 'file:D:/Projects/' OR SCOPE = 'file:E:/Documents/')"
|
||||
);
|
||||
ensure_it_is_valid_sql(&sql);
|
||||
}
|
||||
|
||||
@@ -537,7 +565,10 @@ mod test {
|
||||
};
|
||||
let sql = query_sql("file", 0, 20, &config);
|
||||
|
||||
assert_eq!(sql, "SELECT TOP 20 System.ItemUrl, System.Search.Rank FROM SystemIndex WHERE (System.FileName LIKE '%file%') AND ((NOT SCOPE = 'file:C:/Windows/'))");
|
||||
assert_eq!(
|
||||
sql,
|
||||
"SELECT TOP 20 System.ItemUrl, System.Search.Rank FROM SystemIndex WHERE (System.FileName LIKE '%file%') AND ((NOT SCOPE = 'file:C:/Windows/'))"
|
||||
);
|
||||
ensure_it_is_valid_sql(&sql);
|
||||
}
|
||||
|
||||
@@ -551,7 +582,10 @@ mod test {
|
||||
};
|
||||
let sql = query_sql("data", 5, 15, &config);
|
||||
|
||||
assert_eq!(sql, "SELECT TOP 20 System.ItemUrl, System.Search.Rank FROM SystemIndex WHERE (System.FileName LIKE '%data%') AND ((NOT SCOPE = 'file:C:/Windows/') AND (NOT SCOPE = 'file:C:/System/') AND (NOT SCOPE = 'file:C:/Temp/'))");
|
||||
assert_eq!(
|
||||
sql,
|
||||
"SELECT TOP 20 System.ItemUrl, System.Search.Rank FROM SystemIndex WHERE (System.FileName LIKE '%data%') AND ((NOT SCOPE = 'file:C:/Windows/') AND (NOT SCOPE = 'file:C:/System/') AND (NOT SCOPE = 'file:C:/Temp/'))"
|
||||
);
|
||||
ensure_it_is_valid_sql(&sql);
|
||||
}
|
||||
|
||||
@@ -565,7 +599,10 @@ mod test {
|
||||
};
|
||||
let sql = query_sql("readme", 0, 10, &config);
|
||||
|
||||
assert_eq!(sql, "SELECT TOP 10 System.ItemUrl, System.Search.Rank FROM SystemIndex WHERE (System.FileName LIKE '%readme%') AND (System.FileExtension = '.txt')");
|
||||
assert_eq!(
|
||||
sql,
|
||||
"SELECT TOP 10 System.ItemUrl, System.Search.Rank FROM SystemIndex WHERE (System.FileName LIKE '%readme%') AND (System.FileExtension = '.txt')"
|
||||
);
|
||||
ensure_it_is_valid_sql(&sql);
|
||||
}
|
||||
|
||||
@@ -579,7 +616,10 @@ mod test {
|
||||
};
|
||||
let sql = query_sql("config", 0, 50, &config);
|
||||
|
||||
assert_eq!(sql, "SELECT TOP 50 System.ItemUrl, System.Search.Rank FROM SystemIndex WHERE (System.FileName LIKE '%config%') AND (System.FileExtension = '.rs' OR System.FileExtension = '.toml' OR System.FileExtension = '.md' OR System.FileExtension = '.json')");
|
||||
assert_eq!(
|
||||
sql,
|
||||
"SELECT TOP 50 System.ItemUrl, System.Search.Rank FROM SystemIndex WHERE (System.FileName LIKE '%config%') AND (System.FileExtension = '.rs' OR System.FileExtension = '.toml' OR System.FileExtension = '.md' OR System.FileExtension = '.json')"
|
||||
);
|
||||
ensure_it_is_valid_sql(&sql);
|
||||
}
|
||||
|
||||
@@ -593,7 +633,10 @@ mod test {
|
||||
};
|
||||
let sql = query_sql("main", 10, 25, &config);
|
||||
|
||||
assert_eq!(sql, "SELECT TOP 35 System.ItemUrl, System.Search.Rank FROM SystemIndex WHERE (System.FileName LIKE '%main%') AND (SCOPE = 'file:C:/Projects/' OR SCOPE = 'file:D:/Code/') AND ((NOT SCOPE = 'file:C:/Projects/temp/')) AND (System.FileExtension = '.rs' OR System.FileExtension = '.ts')");
|
||||
assert_eq!(
|
||||
sql,
|
||||
"SELECT TOP 35 System.ItemUrl, System.Search.Rank FROM SystemIndex WHERE (System.FileName LIKE '%main%') AND (SCOPE = 'file:C:/Projects/' OR SCOPE = 'file:D:/Code/') AND ((NOT SCOPE = 'file:C:/Projects/temp/')) AND (System.FileExtension = '.rs' OR System.FileExtension = '.ts')"
|
||||
);
|
||||
ensure_it_is_valid_sql(&sql);
|
||||
}
|
||||
|
||||
@@ -607,7 +650,10 @@ mod test {
|
||||
};
|
||||
let sql = query_sql("hello-world", 0, 10, &config);
|
||||
|
||||
assert_eq!(sql, "SELECT TOP 10 System.ItemUrl, System.Search.Rank FROM SystemIndex WHERE (System.FileName LIKE '%hello-world%') AND (SCOPE = 'file:C:/Users/John Doe/') AND (System.FileExtension = '.c++')");
|
||||
assert_eq!(
|
||||
sql,
|
||||
"SELECT TOP 10 System.ItemUrl, System.Search.Rank FROM SystemIndex WHERE (System.FileName LIKE '%hello-world%') AND (SCOPE = 'file:C:/Users/John Doe/') AND (System.FileExtension = '.c++')"
|
||||
);
|
||||
ensure_it_is_valid_sql(&sql);
|
||||
}
|
||||
|
||||
@@ -628,3 +674,78 @@ mod test {
|
||||
ensure_it_is_valid_sql(&sql);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_query_string_cleanup_no_unsupported_chars() {
|
||||
let input = "hello world";
|
||||
let result = query_string_cleanup(input);
|
||||
assert_eq!(result, input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_string_cleanup_single_quote() {
|
||||
let input = "don't worry";
|
||||
let result = query_string_cleanup(input);
|
||||
assert_eq!(result, "don t worry");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_string_cleanup_newline() {
|
||||
let input = "line1\nline2";
|
||||
let result = query_string_cleanup(input);
|
||||
assert_eq!(result, "line1 line2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_string_cleanup_both_unsupported_chars() {
|
||||
let input = "don't\nworry";
|
||||
let result = query_string_cleanup(input);
|
||||
assert_eq!(result, "don t worry");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_string_cleanup_multiple_single_quotes() {
|
||||
let input = "it's a 'test' string";
|
||||
let result = query_string_cleanup(input);
|
||||
assert_eq!(result, "it s a test string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_string_cleanup_multiple_newlines() {
|
||||
let input = "line1\n\nline2\nline3";
|
||||
let result = query_string_cleanup(input);
|
||||
assert_eq!(result, "line1 line2 line3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_string_cleanup_empty_string() {
|
||||
let input = "";
|
||||
let result = query_string_cleanup(input);
|
||||
assert_eq!(result, input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_string_cleanup_only_unsupported_chars() {
|
||||
let input = "'\n'";
|
||||
let result = query_string_cleanup(input);
|
||||
assert_eq!(result, " ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_string_cleanup_unicode_characters() {
|
||||
let input = "héllo wörld's\nfile";
|
||||
let result = query_string_cleanup(input);
|
||||
assert_eq!(result, "héllo wörld s file");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_string_cleanup_special_chars_preserved() {
|
||||
let input = "test@file#name$with%symbols";
|
||||
let result = query_string_cleanup(input);
|
||||
assert_eq!(result, input);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::common::{
|
||||
use async_trait::async_trait;
|
||||
use config::FileSearchConfig;
|
||||
use hostname;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub(crate) const EXTENSION_ID: &str = "File Search";
|
||||
|
||||
@@ -40,7 +41,11 @@ impl SearchSource for FileSearchExtensionSearchSource {
|
||||
}
|
||||
}
|
||||
|
||||
async fn search(&self, query: SearchQuery) -> Result<QueryResponse, SearchError> {
|
||||
async fn search(
|
||||
&self,
|
||||
tauri_app_handle: AppHandle,
|
||||
query: SearchQuery,
|
||||
) -> Result<QueryResponse, SearchError> {
|
||||
let Some(query_string) = query.query_strings.get("query") else {
|
||||
return Ok(QueryResponse {
|
||||
source: self.get_type(),
|
||||
@@ -61,7 +66,7 @@ impl SearchSource for FileSearchExtensionSearchSource {
|
||||
}
|
||||
|
||||
// Get configuration from tauri store
|
||||
let config = FileSearchConfig::get();
|
||||
let config = FileSearchConfig::get(&tauri_app_handle);
|
||||
|
||||
// If search paths are empty, then the hit should be empty.
|
||||
//
|
||||
@@ -78,7 +83,9 @@ impl SearchSource for FileSearchExtensionSearchSource {
|
||||
// Execute search in a blocking task
|
||||
let query_source = self.get_type();
|
||||
|
||||
let hits = implementation::hits(&query_string, from, size, &config).await.map_err(SearchError::InternalError)?;
|
||||
let hits = implementation::hits(&query_string, from, size, &config)
|
||||
.await
|
||||
.map_err(SearchError::InternalError)?;
|
||||
|
||||
let total_hits = hits.len();
|
||||
Ok(QueryResponse {
|
||||
|
||||
@@ -9,29 +9,25 @@ pub mod pizza_engine_runtime;
|
||||
pub mod quick_ai_access;
|
||||
|
||||
use super::Extension;
|
||||
use crate::SearchSourceRegistry;
|
||||
use crate::extension::built_in::application::{set_apps_hotkey, unset_apps_hotkey};
|
||||
use crate::extension::{
|
||||
alter_extension_json_file, ExtensionBundleIdBorrowed, PLUGIN_JSON_FILE_NAME,
|
||||
ExtensionBundleIdBorrowed, PLUGIN_JSON_FILE_NAME, alter_extension_json_file,
|
||||
};
|
||||
use crate::{SearchSourceRegistry, GLOBAL_TAURI_APP_HANDLE};
|
||||
use anyhow::Context;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
|
||||
pub(crate) static BUILT_IN_EXTENSION_DIRECTORY: LazyLock<PathBuf> = LazyLock::new(|| {
|
||||
let mut resource_dir = GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set")
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.expect(
|
||||
pub(crate) fn get_built_in_extension_directory<R: Runtime>(
|
||||
tauri_app_handle: &AppHandle<R>,
|
||||
) -> PathBuf {
|
||||
let mut resource_dir = tauri_app_handle.path().app_data_dir().expect(
|
||||
"User home directory not found, which should be impossible on desktop environments",
|
||||
);
|
||||
resource_dir.push("built_in_extensions");
|
||||
|
||||
resource_dir
|
||||
});
|
||||
}
|
||||
|
||||
/// Helper function to load the built-in extension specified by `extension_id`, used
|
||||
/// in `list_built_in_extensions()`.
|
||||
@@ -86,7 +82,10 @@ async fn load_built_in_extension(
|
||||
.map_err(|e| e.to_string())?;
|
||||
let res_plugin_json = serde_json::from_str::<Extension>(&plugin_json_file_content);
|
||||
let Ok(plugin_json) = res_plugin_json else {
|
||||
log::warn!("user invalidated built-in extension [{}] file, overwriting it with the default template", extension_id);
|
||||
log::warn!(
|
||||
"user invalidated built-in extension [{}] file, overwriting it with the default template",
|
||||
extension_id
|
||||
);
|
||||
|
||||
// If the JSON file cannot be parsed as `struct Extension`, overwrite it with the default template and return.
|
||||
tokio::fs::write(plugin_json_file_path, default_plugin_json_file)
|
||||
@@ -137,13 +136,15 @@ async fn load_built_in_extension(
|
||||
/// We only read alias/hotkey/enabled from the JSON file, we have ensured that if
|
||||
/// alias/hotkey is not supported, then it will be `None`. Besides that, no further
|
||||
/// validation is needed because nothing could go wrong.
|
||||
pub(crate) async fn list_built_in_extensions() -> Result<Vec<Extension>, String> {
|
||||
let dir = BUILT_IN_EXTENSION_DIRECTORY.as_path();
|
||||
pub(crate) async fn list_built_in_extensions<R: Runtime>(
|
||||
tauri_app_handle: &AppHandle<R>,
|
||||
) -> Result<Vec<Extension>, String> {
|
||||
let dir = get_built_in_extension_directory(tauri_app_handle);
|
||||
|
||||
let mut built_in_extensions = Vec::new();
|
||||
built_in_extensions.push(
|
||||
load_built_in_extension(
|
||||
dir,
|
||||
&dir,
|
||||
application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME,
|
||||
application::PLUGIN_JSON_FILE,
|
||||
)
|
||||
@@ -151,7 +152,7 @@ pub(crate) async fn list_built_in_extensions() -> Result<Vec<Extension>, String>
|
||||
);
|
||||
built_in_extensions.push(
|
||||
load_built_in_extension(
|
||||
dir,
|
||||
&dir,
|
||||
calculator::DATA_SOURCE_ID,
|
||||
calculator::PLUGIN_JSON_FILE,
|
||||
)
|
||||
@@ -159,7 +160,7 @@ pub(crate) async fn list_built_in_extensions() -> Result<Vec<Extension>, String>
|
||||
);
|
||||
built_in_extensions.push(
|
||||
load_built_in_extension(
|
||||
dir,
|
||||
&dir,
|
||||
ai_overview::EXTENSION_ID,
|
||||
ai_overview::PLUGIN_JSON_FILE,
|
||||
)
|
||||
@@ -167,7 +168,7 @@ pub(crate) async fn list_built_in_extensions() -> Result<Vec<Extension>, String>
|
||||
);
|
||||
built_in_extensions.push(
|
||||
load_built_in_extension(
|
||||
dir,
|
||||
&dir,
|
||||
quick_ai_access::EXTENSION_ID,
|
||||
quick_ai_access::PLUGIN_JSON_FILE,
|
||||
)
|
||||
@@ -178,7 +179,7 @@ pub(crate) async fn list_built_in_extensions() -> Result<Vec<Extension>, String>
|
||||
if #[cfg(any(target_os = "macos", target_os = "windows"))] {
|
||||
built_in_extensions.push(
|
||||
load_built_in_extension(
|
||||
dir,
|
||||
&dir,
|
||||
file_search::EXTENSION_ID,
|
||||
file_search::PLUGIN_JSON_FILE,
|
||||
)
|
||||
@@ -232,12 +233,10 @@ pub(crate) fn is_extension_built_in(bundle_id: &ExtensionBundleIdBorrowed<'_>) -
|
||||
bundle_id.developer.is_none()
|
||||
}
|
||||
|
||||
pub(crate) async fn enable_built_in_extension(
|
||||
pub(crate) async fn enable_built_in_extension<R: Runtime>(
|
||||
tauri_app_handle: &AppHandle<R>,
|
||||
bundle_id: &ExtensionBundleIdBorrowed<'_>,
|
||||
) -> Result<(), String> {
|
||||
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set");
|
||||
let search_source_registry_tauri_state = tauri_app_handle.state::<SearchSourceRegistry>();
|
||||
|
||||
let update_extension = |extension: &mut Extension| -> Result<(), String> {
|
||||
@@ -254,7 +253,7 @@ pub(crate) async fn enable_built_in_extension(
|
||||
set_apps_hotkey(tauri_app_handle)?;
|
||||
|
||||
alter_extension_json_file(
|
||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||
&get_built_in_extension_directory(tauri_app_handle),
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
@@ -277,7 +276,7 @@ pub(crate) async fn enable_built_in_extension(
|
||||
.register_source(calculator_search)
|
||||
.await;
|
||||
alter_extension_json_file(
|
||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||
&get_built_in_extension_directory(tauri_app_handle),
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
@@ -286,7 +285,7 @@ pub(crate) async fn enable_built_in_extension(
|
||||
|
||||
if bundle_id.extension_id == quick_ai_access::EXTENSION_ID {
|
||||
alter_extension_json_file(
|
||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||
&get_built_in_extension_directory(tauri_app_handle),
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
@@ -295,7 +294,7 @@ pub(crate) async fn enable_built_in_extension(
|
||||
|
||||
if bundle_id.extension_id == ai_overview::EXTENSION_ID {
|
||||
alter_extension_json_file(
|
||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||
&get_built_in_extension_directory(tauri_app_handle),
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
@@ -310,7 +309,7 @@ pub(crate) async fn enable_built_in_extension(
|
||||
.register_source(file_system_search)
|
||||
.await;
|
||||
alter_extension_json_file(
|
||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||
&get_built_in_extension_directory(tauri_app_handle),
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
@@ -322,12 +321,10 @@ pub(crate) async fn enable_built_in_extension(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn disable_built_in_extension(
|
||||
pub(crate) async fn disable_built_in_extension<R: Runtime>(
|
||||
tauri_app_handle: &AppHandle<R>,
|
||||
bundle_id: &ExtensionBundleIdBorrowed<'_>,
|
||||
) -> Result<(), String> {
|
||||
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set");
|
||||
let search_source_registry_tauri_state = tauri_app_handle.state::<SearchSourceRegistry>();
|
||||
|
||||
let update_extension = |extension: &mut Extension| -> Result<(), String> {
|
||||
@@ -344,7 +341,7 @@ pub(crate) async fn disable_built_in_extension(
|
||||
unset_apps_hotkey(tauri_app_handle)?;
|
||||
|
||||
alter_extension_json_file(
|
||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||
&get_built_in_extension_directory(tauri_app_handle),
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
@@ -365,7 +362,7 @@ pub(crate) async fn disable_built_in_extension(
|
||||
.remove_source(bundle_id.extension_id)
|
||||
.await;
|
||||
alter_extension_json_file(
|
||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||
&get_built_in_extension_directory(tauri_app_handle),
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
@@ -374,7 +371,7 @@ pub(crate) async fn disable_built_in_extension(
|
||||
|
||||
if bundle_id.extension_id == quick_ai_access::EXTENSION_ID {
|
||||
alter_extension_json_file(
|
||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||
&get_built_in_extension_directory(tauri_app_handle),
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
@@ -384,7 +381,7 @@ pub(crate) async fn disable_built_in_extension(
|
||||
|
||||
if bundle_id.extension_id == ai_overview::EXTENSION_ID {
|
||||
alter_extension_json_file(
|
||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||
&get_built_in_extension_directory(tauri_app_handle),
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
@@ -399,7 +396,7 @@ pub(crate) async fn disable_built_in_extension(
|
||||
.remove_source(bundle_id.extension_id)
|
||||
.await;
|
||||
alter_extension_json_file(
|
||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||
&get_built_in_extension_directory(tauri_app_handle),
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
@@ -411,11 +408,11 @@ pub(crate) async fn disable_built_in_extension(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn set_built_in_extension_alias(bundle_id: &ExtensionBundleIdBorrowed<'_>, alias: &str) {
|
||||
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set");
|
||||
|
||||
pub(crate) fn set_built_in_extension_alias<R: Runtime>(
|
||||
tauri_app_handle: &AppHandle<R>,
|
||||
bundle_id: &ExtensionBundleIdBorrowed<'_>,
|
||||
alias: &str,
|
||||
) {
|
||||
if bundle_id.extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
||||
if let Some(app_path) = bundle_id.sub_extension_id {
|
||||
application::set_app_alias(tauri_app_handle, app_path, alias);
|
||||
@@ -423,14 +420,11 @@ pub(crate) fn set_built_in_extension_alias(bundle_id: &ExtensionBundleIdBorrowed
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn register_built_in_extension_hotkey(
|
||||
pub(crate) fn register_built_in_extension_hotkey<R: Runtime>(
|
||||
tauri_app_handle: &AppHandle<R>,
|
||||
bundle_id: &ExtensionBundleIdBorrowed<'_>,
|
||||
hotkey: &str,
|
||||
) -> Result<(), String> {
|
||||
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set");
|
||||
|
||||
if bundle_id.extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
||||
if let Some(app_path) = bundle_id.sub_extension_id {
|
||||
application::register_app_hotkey(&tauri_app_handle, app_path, hotkey)?;
|
||||
@@ -439,13 +433,10 @@ pub(crate) fn register_built_in_extension_hotkey(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn unregister_built_in_extension_hotkey(
|
||||
pub(crate) fn unregister_built_in_extension_hotkey<R: Runtime>(
|
||||
tauri_app_handle: &AppHandle<R>,
|
||||
bundle_id: &ExtensionBundleIdBorrowed<'_>,
|
||||
) -> Result<(), String> {
|
||||
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set");
|
||||
|
||||
if bundle_id.extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
||||
if let Some(app_path) = bundle_id.sub_extension_id {
|
||||
application::unregister_app_hotkey(&tauri_app_handle, app_path)?;
|
||||
@@ -490,12 +481,10 @@ fn load_extension_from_json_file(
|
||||
Ok(extension)
|
||||
}
|
||||
|
||||
pub(crate) async fn is_built_in_extension_enabled(
|
||||
pub(crate) async fn is_built_in_extension_enabled<R: Runtime>(
|
||||
tauri_app_handle: &AppHandle<R>,
|
||||
bundle_id: &ExtensionBundleIdBorrowed<'_>,
|
||||
) -> Result<bool, String> {
|
||||
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set");
|
||||
let search_source_registry_tauri_state = tauri_app_handle.state::<SearchSourceRegistry>();
|
||||
|
||||
if bundle_id.extension_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME
|
||||
@@ -523,7 +512,7 @@ pub(crate) async fn is_built_in_extension_enabled(
|
||||
|
||||
if bundle_id.extension_id == quick_ai_access::EXTENSION_ID {
|
||||
let extension = load_extension_from_json_file(
|
||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||
&get_built_in_extension_directory(tauri_app_handle),
|
||||
bundle_id.extension_id,
|
||||
)?;
|
||||
return Ok(extension.enabled);
|
||||
@@ -531,7 +520,7 @@ pub(crate) async fn is_built_in_extension_enabled(
|
||||
|
||||
if bundle_id.extension_id == ai_overview::EXTENSION_ID {
|
||||
let extension = load_extension_from_json_file(
|
||||
&BUILT_IN_EXTENSION_DIRECTORY.as_path(),
|
||||
&get_built_in_extension_directory(tauri_app_handle),
|
||||
bundle_id.extension_id,
|
||||
)?;
|
||||
return Ok(extension.enabled);
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
//! which forces us to create a dedicated thread/runtime to execute them.
|
||||
|
||||
use std::any::Any;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub(crate) trait SearchSourceState {
|
||||
|
||||
@@ -2,7 +2,8 @@ pub(crate) mod built_in;
|
||||
pub(crate) mod third_party;
|
||||
|
||||
use crate::common::document::OnOpened;
|
||||
use crate::{common::register::SearchSourceRegistry, GLOBAL_TAURI_APP_HANDLE};
|
||||
use crate::common::register::SearchSourceRegistry;
|
||||
use crate::util::platform::Platform;
|
||||
use anyhow::Context;
|
||||
use borrowme::{Borrow, ToOwned};
|
||||
use derive_more::Display;
|
||||
@@ -11,9 +12,8 @@ use serde::Serialize;
|
||||
use serde_json::Value as Json;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use tauri::Manager;
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
use third_party::THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE;
|
||||
use crate::util::platform::Platform;
|
||||
|
||||
pub const LOCAL_QUERY_SOURCE_TYPE: &str = "local";
|
||||
const PLUGIN_JSON_FILE_NAME: &str = "plugin.json";
|
||||
@@ -413,23 +413,24 @@ fn filter_out_extensions(
|
||||
/// * boolean: indicates if we found any invalid extensions
|
||||
/// * Vec<Extension>: loaded extensions
|
||||
#[tauri::command]
|
||||
pub(crate) async fn list_extensions(
|
||||
pub(crate) async fn list_extensions<R: Runtime>(
|
||||
tauri_app_handle: AppHandle<R>,
|
||||
query: Option<String>,
|
||||
extension_type: Option<ExtensionType>,
|
||||
list_enabled: bool,
|
||||
) -> Result<(bool, Vec<Extension>), String> {
|
||||
log::trace!("loading extensions");
|
||||
|
||||
let third_party_dir = third_party::THIRD_PARTY_EXTENSIONS_DIRECTORY.as_path();
|
||||
let third_party_dir = third_party::get_third_party_extension_directory(&tauri_app_handle);
|
||||
if !third_party_dir.try_exists().map_err(|e| e.to_string())? {
|
||||
tokio::fs::create_dir_all(third_party_dir)
|
||||
tokio::fs::create_dir_all(&third_party_dir)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
let (third_party_found_invalid_extension, mut third_party_extensions) =
|
||||
third_party::list_third_party_extensions(third_party_dir).await?;
|
||||
third_party::list_third_party_extensions(&third_party_dir).await?;
|
||||
|
||||
let built_in_extensions = built_in::list_built_in_extensions().await?;
|
||||
let built_in_extensions = built_in::list_built_in_extensions(&tauri_app_handle).await?;
|
||||
|
||||
let found_invalid_extension = third_party_found_invalid_extension;
|
||||
let mut extensions = {
|
||||
@@ -482,12 +483,12 @@ pub(crate) async fn list_extensions(
|
||||
Ok((found_invalid_extension, extensions))
|
||||
}
|
||||
|
||||
pub(crate) async fn init_extensions(mut extensions: Vec<Extension>) -> Result<(), String> {
|
||||
pub(crate) async fn init_extensions(
|
||||
tauri_app_handle: AppHandle,
|
||||
mut extensions: Vec<Extension>,
|
||||
) -> Result<(), String> {
|
||||
log::trace!("initializing extensions");
|
||||
|
||||
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set");
|
||||
let search_source_registry_tauri_state = tauri_app_handle.state::<SearchSourceRegistry>();
|
||||
|
||||
built_in::application::ApplicationSearchSource::prepare_index_and_store(
|
||||
@@ -508,7 +509,7 @@ pub(crate) async fn init_extensions(mut extensions: Vec<Extension>) -> Result<()
|
||||
.filter(|ext| ext.enabled)
|
||||
{
|
||||
built_in::init_built_in_extension(
|
||||
tauri_app_handle,
|
||||
&tauri_app_handle,
|
||||
&built_in_extension,
|
||||
&search_source_registry_tauri_state,
|
||||
)
|
||||
@@ -517,7 +518,7 @@ pub(crate) async fn init_extensions(mut extensions: Vec<Extension>) -> Result<()
|
||||
|
||||
// Now the third-party extensions
|
||||
let third_party_search_source = third_party::ThirdPartyExtensionsSearchSource::new(extensions);
|
||||
third_party_search_source.init().await?;
|
||||
third_party_search_source.init(&tauri_app_handle).await?;
|
||||
let third_party_search_source_clone = third_party_search_source.clone();
|
||||
// Set the global search source so that we can access it in `#[tauri::command]`s
|
||||
// ignore the result because this function will be invoked twice, which
|
||||
@@ -531,79 +532,96 @@ pub(crate) async fn init_extensions(mut extensions: Vec<Extension>) -> Result<()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn enable_extension(bundle_id: ExtensionBundleId) -> Result<(), String> {
|
||||
pub(crate) async fn enable_extension(
|
||||
tauri_app_handle: AppHandle,
|
||||
bundle_id: ExtensionBundleId,
|
||||
) -> Result<(), String> {
|
||||
let bundle_id_borrowed = bundle_id.borrow();
|
||||
|
||||
if built_in::is_extension_built_in(&bundle_id_borrowed) {
|
||||
built_in::enable_built_in_extension(&bundle_id_borrowed).await?;
|
||||
built_in::enable_built_in_extension(&tauri_app_handle, &bundle_id_borrowed).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
third_party::THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE.get().expect("global third party search source not set, looks like init_extensions() has not been executed").enable_extension(&bundle_id_borrowed).await
|
||||
third_party::THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE.get().expect("global third party search source not set, looks like init_extensions() has not been executed").enable_extension(&tauri_app_handle, &bundle_id_borrowed).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn disable_extension(bundle_id: ExtensionBundleId) -> Result<(), String> {
|
||||
pub(crate) async fn disable_extension(
|
||||
tauri_app_handle: AppHandle,
|
||||
bundle_id: ExtensionBundleId,
|
||||
) -> Result<(), String> {
|
||||
let bundle_id_borrowed = bundle_id.borrow();
|
||||
|
||||
if built_in::is_extension_built_in(&bundle_id_borrowed) {
|
||||
built_in::disable_built_in_extension(&bundle_id_borrowed).await?;
|
||||
built_in::disable_built_in_extension(&tauri_app_handle, &bundle_id_borrowed).await?;
|
||||
return Ok(());
|
||||
}
|
||||
third_party::THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE.get().expect("global third party search source not set, looks like init_extensions() has not been executed").disable_extension(&bundle_id_borrowed).await
|
||||
third_party::THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE.get().expect("global third party search source not set, looks like init_extensions() has not been executed").disable_extension(&tauri_app_handle, &bundle_id_borrowed).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn set_extension_alias(
|
||||
tauri_app_handle: AppHandle,
|
||||
bundle_id: ExtensionBundleId,
|
||||
alias: String,
|
||||
) -> Result<(), String> {
|
||||
let bundle_id_borrowed = bundle_id.borrow();
|
||||
|
||||
if built_in::is_extension_built_in(&bundle_id_borrowed) {
|
||||
built_in::set_built_in_extension_alias(&bundle_id_borrowed, &alias);
|
||||
built_in::set_built_in_extension_alias(&tauri_app_handle, &bundle_id_borrowed, &alias);
|
||||
return Ok(());
|
||||
}
|
||||
third_party::THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE.get().expect("global third party search source not set, looks like init_extensions() has not been executed").set_extension_alias(&bundle_id_borrowed, &alias).await
|
||||
third_party::THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE.get().expect("global third party search source not set, looks like init_extensions() has not been executed").set_extension_alias(&tauri_app_handle, &bundle_id_borrowed, &alias).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn register_extension_hotkey(
|
||||
tauri_app_handle: AppHandle,
|
||||
bundle_id: ExtensionBundleId,
|
||||
hotkey: String,
|
||||
) -> Result<(), String> {
|
||||
let bundle_id_borrowed = bundle_id.borrow();
|
||||
|
||||
if built_in::is_extension_built_in(&bundle_id_borrowed) {
|
||||
built_in::register_built_in_extension_hotkey(&bundle_id_borrowed, &hotkey)?;
|
||||
built_in::register_built_in_extension_hotkey(
|
||||
&tauri_app_handle,
|
||||
&bundle_id_borrowed,
|
||||
&hotkey,
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
third_party::THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE.get().expect("global third party search source not set, looks like init_extensions() has not been executed").register_extension_hotkey(&bundle_id_borrowed, &hotkey).await
|
||||
third_party::THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE.get().expect("global third party search source not set, looks like init_extensions() has not been executed").register_extension_hotkey(&tauri_app_handle, &bundle_id_borrowed, &hotkey).await
|
||||
}
|
||||
|
||||
/// NOTE: this function won't error out if the extension specified by `extension_id`
|
||||
/// has no hotkey set because we need it to behave like this.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn unregister_extension_hotkey(
|
||||
tauri_app_handle: AppHandle,
|
||||
bundle_id: ExtensionBundleId,
|
||||
) -> Result<(), String> {
|
||||
let bundle_id_borrowed = bundle_id.borrow();
|
||||
|
||||
if built_in::is_extension_built_in(&bundle_id_borrowed) {
|
||||
built_in::unregister_built_in_extension_hotkey(&bundle_id_borrowed)?;
|
||||
built_in::unregister_built_in_extension_hotkey(&tauri_app_handle, &bundle_id_borrowed)?;
|
||||
return Ok(());
|
||||
}
|
||||
third_party::THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE.get().expect("global third party search source not set, looks like init_extensions() has not been executed").unregister_extension_hotkey(&bundle_id_borrowed).await?;
|
||||
third_party::THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE.get().expect("global third party search source not set, looks like init_extensions() has not been executed").unregister_extension_hotkey(&tauri_app_handle, &bundle_id_borrowed).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn is_extension_enabled(bundle_id: ExtensionBundleId) -> Result<bool, String> {
|
||||
pub(crate) async fn is_extension_enabled(
|
||||
tauri_app_handle: AppHandle,
|
||||
bundle_id: ExtensionBundleId,
|
||||
) -> Result<bool, String> {
|
||||
let bundle_id_borrowed = bundle_id.borrow();
|
||||
|
||||
if built_in::is_extension_built_in(&bundle_id_borrowed) {
|
||||
return built_in::is_built_in_extension_enabled(&bundle_id_borrowed).await;
|
||||
return built_in::is_built_in_extension_enabled(&tauri_app_handle, &bundle_id_borrowed)
|
||||
.await;
|
||||
}
|
||||
third_party::THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE.get().expect("global third party search source not set, looks like init_extensions() has not been executed").is_extension_enabled(&bundle_id_borrowed).await
|
||||
}
|
||||
@@ -704,10 +722,7 @@ fn alter_extension_json_file(
|
||||
|
||||
// Search in quicklinks
|
||||
if let Some(ref mut quicklinks) = root_extension.quicklinks {
|
||||
if let Some(link) = quicklinks
|
||||
.iter_mut()
|
||||
.find(|lnk| lnk.id == sub_extension_id)
|
||||
{
|
||||
if let Some(link) = quicklinks.iter_mut().find(|lnk| lnk.id == sub_extension_id) {
|
||||
how(link)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
123
src-tauri/src/extension/third_party/mod.rs
vendored
123
src-tauri/src/extension/third_party/mod.rs
vendored
@@ -1,14 +1,14 @@
|
||||
pub(crate) mod store;
|
||||
|
||||
use super::alter_extension_json_file;
|
||||
use super::canonicalize_relative_icon_path;
|
||||
use super::Extension;
|
||||
use super::ExtensionType;
|
||||
use super::LOCAL_QUERY_SOURCE_TYPE;
|
||||
use super::PLUGIN_JSON_FILE_NAME;
|
||||
use crate::common::document::open;
|
||||
use super::alter_extension_json_file;
|
||||
use super::canonicalize_relative_icon_path;
|
||||
use crate::common::document::DataSourceReference;
|
||||
use crate::common::document::Document;
|
||||
use crate::common::document::open;
|
||||
use crate::common::error::SearchError;
|
||||
use crate::common::search::QueryResponse;
|
||||
use crate::common::search::QuerySource;
|
||||
@@ -16,7 +16,6 @@ use crate::common::search::SearchQuery;
|
||||
use crate::common::traits::SearchSource;
|
||||
use crate::extension::ExtensionBundleIdBorrowed;
|
||||
use crate::util::platform::Platform;
|
||||
use crate::GLOBAL_TAURI_APP_HANDLE;
|
||||
use async_trait::async_trait;
|
||||
use borrowme::ToOwned;
|
||||
use function_name::named;
|
||||
@@ -24,29 +23,27 @@ use std::ffi::OsStr;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::OnceLock;
|
||||
use tauri::async_runtime;
|
||||
use tauri::AppHandle;
|
||||
use tauri::Manager;
|
||||
use tauri::Runtime;
|
||||
use tauri::async_runtime;
|
||||
use tauri_plugin_global_shortcut::GlobalShortcutExt;
|
||||
use tauri_plugin_global_shortcut::ShortcutState;
|
||||
use tokio::fs::read_dir;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::sync::RwLockWriteGuard;
|
||||
|
||||
pub(crate) static THIRD_PARTY_EXTENSIONS_DIRECTORY: LazyLock<PathBuf> = LazyLock::new(|| {
|
||||
let mut app_data_dir = GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set")
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.expect(
|
||||
pub(crate) fn get_third_party_extension_directory<R: Runtime>(
|
||||
tauri_app_handle: &AppHandle<R>,
|
||||
) -> PathBuf {
|
||||
let mut app_data_dir = tauri_app_handle.path().app_data_dir().expect(
|
||||
"User home directory not found, which should be impossible on desktop environments",
|
||||
);
|
||||
app_data_dir.push("third_party_extensions");
|
||||
|
||||
app_data_dir
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) async fn list_third_party_extensions(
|
||||
directory: &Path,
|
||||
@@ -201,7 +198,15 @@ fn validate_extension(
|
||||
// Extension is incompatible
|
||||
if let Some(ref platforms) = extension.platforms {
|
||||
if !platforms.contains(¤t_platform) {
|
||||
log::warn!("extension [{}] is not compatible with the current platform [{}], it is available to {:?}", extension.id, current_platform, platforms.iter().map(|os|os.to_string()).collect::<Vec<_>>());
|
||||
log::warn!(
|
||||
"extension [{}] is not compatible with the current platform [{}], it is available to {:?}",
|
||||
extension.id,
|
||||
current_platform,
|
||||
platforms
|
||||
.iter()
|
||||
.map(|os| os.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -320,7 +325,8 @@ fn validate_sub_items(extension_id: &str, sub_items: &[Extension]) -> bool {
|
||||
if sub_item.r#type == ExtensionType::Group || sub_item.r#type == ExtensionType::Extension {
|
||||
log::warn!(
|
||||
"invalid extension sub-item [{}-{}]: sub-item should not be of type [Group] or [Extension]",
|
||||
extension_id, sub_item.id
|
||||
extension_id,
|
||||
sub_item.id
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -394,11 +400,11 @@ impl ThirdPartyExtensionsSearchSource {
|
||||
/// Note that when you enable a parent extension, its **enabled** children extensions
|
||||
/// should also be enabled.
|
||||
#[async_recursion::async_recursion]
|
||||
async fn _enable_extension(extension: &Extension) -> Result<(), String> {
|
||||
async fn _enable_extension(
|
||||
tauri_app_handle: &AppHandle,
|
||||
extension: &Extension,
|
||||
) -> Result<(), String> {
|
||||
if extension.supports_alias_hotkey() {
|
||||
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set");
|
||||
if let Some(ref hotkey) = extension.hotkey {
|
||||
let on_opened = extension.on_opened().unwrap_or_else(|| panic!( "extension has hotkey, but on_open() returns None, extension ID [{}], extension type [{:?}]", extension.id, extension.r#type));
|
||||
|
||||
@@ -406,12 +412,14 @@ impl ThirdPartyExtensionsSearchSource {
|
||||
|
||||
tauri_app_handle
|
||||
.global_shortcut()
|
||||
.on_shortcut(hotkey.as_str(), move |_tauri_app_handle, _hotkey, event| {
|
||||
.on_shortcut(hotkey.as_str(), move |tauri_app_handle, _hotkey, event| {
|
||||
let on_opened_clone = on_opened.clone();
|
||||
let extension_id_clone = extension_id_clone.clone();
|
||||
let app_handle_clone = tauri_app_handle.clone();
|
||||
|
||||
if event.state() == ShortcutState::Pressed {
|
||||
async_runtime::spawn(async move {
|
||||
let result = open(on_opened_clone).await;
|
||||
let result = open(app_handle_clone, on_opened_clone).await;
|
||||
if let Err(msg) = result {
|
||||
log::warn!(
|
||||
"failed to open extension [{}], error [{}]",
|
||||
@@ -430,19 +438,19 @@ impl ThirdPartyExtensionsSearchSource {
|
||||
if extension.r#type.contains_sub_items() {
|
||||
if let Some(commands) = &extension.commands {
|
||||
for command in commands.iter().filter(|ext| ext.enabled) {
|
||||
Self::_enable_extension(command).await?;
|
||||
Self::_enable_extension(&tauri_app_handle, command).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(scripts) = &extension.scripts {
|
||||
for script in scripts.iter().filter(|ext| ext.enabled) {
|
||||
Self::_enable_extension(script).await?;
|
||||
Self::_enable_extension(&tauri_app_handle, script).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(quicklinks) = &extension.quicklinks {
|
||||
for quicklink in quicklinks.iter().filter(|ext| ext.enabled) {
|
||||
Self::_enable_extension(quicklink).await?;
|
||||
Self::_enable_extension(&tauri_app_handle, quicklink).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -457,12 +465,11 @@ impl ThirdPartyExtensionsSearchSource {
|
||||
/// Note that when you disable a parent extension, its **enabled** children extensions
|
||||
/// should also be disabled.
|
||||
#[async_recursion::async_recursion]
|
||||
async fn _disable_extension(extension: &Extension) -> Result<(), String> {
|
||||
async fn _disable_extension(
|
||||
tauri_app_handle: &AppHandle,
|
||||
extension: &Extension,
|
||||
) -> Result<(), String> {
|
||||
if let Some(ref hotkey) = extension.hotkey {
|
||||
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set");
|
||||
|
||||
tauri_app_handle
|
||||
.global_shortcut()
|
||||
.unregister(hotkey.as_str())
|
||||
@@ -473,19 +480,19 @@ impl ThirdPartyExtensionsSearchSource {
|
||||
if extension.r#type.contains_sub_items() {
|
||||
if let Some(commands) = &extension.commands {
|
||||
for command in commands.iter().filter(|ext| ext.enabled) {
|
||||
Self::_disable_extension(command).await?;
|
||||
Self::_disable_extension(tauri_app_handle, command).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(scripts) = &extension.scripts {
|
||||
for script in scripts.iter().filter(|ext| ext.enabled) {
|
||||
Self::_disable_extension(script).await?;
|
||||
Self::_disable_extension(tauri_app_handle, script).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(quicklinks) = &extension.quicklinks {
|
||||
for quicklink in quicklinks.iter().filter(|ext| ext.enabled) {
|
||||
Self::_disable_extension(quicklink).await?;
|
||||
Self::_disable_extension(tauri_app_handle, quicklink).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -504,6 +511,7 @@ impl ThirdPartyExtensionsSearchSource {
|
||||
#[named]
|
||||
pub(super) async fn enable_extension(
|
||||
&self,
|
||||
tauri_app_handle: &AppHandle,
|
||||
bundle_id: &ExtensionBundleIdBorrowed<'_>,
|
||||
) -> Result<(), String> {
|
||||
let mut extensions_write_lock = self.inner.extensions.write().await;
|
||||
@@ -531,11 +539,11 @@ impl ThirdPartyExtensionsSearchSource {
|
||||
|
||||
update_extension(extension)?;
|
||||
alter_extension_json_file(
|
||||
&THIRD_PARTY_EXTENSIONS_DIRECTORY,
|
||||
&get_third_party_extension_directory(tauri_app_handle),
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
Self::_enable_extension(extension).await?;
|
||||
Self::_enable_extension(tauri_app_handle, extension).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -543,6 +551,7 @@ impl ThirdPartyExtensionsSearchSource {
|
||||
#[named]
|
||||
pub(super) async fn disable_extension(
|
||||
&self,
|
||||
tauri_app_handle: &AppHandle,
|
||||
bundle_id: &ExtensionBundleIdBorrowed<'_>,
|
||||
) -> Result<(), String> {
|
||||
let mut extensions_write_lock = self.inner.extensions.write().await;
|
||||
@@ -570,11 +579,11 @@ impl ThirdPartyExtensionsSearchSource {
|
||||
|
||||
update_extension(extension)?;
|
||||
alter_extension_json_file(
|
||||
&THIRD_PARTY_EXTENSIONS_DIRECTORY,
|
||||
&get_third_party_extension_directory(tauri_app_handle),
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
Self::_disable_extension(extension).await?;
|
||||
Self::_disable_extension(tauri_app_handle, extension).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -582,6 +591,7 @@ impl ThirdPartyExtensionsSearchSource {
|
||||
#[named]
|
||||
pub(super) async fn set_extension_alias(
|
||||
&self,
|
||||
tauri_app_handle: &AppHandle,
|
||||
bundle_id: &ExtensionBundleIdBorrowed<'_>,
|
||||
alias: &str,
|
||||
) -> Result<(), String> {
|
||||
@@ -602,7 +612,7 @@ impl ThirdPartyExtensionsSearchSource {
|
||||
|
||||
update_extension(extension)?;
|
||||
alter_extension_json_file(
|
||||
&THIRD_PARTY_EXTENSIONS_DIRECTORY,
|
||||
&get_third_party_extension_directory(tauri_app_handle),
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
@@ -612,11 +622,11 @@ impl ThirdPartyExtensionsSearchSource {
|
||||
|
||||
/// Initialize the third-party extensions, which literally means
|
||||
/// enabling/activating the enabled extensions.
|
||||
pub(super) async fn init(&self) -> Result<(), String> {
|
||||
pub(super) async fn init(&self, tauri_app_handle: &AppHandle) -> Result<(), String> {
|
||||
let extensions_read_lock = self.inner.extensions.read().await;
|
||||
|
||||
for extension in extensions_read_lock.iter().filter(|ext| ext.enabled) {
|
||||
Self::_enable_extension(extension).await?;
|
||||
Self::_enable_extension(tauri_app_handle, extension).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -625,10 +635,12 @@ impl ThirdPartyExtensionsSearchSource {
|
||||
#[named]
|
||||
pub(super) async fn register_extension_hotkey(
|
||||
&self,
|
||||
tauri_app_handle: &AppHandle,
|
||||
bundle_id: &ExtensionBundleIdBorrowed<'_>,
|
||||
hotkey: &str,
|
||||
) -> Result<(), String> {
|
||||
self.unregister_extension_hotkey(bundle_id).await?;
|
||||
self.unregister_extension_hotkey(tauri_app_handle, bundle_id)
|
||||
.await?;
|
||||
|
||||
let mut extensions_write_lock = self.inner.extensions.write().await;
|
||||
let extension =
|
||||
@@ -648,15 +660,12 @@ impl ThirdPartyExtensionsSearchSource {
|
||||
// Update extension (memory and file)
|
||||
update_extension(extension)?;
|
||||
alter_extension_json_file(
|
||||
&THIRD_PARTY_EXTENSIONS_DIRECTORY,
|
||||
&get_third_party_extension_directory(tauri_app_handle),
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
|
||||
// Set hotkey
|
||||
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set");
|
||||
let on_opened = extension.on_opened().unwrap_or_else(|| panic!(
|
||||
"setting hotkey for an extension that cannot be opened, extension ID [{:?}], extension type [{:?}]", bundle_id, extension.r#type,
|
||||
));
|
||||
@@ -664,12 +673,14 @@ impl ThirdPartyExtensionsSearchSource {
|
||||
let bundle_id_owned = bundle_id.to_owned();
|
||||
tauri_app_handle
|
||||
.global_shortcut()
|
||||
.on_shortcut(hotkey, move |_tauri_app_handle, _hotkey, event| {
|
||||
.on_shortcut(hotkey, move |tauri_app_handle, _hotkey, event| {
|
||||
let on_opened_clone = on_opened.clone();
|
||||
let bundle_id_clone = bundle_id_owned.clone();
|
||||
let app_handle_clone = tauri_app_handle.clone();
|
||||
|
||||
if event.state() == ShortcutState::Pressed {
|
||||
async_runtime::spawn(async move {
|
||||
let result = open(on_opened_clone).await;
|
||||
let result = open(app_handle_clone, on_opened_clone).await;
|
||||
if let Err(msg) = result {
|
||||
log::warn!(
|
||||
"failed to open extension [{:?}], error [{}]",
|
||||
@@ -690,6 +701,7 @@ impl ThirdPartyExtensionsSearchSource {
|
||||
#[named]
|
||||
pub(super) async fn unregister_extension_hotkey(
|
||||
&self,
|
||||
tauri_app_handle: &AppHandle,
|
||||
bundle_id: &ExtensionBundleIdBorrowed<'_>,
|
||||
) -> Result<(), String> {
|
||||
let mut extensions_write_lock = self.inner.extensions.write().await;
|
||||
@@ -717,15 +729,12 @@ impl ThirdPartyExtensionsSearchSource {
|
||||
|
||||
update_extension(extension)?;
|
||||
alter_extension_json_file(
|
||||
&THIRD_PARTY_EXTENSIONS_DIRECTORY,
|
||||
&get_third_party_extension_directory(tauri_app_handle),
|
||||
bundle_id,
|
||||
update_extension,
|
||||
)?;
|
||||
|
||||
// Set hotkey
|
||||
let tauri_app_handle = GLOBAL_TAURI_APP_HANDLE
|
||||
.get()
|
||||
.expect("global tauri app handle not set");
|
||||
tauri_app_handle
|
||||
.global_shortcut()
|
||||
.unregister(hotkey.as_str())
|
||||
@@ -846,7 +855,11 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
|
||||
}
|
||||
}
|
||||
|
||||
async fn search(&self, query: SearchQuery) -> Result<QueryResponse, SearchError> {
|
||||
async fn search(
|
||||
&self,
|
||||
_tauri_app_handle: AppHandle,
|
||||
query: SearchQuery,
|
||||
) -> Result<QueryResponse, SearchError> {
|
||||
let Some(query_string) = query.query_strings.get("query") else {
|
||||
return Ok(QueryResponse {
|
||||
source: self.get_type(),
|
||||
@@ -1045,11 +1058,13 @@ fn calculate_text_similarity(query: &str, text: &str) -> Option<f64> {
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn uninstall_extension(
|
||||
tauri_app_handle: AppHandle,
|
||||
developer: String,
|
||||
extension_id: String,
|
||||
) -> Result<(), String> {
|
||||
let extension_dir = {
|
||||
let mut path = THIRD_PARTY_EXTENSIONS_DIRECTORY.join(developer.as_str());
|
||||
let mut path = get_third_party_extension_directory(&tauri_app_handle);
|
||||
path.push(developer.as_str());
|
||||
path.push(extension_id.as_str());
|
||||
|
||||
path
|
||||
@@ -1075,7 +1090,7 @@ pub(crate) async fn uninstall_extension(
|
||||
// Unregistering hotkey is the only thing that we will do when we disable
|
||||
// an extension, so we directly use this function here even though "disabling"
|
||||
// the extension that one is trying to uninstall does not make too much sense.
|
||||
ThirdPartyExtensionsSearchSource::_disable_extension(&extension).await?;
|
||||
ThirdPartyExtensionsSearchSource::_disable_extension(&tauri_app_handle, &extension).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
51
src-tauri/src/extension/third_party/store.rs
vendored
51
src-tauri/src/extension/third_party/store.rs
vendored
@@ -8,17 +8,18 @@ use crate::common::search::QueryResponse;
|
||||
use crate::common::search::QuerySource;
|
||||
use crate::common::search::SearchQuery;
|
||||
use crate::common::traits::SearchSource;
|
||||
use crate::extension::canonicalize_relative_icon_path;
|
||||
use crate::extension::third_party::THIRD_PARTY_EXTENSIONS_DIRECTORY;
|
||||
use crate::extension::Extension;
|
||||
use crate::extension::PLUGIN_JSON_FILE_NAME;
|
||||
use crate::extension::THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE;
|
||||
use crate::extension::canonicalize_relative_icon_path;
|
||||
use crate::extension::third_party::get_third_party_extension_directory;
|
||||
use crate::server::http_client::HttpClient;
|
||||
use async_trait::async_trait;
|
||||
use reqwest::StatusCode;
|
||||
use serde_json::Map as JsonObject;
|
||||
use serde_json::Value as Json;
|
||||
use std::io::Read;
|
||||
use tauri::AppHandle;
|
||||
|
||||
const DATA_SOURCE_ID: &str = "Extension Store";
|
||||
|
||||
@@ -37,7 +38,11 @@ impl SearchSource for ExtensionStore {
|
||||
}
|
||||
}
|
||||
|
||||
async fn search(&self, query: SearchQuery) -> Result<QueryResponse, SearchError> {
|
||||
async fn search(
|
||||
&self,
|
||||
_tauri_app_handle: AppHandle,
|
||||
query: SearchQuery,
|
||||
) -> Result<QueryResponse, SearchError> {
|
||||
const SCORE: f64 = 2000.0;
|
||||
|
||||
let Some(query_string) = query.query_strings.get("query") else {
|
||||
@@ -174,7 +179,10 @@ async fn is_extension_installed(developer: String, extension_id: String) -> bool
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn install_extension_from_store(id: String) -> Result<(), String> {
|
||||
pub(crate) async fn install_extension_from_store(
|
||||
tauri_app_handle: AppHandle,
|
||||
id: String,
|
||||
) -> Result<(), String> {
|
||||
let path = format!("store/extension/{}/_download", id);
|
||||
let response = HttpClient::get("default_coco_server", &path, None)
|
||||
.await
|
||||
@@ -199,7 +207,9 @@ pub(crate) async fn install_extension_from_store(id: String) -> Result<(), Strin
|
||||
// 2. sub-extensions won't have their `id` fields set
|
||||
//
|
||||
// we need to correct it
|
||||
let mut plugin_json = archive.by_name(PLUGIN_JSON_FILE_NAME).map_err(|e| e.to_string())?;
|
||||
let mut plugin_json = archive
|
||||
.by_name(PLUGIN_JSON_FILE_NAME)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut plugin_json_content = String::new();
|
||||
std::io::Read::read_to_string(&mut plugin_json, &mut plugin_json_content)
|
||||
.map_err(|e| e.to_string())?;
|
||||
@@ -249,12 +259,11 @@ pub(crate) async fn install_extension_from_store(id: String) -> Result<(), Strin
|
||||
|
||||
drop(plugin_json);
|
||||
|
||||
|
||||
// Write extension files to the extension directory
|
||||
let developer = extension.developer.clone().unwrap_or_default();
|
||||
let extension_id = extension.id.clone();
|
||||
let extension_directory = {
|
||||
let mut path = THIRD_PARTY_EXTENSIONS_DIRECTORY.to_path_buf();
|
||||
let mut path = get_third_party_extension_directory(&tauri_app_handle);
|
||||
path.push(developer);
|
||||
path.push(extension_id.as_str());
|
||||
path
|
||||
@@ -288,14 +297,29 @@ pub(crate) async fn install_extension_from_store(id: String) -> Result<(), Strin
|
||||
let dest_file_path = extension_directory.join(zip_file_name);
|
||||
|
||||
// For cases like `assets/xxx.png`
|
||||
if let Some(parent_dir) = dest_file_path.parent() && !parent_dir.exists() {
|
||||
tokio::fs::create_dir_all(parent_dir).await.map_err(|e| e.to_string())?;
|
||||
if let Some(parent_dir) = dest_file_path.parent()
|
||||
&& !parent_dir.exists()
|
||||
{
|
||||
tokio::fs::create_dir_all(parent_dir)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
let mut dest_file = tokio::fs::File::create(&dest_file_path) .await .map_err(|e| e.to_string())?;
|
||||
let mut src_bytes = Vec::with_capacity(zip_file.size().try_into().expect("we won't have a extension file that is bigger than 4GiB"));
|
||||
zip_file.read_to_end(&mut src_bytes).map_err(|e| e.to_string())?;
|
||||
tokio::io::copy(&mut src_bytes.as_slice(), &mut dest_file).await.map_err(|e| e.to_string())?;
|
||||
let mut dest_file = tokio::fs::File::create(&dest_file_path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut src_bytes = Vec::with_capacity(
|
||||
zip_file
|
||||
.size()
|
||||
.try_into()
|
||||
.expect("we won't have a extension file that is bigger than 4GiB"),
|
||||
);
|
||||
zip_file
|
||||
.read_to_end(&mut src_bytes)
|
||||
.map_err(|e| e.to_string())?;
|
||||
tokio::io::copy(&mut src_bytes.as_slice(), &mut dest_file)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
// Create plugin.json from the extension variable
|
||||
let plugin_json_path = extension_directory.join(PLUGIN_JSON_FILE_NAME);
|
||||
@@ -304,7 +328,6 @@ pub(crate) async fn install_extension_from_store(id: String) -> Result<(), Strin
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
|
||||
// Turn it into an absolute path if it is a valid relative path because frontend code need this.
|
||||
canonicalize_relative_icon_path(&extension_directory, &mut extension)?;
|
||||
|
||||
|
||||
@@ -28,9 +28,14 @@ pub(crate) const COCO_TAURI_STORE: &str = "coco_tauri_store";
|
||||
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()`.
|
||||
///
|
||||
/// # WARNING
|
||||
///
|
||||
/// You may find this work, but the usage is discouraged and should be generally
|
||||
/// avoided. If you do need it, always be careful that it may not be set() when
|
||||
/// you access it.
|
||||
pub(crate) static GLOBAL_TAURI_APP_HANDLE: OnceLock<AppHandle> = OnceLock::new();
|
||||
|
||||
#[tauri::command]
|
||||
@@ -85,7 +90,11 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_macos_permissions::init())
|
||||
.plugin(tauri_plugin_screenshots::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(
|
||||
tauri_plugin_updater::Builder::new()
|
||||
.default_version_comparator(crate::util::updater::custom_version_comparator)
|
||||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_windows_version::init())
|
||||
.plugin(tauri_plugin_opener::init());
|
||||
|
||||
@@ -107,7 +116,6 @@ pub fn run() {
|
||||
show_settings,
|
||||
show_check,
|
||||
hide_check,
|
||||
server::servers::get_server_token,
|
||||
server::servers::add_coco_server,
|
||||
server::servers::remove_coco_server,
|
||||
server::servers::list_coco_servers,
|
||||
@@ -172,8 +180,16 @@ pub fn run() {
|
||||
server::synthesize::synthesize,
|
||||
util::file::get_file_icon,
|
||||
util::app_lang::update_app_lang,
|
||||
#[cfg(target_os = "macos")]
|
||||
setup::toggle_move_to_active_space_attribute,
|
||||
])
|
||||
.setup(|app| {
|
||||
let app_handle = app.handle().clone();
|
||||
GLOBAL_TAURI_APP_HANDLE
|
||||
.set(app_handle.clone())
|
||||
.expect("global tauri AppHandle already initialized");
|
||||
log::trace!("global Tauri AppHandle set");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
log::trace!("hiding Dock icon on macOS");
|
||||
@@ -181,22 +197,35 @@ pub fn run() {
|
||||
log::trace!("Dock icon should be hidden now");
|
||||
}
|
||||
|
||||
let app_handle = app.handle().clone();
|
||||
GLOBAL_TAURI_APP_HANDLE
|
||||
.set(app_handle.clone())
|
||||
.expect("variable already initialized");
|
||||
log::trace!("global Tauri app handle set");
|
||||
|
||||
let registry = SearchSourceRegistry::default();
|
||||
|
||||
app.manage(registry); // Store registry in Tauri's app state
|
||||
app.manage(server::websocket::WebSocketManager::default());
|
||||
|
||||
// This has to be called before initializing extensions as doing that
|
||||
// requires access to the shortcut store, which will be set by this
|
||||
// function.
|
||||
shortcut::enable_shortcut(app);
|
||||
|
||||
block_on(async {
|
||||
init(app.handle()).await;
|
||||
});
|
||||
|
||||
shortcut::enable_shortcut(app);
|
||||
// We want all the extensions here, so no filter condition specified.
|
||||
match extension::list_extensions(app_handle.clone(), None, None, false).await {
|
||||
Ok((_found_invalid_extensions, extensions)) => {
|
||||
// Initializing extension relies on SearchSourceRegistry, so this should
|
||||
// be executed after `app.manage(registry)`
|
||||
if let Err(e) =
|
||||
extension::init_extensions(app_handle.clone(), extensions).await
|
||||
{
|
||||
log::error!("initializing extensions failed with error [{}]", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("listing extensions failed with error [{}]", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ensure_autostart_state_consistent(app)?;
|
||||
|
||||
@@ -274,7 +303,7 @@ pub async fn init<R: Runtime>(app_handle: &AppHandle<R>) {
|
||||
log::error!("Failed to load server tokens: {}", err);
|
||||
}
|
||||
|
||||
let coco_servers = server::servers::get_all_servers();
|
||||
let coco_servers = server::servers::get_all_servers().await;
|
||||
|
||||
// Get the registry from Tauri's state
|
||||
// let registry: State<SearchSourceRegistry> = app_handle.state::<SearchSourceRegistry>();
|
||||
@@ -407,13 +436,7 @@ fn move_window_to_active_monitor<R: Runtime>(window: &WebviewWindow<R>) {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_app_search_source<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
|
||||
// We want all the extensions here, so no filter condition specified.
|
||||
let (_found_invalid_extensions, extensions) = extension::list_extensions(None, None, false)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
extension::init_extensions(extensions).await?;
|
||||
|
||||
async fn get_app_search_source(app_handle: AppHandle) -> Result<(), String> {
|
||||
let _ = server::connector::refresh_all_connectors(&app_handle).await;
|
||||
let _ = server::datasource::refresh_all_datasources(&app_handle).await;
|
||||
|
||||
|
||||
@@ -1,5 +1,112 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Helper function to return the log directory.
|
||||
///
|
||||
/// This should return the same value as `tauri_app_handle.path().app_log_dir().unwrap()`.
|
||||
fn app_log_dir() -> PathBuf {
|
||||
// This function `app_log_dir()` is for the panic hook, which should be set
|
||||
// before Tauri performs any initialization. At that point, we do not have
|
||||
// access to the identifier provided by Tauri, so we need to define our own
|
||||
// one here.
|
||||
//
|
||||
// NOTE: If you update identifier in the following files, update this one
|
||||
// as well!
|
||||
//
|
||||
// src-tauri/tauri.linux.conf.json
|
||||
// src-tauri/Entitlements.plist
|
||||
// src-tauri/tauri.conf.json
|
||||
// src-tauri/Info.plist
|
||||
const IDENTIFIER: &str = "rs.coco.app";
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let path = dirs::home_dir()
|
||||
.expect("cannot find the home directory, Coco should never run in such a environment")
|
||||
.join("Library/Logs")
|
||||
.join(IDENTIFIER);
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let path = dirs::data_local_dir()
|
||||
.expect("app local dir is None, we should not encounter this")
|
||||
.join(IDENTIFIER)
|
||||
.join("logs");
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
/// Set up panic hook to log panic information to a file
|
||||
fn setup_panic_hook() {
|
||||
std::panic::set_hook(Box::new(|panic_info| {
|
||||
let timestamp = chrono::Local::now();
|
||||
// "%Y-%m-%d %H:%M:%S"
|
||||
//
|
||||
// I would like to use the above format, but Windows does not allow that
|
||||
// and complains with OS error 123.
|
||||
let datetime_str = timestamp.format("%Y-%m-%d-%H-%M-%S").to_string();
|
||||
|
||||
let log_dir = app_log_dir();
|
||||
|
||||
// Ensure the log directory exists
|
||||
if let Err(e) = std::fs::create_dir_all(&log_dir) {
|
||||
eprintln!("Panic hook error: failed to create log directory: {}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
let panic_file = log_dir.join(format!("{}_rust_panic.log", datetime_str));
|
||||
|
||||
// Prepare panic information
|
||||
let panic_message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
|
||||
s.to_string()
|
||||
} else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
|
||||
s.clone()
|
||||
} else {
|
||||
"Unknown panic message".to_string()
|
||||
};
|
||||
|
||||
let location = if let Some(location) = panic_info.location() {
|
||||
format!(
|
||||
"{}:{}:{}",
|
||||
location.file(),
|
||||
location.line(),
|
||||
location.column()
|
||||
)
|
||||
} else {
|
||||
"Unknown location".to_string()
|
||||
};
|
||||
|
||||
// Use `force_capture()` instead of `capture()` as we want backtrace
|
||||
// regardless of whether the corresponding env vars are set or not.
|
||||
let backtrace = std::backtrace::Backtrace::force_capture();
|
||||
|
||||
let panic_log = format!(
|
||||
"Time: [{}]\nLocation: [{}]\nMessage: [{}]\nBacktrace: \n{}",
|
||||
datetime_str, location, panic_message, backtrace
|
||||
);
|
||||
|
||||
// Write to panic file
|
||||
match OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&panic_file)
|
||||
{
|
||||
Ok(mut file) => {
|
||||
if let Err(e) = writeln!(file, "{}", panic_log) {
|
||||
eprintln!("Panic hook error: Failed to write panic to file: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Panic hook error: Failed to open panic log file: {}", e);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Panic hook setup should be the first thing to do, everything could panic!
|
||||
setup_panic_hook();
|
||||
coco_lib::run();
|
||||
}
|
||||
|
||||
@@ -4,17 +4,20 @@ use crate::common::search::{
|
||||
FailedRequest, MultiSourceQueryResponse, QueryHits, QueryResponse, QuerySource, SearchQuery,
|
||||
};
|
||||
use crate::common::traits::SearchSource;
|
||||
use crate::server::servers::logout_coco_server;
|
||||
use crate::server::servers::mark_server_as_offline;
|
||||
use function_name::named;
|
||||
use futures::stream::FuturesUnordered;
|
||||
use futures::StreamExt;
|
||||
use futures::stream::FuturesUnordered;
|
||||
use reqwest::StatusCode;
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tokio::time::error::Elapsed;
|
||||
use tokio::time::{timeout, Duration};
|
||||
use tokio::time::{Duration, timeout};
|
||||
|
||||
/// Helper function to return the Future used for querying querysources.
|
||||
///
|
||||
@@ -31,6 +34,7 @@ fn same_type_futures(
|
||||
query_source_trait_object: Arc<dyn SearchSource>,
|
||||
timeout_duration: Duration,
|
||||
search_query: SearchQuery,
|
||||
tauri_app_handle: AppHandle,
|
||||
) -> impl Future<
|
||||
Output = (
|
||||
QuerySource,
|
||||
@@ -42,7 +46,9 @@ fn same_type_futures(
|
||||
// Store `query_source` as part of future for debugging purposes.
|
||||
query_source,
|
||||
timeout(timeout_duration, async {
|
||||
query_source_trait_object.search(search_query).await
|
||||
query_source_trait_object
|
||||
.search(tauri_app_handle.clone(), search_query)
|
||||
.await
|
||||
})
|
||||
.await,
|
||||
)
|
||||
@@ -51,8 +57,8 @@ fn same_type_futures(
|
||||
|
||||
#[named]
|
||||
#[tauri::command]
|
||||
pub async fn query_coco_fusion<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
pub async fn query_coco_fusion(
|
||||
app_handle: AppHandle,
|
||||
from: u64,
|
||||
size: u64,
|
||||
query_strings: HashMap<String, String>,
|
||||
@@ -77,8 +83,10 @@ pub async fn query_coco_fusion<R: Runtime>(
|
||||
let timeout_duration = Duration::from_millis(query_timeout);
|
||||
|
||||
log::debug!(
|
||||
"{}(): {:?}, timeout: {:?}",
|
||||
"{}() invoked with parameters: from: [{}], size: [{}], query_strings: [{:?}], timeout: [{:?}]",
|
||||
function_name!(),
|
||||
from,
|
||||
size,
|
||||
query_strings,
|
||||
timeout_duration
|
||||
);
|
||||
@@ -124,16 +132,25 @@ pub async fn query_coco_fusion<R: Runtime>(
|
||||
query_source_trait_object,
|
||||
timeout_duration,
|
||||
search_query,
|
||||
app_handle.clone(),
|
||||
));
|
||||
} else {
|
||||
log::debug!(
|
||||
"will query querysources {:?}",
|
||||
sources_list
|
||||
.iter()
|
||||
.map(|search_source| search_source.get_type().id.clone())
|
||||
.collect::<Vec<String>>()
|
||||
);
|
||||
|
||||
for query_source_trait_object in sources_list {
|
||||
let query_source = query_source_trait_object.get_type().clone();
|
||||
log::debug!("will query querysource [{}]", query_source.id);
|
||||
futures.push(same_type_futures(
|
||||
query_source,
|
||||
query_source_trait_object,
|
||||
timeout_duration,
|
||||
search_query.clone(),
|
||||
app_handle.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -191,9 +208,38 @@ pub async fn query_coco_fusion<R: Runtime>(
|
||||
query_source.id,
|
||||
search_error
|
||||
);
|
||||
|
||||
let mut status_code_num: u16 = 0;
|
||||
|
||||
if let SearchError::HttpError {
|
||||
status_code: opt_status_code,
|
||||
msg: _,
|
||||
} = search_error
|
||||
{
|
||||
if let Some(status_code) = opt_status_code {
|
||||
status_code_num = status_code.as_u16();
|
||||
if status_code != StatusCode::OK {
|
||||
if status_code == StatusCode::UNAUTHORIZED {
|
||||
// This Coco server is unavailable. In addition to marking it as
|
||||
// unavailable, we need to log out because the status code is 401.
|
||||
logout_coco_server(app_handle.clone(), query_source.id.clone()).await.unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"the search request to Coco server [id {}, name {}] failed with status code {}, the login token is invalid, we are trying to log out, but failed with error [{}]",
|
||||
query_source.id, query_source.name, StatusCode::UNAUTHORIZED, e
|
||||
);
|
||||
})
|
||||
} else {
|
||||
// This Coco server is unavailable
|
||||
mark_server_as_offline(app_handle.clone(), &query_source.id)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
failed_requests.push(FailedRequest {
|
||||
source: query_source,
|
||||
status: 0,
|
||||
status: status_code_num,
|
||||
error: Some(search_error.to_string()),
|
||||
reason: None,
|
||||
});
|
||||
|
||||
@@ -45,10 +45,12 @@ pub async fn upload_attachment(
|
||||
form = form.part("files", part);
|
||||
}
|
||||
|
||||
let server = get_server_by_id(&server_id).ok_or("Server not found")?;
|
||||
let server = get_server_by_id(&server_id)
|
||||
.await
|
||||
.ok_or("Server not found")?;
|
||||
let url = HttpClient::join_url(&server.endpoint, &format!("attachment/_upload"));
|
||||
|
||||
let token = get_server_token(&server_id).await?;
|
||||
let token = get_server_token(&server_id).await;
|
||||
let mut headers = HashMap::new();
|
||||
if let Some(token) = token {
|
||||
headers.insert("X-API-TOKEN".to_string(), token.access_token);
|
||||
|
||||
@@ -20,15 +20,15 @@ pub async fn handle_sso_callback<R: Runtime>(
|
||||
code: String,
|
||||
) -> Result<(), String> {
|
||||
// Retrieve the server details using the server ID
|
||||
let server = get_server_by_id(&server_id);
|
||||
let server = get_server_by_id(&server_id).await;
|
||||
|
||||
let expire_in = 3600; // TODO, need to update to actual expire_in value
|
||||
if let Some(mut server) = server {
|
||||
// Save the access token for the server
|
||||
let access_token = ServerAccessToken::new(server_id.clone(), code.clone(), expire_in);
|
||||
// dbg!(&server_id, &request_id, &code, &token);
|
||||
save_access_token(server_id.clone(), access_token);
|
||||
persist_servers_token(&app_handle)?;
|
||||
save_access_token(server_id.clone(), access_token).await;
|
||||
persist_servers_token(&app_handle).await?;
|
||||
|
||||
// Register the server to the search source
|
||||
try_register_server_to_search_source(app_handle.clone(), &server).await;
|
||||
@@ -41,7 +41,7 @@ pub async fn handle_sso_callback<R: Runtime>(
|
||||
Ok(p) => {
|
||||
server.profile = Some(p);
|
||||
server.available = true;
|
||||
save_server(&server);
|
||||
save_server(&server).await;
|
||||
persist_servers(&app_handle).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ pub fn get_connector_by_id(server_id: &str, connector_id: &str) -> Option<Connec
|
||||
}
|
||||
|
||||
pub async fn refresh_all_connectors<R: Runtime>(app_handle: &AppHandle<R>) -> Result<(), String> {
|
||||
let servers = get_all_servers();
|
||||
let servers = get_all_servers().await;
|
||||
|
||||
// Collect all the tasks for fetching and refreshing connectors
|
||||
let mut server_map = HashMap::new();
|
||||
|
||||
@@ -34,7 +34,7 @@ pub fn get_datasources_from_cache(server_id: &str) -> Option<HashMap<String, Dat
|
||||
pub async fn refresh_all_datasources<R: Runtime>(_app_handle: &AppHandle<R>) -> Result<(), String> {
|
||||
// dbg!("Attempting to refresh all datasources");
|
||||
|
||||
let servers = get_all_servers();
|
||||
let servers = get_all_servers().await;
|
||||
|
||||
let mut server_map = HashMap::new();
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ use tokio::sync::Mutex;
|
||||
|
||||
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
|
||||
.read_timeout(Duration::from_secs(60)) // Set a timeout of 60 second
|
||||
.connect_timeout(Duration::from_secs(30)) // Set a timeout of 30 second
|
||||
.timeout(Duration::from_secs(5 * 60)) // Set a timeout of 5 minute
|
||||
.danger_accept_invalid_certs(accept_invalid_certs) // allow self-signed certificates
|
||||
.build()
|
||||
@@ -175,14 +175,14 @@ impl HttpClient {
|
||||
body: Option<reqwest::Body>,
|
||||
) -> Result<reqwest::Response, String> {
|
||||
// Fetch the server using the server_id
|
||||
let server = get_server_by_id(server_id);
|
||||
let server = get_server_by_id(server_id).await;
|
||||
if let Some(s) = server {
|
||||
// Construct the URL
|
||||
let url = HttpClient::join_url(&s.endpoint, path);
|
||||
|
||||
// Retrieve the token for the server (token is optional)
|
||||
let token = get_server_token(server_id)
|
||||
.await?
|
||||
.await
|
||||
.map(|t| t.access_token.clone());
|
||||
|
||||
let mut headers = if let Some(custom_headers) = custom_headers {
|
||||
@@ -205,7 +205,7 @@ impl HttpClient {
|
||||
|
||||
Self::send_raw_request(method, &url, query_params, Some(headers), body).await
|
||||
} else {
|
||||
Err("Server not found".to_string())
|
||||
Err(format!("Server [{}] not found", server_id))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@ use crate::common::server::Server;
|
||||
use crate::common::traits::SearchSource;
|
||||
use crate::server::http_client::HttpClient;
|
||||
use async_trait::async_trait;
|
||||
// use futures::stream::StreamExt;
|
||||
use ordered_float::OrderedFloat;
|
||||
use reqwest::StatusCode;
|
||||
use std::collections::HashMap;
|
||||
// use std::hash::Hash;
|
||||
use tauri::AppHandle;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct DocumentsSizedCollector {
|
||||
@@ -44,7 +44,7 @@ impl DocumentsSizedCollector {
|
||||
}
|
||||
}
|
||||
|
||||
fn documents(self) -> impl ExactSizeIterator<Item=Document> {
|
||||
fn documents(self) -> impl ExactSizeIterator<Item = Document> {
|
||||
self.docs.into_iter().map(|(_, doc, _)| doc)
|
||||
}
|
||||
|
||||
@@ -90,7 +90,11 @@ impl SearchSource for CocoSearchSource {
|
||||
}
|
||||
}
|
||||
|
||||
async fn search(&self, query: SearchQuery) -> Result<QueryResponse, SearchError> {
|
||||
async fn search(
|
||||
&self,
|
||||
_tauri_app_handle: AppHandle,
|
||||
query: SearchQuery,
|
||||
) -> Result<QueryResponse, SearchError> {
|
||||
let url = "/query/_search";
|
||||
let mut total_hits = 0;
|
||||
let mut hits: Vec<(Document, f64)> = Vec::new();
|
||||
@@ -108,7 +112,18 @@ impl SearchSource for CocoSearchSource {
|
||||
|
||||
let response = HttpClient::get(&self.server.id, &url, Some(query_params))
|
||||
.await
|
||||
.map_err(|e| SearchError::HttpError(format!("{}", e)))?;
|
||||
.map_err(|e| SearchError::HttpError {
|
||||
status_code: None,
|
||||
msg: format!("{}", e),
|
||||
})?;
|
||||
let status_code = response.status();
|
||||
|
||||
if ![StatusCode::OK, StatusCode::CREATED].contains(&status_code) {
|
||||
return Err(SearchError::HttpError {
|
||||
status_code: Some(status_code),
|
||||
msg: format!("Request failed with status code [{}]", status_code),
|
||||
});
|
||||
}
|
||||
|
||||
// Use the helper function to parse the response body
|
||||
let response_body = get_response_body_text(response)
|
||||
@@ -123,7 +138,6 @@ impl SearchSource for CocoSearchSource {
|
||||
let parsed: SearchResponse<Document> = serde_json::from_str(&response_body)
|
||||
.map_err(|e| SearchError::ParseError(format!("{}", e)))?;
|
||||
|
||||
|
||||
// Process the parsed response
|
||||
total_hits = parsed.hits.total.value as usize;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::COCO_TAURI_STORE;
|
||||
use crate::common::http::get_response_body_text;
|
||||
use crate::common::register::SearchSourceRegistry;
|
||||
use crate::common::server::{AuthProvider, Provider, Server, ServerAccessToken, Sso, Version};
|
||||
@@ -5,68 +6,72 @@ use crate::server::connector::fetch_connectors_by_server;
|
||||
use crate::server::datasource::datasource_search;
|
||||
use crate::server::http_client::HttpClient;
|
||||
use crate::server::search::CocoSearchSource;
|
||||
use crate::COCO_TAURI_STORE;
|
||||
use lazy_static::lazy_static;
|
||||
use function_name;
|
||||
use http::StatusCode;
|
||||
use reqwest::Method;
|
||||
use serde_json::from_value;
|
||||
use serde_json::Value as JsonValue;
|
||||
use serde_json::from_value;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
use std::sync::LazyLock;
|
||||
use tauri::Runtime;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri_plugin_store::StoreExt;
|
||||
// Assuming you're using serde_json
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
lazy_static! {
|
||||
static ref SERVER_CACHE: Arc<RwLock<HashMap<String, Server>>> =
|
||||
Arc::new(RwLock::new(HashMap::new()));
|
||||
static ref SERVER_TOKEN: Arc<RwLock<HashMap<String, ServerAccessToken>>> =
|
||||
Arc::new(RwLock::new(HashMap::new()));
|
||||
}
|
||||
/// Coco sever list
|
||||
static SERVER_LIST_CACHE: LazyLock<RwLock<HashMap<String, Server>>> =
|
||||
LazyLock::new(|| RwLock::new(HashMap::new()));
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn check_server_exists(id: &str) -> bool {
|
||||
let cache = SERVER_CACHE.read().unwrap(); // Acquire read lock
|
||||
cache.contains_key(id)
|
||||
}
|
||||
/// If a server has a token stored here that has not expired, it is considered logged in.
|
||||
///
|
||||
/// Since the `expire_at` field of `struct ServerAccessToken` is currently unused,
|
||||
/// all servers stored here are treated as logged in.
|
||||
static SERVER_TOKEN_LIST_CACHE: LazyLock<RwLock<HashMap<String, ServerAccessToken>>> =
|
||||
LazyLock::new(|| RwLock::new(HashMap::new()));
|
||||
|
||||
pub fn get_server_by_id(id: &str) -> Option<Server> {
|
||||
let cache = SERVER_CACHE.read().unwrap(); // Acquire read lock
|
||||
/// `SERVER_LIST_CACHE` will be stored in KV store COCO_TAURI_STORE, under this key.
|
||||
pub const COCO_SERVERS: &str = "coco_servers";
|
||||
|
||||
/// `SERVER_TOKEN_LIST_CACHE` will be stored in KV store COCO_TAURI_STORE, under this key.
|
||||
const COCO_SERVER_TOKENS: &str = "coco_server_tokens";
|
||||
|
||||
pub async fn get_server_by_id(id: &str) -> Option<Server> {
|
||||
let cache = SERVER_LIST_CACHE.read().await;
|
||||
cache.get(id).cloned()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_server_token(id: &str) -> Result<Option<ServerAccessToken>, String> {
|
||||
let cache = SERVER_TOKEN.read().map_err(|err| err.to_string())?;
|
||||
pub async fn get_server_token(id: &str) -> Option<ServerAccessToken> {
|
||||
let cache = SERVER_TOKEN_LIST_CACHE.read().await;
|
||||
|
||||
Ok(cache.get(id).cloned())
|
||||
cache.get(id).cloned()
|
||||
}
|
||||
|
||||
pub fn save_access_token(server_id: String, token: ServerAccessToken) -> bool {
|
||||
let mut cache = SERVER_TOKEN.write().unwrap();
|
||||
pub async fn save_access_token(server_id: String, token: ServerAccessToken) -> bool {
|
||||
let mut cache = SERVER_TOKEN_LIST_CACHE.write().await;
|
||||
cache.insert(server_id, token).is_none()
|
||||
}
|
||||
|
||||
fn check_endpoint_exists(endpoint: &str) -> bool {
|
||||
let cache = SERVER_CACHE.read().unwrap();
|
||||
async fn check_endpoint_exists(endpoint: &str) -> bool {
|
||||
let cache = SERVER_LIST_CACHE.read().await;
|
||||
cache.values().any(|server| server.endpoint == endpoint)
|
||||
}
|
||||
|
||||
pub fn save_server(server: &Server) -> bool {
|
||||
let mut cache = SERVER_CACHE.write().unwrap();
|
||||
cache.insert(server.id.clone(), server.clone()).is_none() // If the server id did not exist, `insert` will return `None`
|
||||
/// Return true if `server` does not exists in the server list, i.e., it is a newly-added
|
||||
/// server.
|
||||
pub async fn save_server(server: &Server) -> bool {
|
||||
let mut cache = SERVER_LIST_CACHE.write().await;
|
||||
cache.insert(server.id.clone(), server.clone()).is_none()
|
||||
}
|
||||
|
||||
fn remove_server_by_id(id: String) -> bool {
|
||||
/// Return the removed `Server` if it exists in the server list.
|
||||
async fn remove_server_by_id(id: &str) -> Option<Server> {
|
||||
log::debug!("remove server by id: {}", &id);
|
||||
let mut cache = SERVER_CACHE.write().unwrap();
|
||||
let deleted = cache.remove(id.as_str());
|
||||
deleted.is_some()
|
||||
let mut cache = SERVER_LIST_CACHE.write().await;
|
||||
cache.remove(id)
|
||||
}
|
||||
|
||||
pub async fn persist_servers<R: Runtime>(app_handle: &AppHandle<R>) -> Result<(), String> {
|
||||
let cache = SERVER_CACHE.read().unwrap(); // Acquire a read lock, not a write lock, since you're not modifying the cache
|
||||
let cache = SERVER_LIST_CACHE.read().await;
|
||||
|
||||
// Convert HashMap to Vec for serialization (iterating over values of HashMap)
|
||||
let servers: Vec<Server> = cache.values().cloned().collect();
|
||||
@@ -86,14 +91,16 @@ pub async fn persist_servers<R: Runtime>(app_handle: &AppHandle<R>) -> Result<()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_server_token(id: &str) -> bool {
|
||||
/// Return true if the server token of the server specified by `id` exists in
|
||||
/// the token list and gets deleted.
|
||||
pub async fn remove_server_token(id: &str) -> bool {
|
||||
log::debug!("remove server token by id: {}", &id);
|
||||
let mut cache = SERVER_TOKEN.write().unwrap();
|
||||
let mut cache = SERVER_TOKEN_LIST_CACHE.write().await;
|
||||
cache.remove(id).is_some()
|
||||
}
|
||||
|
||||
pub fn persist_servers_token<R: Runtime>(app_handle: &AppHandle<R>) -> Result<(), String> {
|
||||
let cache = SERVER_TOKEN.read().unwrap(); // Acquire a read lock, not a write lock, since you're not modifying the cache
|
||||
pub async fn persist_servers_token<R: Runtime>(app_handle: &AppHandle<R>) -> Result<(), String> {
|
||||
let cache = SERVER_TOKEN_LIST_CACHE.read().await;
|
||||
|
||||
// Convert HashMap to Vec for serialization (iterating over values of HashMap)
|
||||
let servers: Vec<ServerAccessToken> = cache.values().cloned().collect();
|
||||
@@ -173,26 +180,42 @@ pub async fn load_servers_token<R: Runtime>(
|
||||
servers.ok_or_else(|| "Failed to read servers from store: No servers found".to_string())?;
|
||||
|
||||
// Convert each item in the JsonValue array to a Server
|
||||
if let JsonValue::Array(servers_array) = servers {
|
||||
// Deserialize each JsonValue into Server, filtering out any errors
|
||||
let deserialized_tokens: Vec<ServerAccessToken> = servers_array
|
||||
.into_iter()
|
||||
.filter_map(|server_json| from_value(server_json).ok()) // Only keep valid Server instances
|
||||
.collect();
|
||||
match servers {
|
||||
JsonValue::Array(servers_array) => {
|
||||
let mut deserialized_tokens: Vec<ServerAccessToken> =
|
||||
Vec::with_capacity(servers_array.len());
|
||||
for server_json in servers_array {
|
||||
match from_value(server_json.clone()) {
|
||||
Ok(token) => {
|
||||
deserialized_tokens.push(token);
|
||||
}
|
||||
Err(e) => {
|
||||
panic!(
|
||||
"failed to deserialize JSON [{}] to [struct ServerAccessToken], error [{}], store [{}] key [{}] is possibly corrupted!",
|
||||
server_json, e, COCO_TAURI_STORE, COCO_SERVER_TOKENS
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if deserialized_tokens.is_empty() {
|
||||
return Err("Failed to deserialize any servers from the store.".to_string());
|
||||
}
|
||||
|
||||
for server in deserialized_tokens.iter() {
|
||||
save_access_token(server.id.clone(), server.clone());
|
||||
save_access_token(server.id.clone(), server.clone()).await;
|
||||
}
|
||||
|
||||
log::debug!("loaded {:?} servers's token", &deserialized_tokens.len());
|
||||
|
||||
Ok(deserialized_tokens)
|
||||
} else {
|
||||
Err("Failed to read servers from store: Invalid format".to_string())
|
||||
}
|
||||
_ => {
|
||||
unreachable!(
|
||||
"coco server tokens should be stored in an array under store [{}] key [{}], but it is not",
|
||||
COCO_TAURI_STORE, COCO_SERVER_TOKENS
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,26 +237,41 @@ pub async fn load_servers<R: Runtime>(app_handle: &AppHandle<R>) -> Result<Vec<S
|
||||
servers.ok_or_else(|| "Failed to read servers from store: No servers found".to_string())?;
|
||||
|
||||
// Convert each item in the JsonValue array to a Server
|
||||
if let JsonValue::Array(servers_array) = servers {
|
||||
// Deserialize each JsonValue into Server, filtering out any errors
|
||||
let deserialized_servers: Vec<Server> = servers_array
|
||||
.into_iter()
|
||||
.filter_map(|server_json| from_value(server_json).ok()) // Only keep valid Server instances
|
||||
.collect();
|
||||
match servers {
|
||||
JsonValue::Array(servers_array) => {
|
||||
let mut deserialized_servers = Vec::with_capacity(servers_array.len());
|
||||
for server_json in servers_array {
|
||||
match from_value(server_json.clone()) {
|
||||
Ok(server) => {
|
||||
deserialized_servers.push(server);
|
||||
}
|
||||
Err(e) => {
|
||||
panic!(
|
||||
"failed to deserialize JSON [{}] to [struct Server], error [{}], store [{}] key [{}] is possibly corrupted!",
|
||||
server_json, e, COCO_TAURI_STORE, COCO_SERVERS
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if deserialized_servers.is_empty() {
|
||||
return Err("Failed to deserialize any servers from the store.".to_string());
|
||||
}
|
||||
|
||||
for server in deserialized_servers.iter() {
|
||||
save_server(&server);
|
||||
save_server(&server).await;
|
||||
}
|
||||
|
||||
log::debug!("load servers: {:?}", &deserialized_servers);
|
||||
|
||||
Ok(deserialized_servers)
|
||||
} else {
|
||||
Err("Failed to read servers from store: Invalid format".to_string())
|
||||
}
|
||||
_ => {
|
||||
unreachable!(
|
||||
"coco servers should be stored in an array under store [{}] key [{}], but it is not",
|
||||
COCO_TAURI_STORE, COCO_SERVERS
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,7 +288,7 @@ pub async fn load_or_insert_default_server<R: Runtime>(
|
||||
}
|
||||
|
||||
let default = get_default_server();
|
||||
save_server(&default);
|
||||
save_server(&default).await;
|
||||
|
||||
log::debug!("loaded default servers");
|
||||
|
||||
@@ -259,33 +297,23 @@ pub async fn load_or_insert_default_server<R: Runtime>(
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_coco_servers<R: Runtime>(
|
||||
_app_handle: AppHandle<R>,
|
||||
app_handle: AppHandle<R>,
|
||||
) -> Result<Vec<Server>, String> {
|
||||
//hard fresh all server's info, in order to get the actual health
|
||||
refresh_all_coco_server_info(_app_handle.clone()).await;
|
||||
refresh_all_coco_server_info(app_handle.clone()).await;
|
||||
|
||||
let servers: Vec<Server> = get_all_servers().await;
|
||||
|
||||
let servers: Vec<Server> = get_all_servers();
|
||||
Ok(servers)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_servers_as_hashmap() -> HashMap<String, Server> {
|
||||
let cache = SERVER_CACHE.read().unwrap();
|
||||
cache.clone()
|
||||
}
|
||||
|
||||
pub fn get_all_servers() -> Vec<Server> {
|
||||
let cache = SERVER_CACHE.read().unwrap();
|
||||
pub async fn get_all_servers() -> Vec<Server> {
|
||||
let cache = SERVER_LIST_CACHE.read().await;
|
||||
cache.values().cloned().collect()
|
||||
}
|
||||
|
||||
/// We store added Coco servers in the Tauri store using this key.
|
||||
pub const COCO_SERVERS: &str = "coco_servers";
|
||||
|
||||
const COCO_SERVER_TOKENS: &str = "coco_server_tokens";
|
||||
|
||||
pub async fn refresh_all_coco_server_info<R: Runtime>(app_handle: AppHandle<R>) {
|
||||
let servers = get_all_servers();
|
||||
let servers = get_all_servers().await;
|
||||
for server in servers {
|
||||
let _ = refresh_coco_server_info(app_handle.clone(), server.id.clone()).await;
|
||||
}
|
||||
@@ -298,7 +326,7 @@ pub async fn refresh_coco_server_info<R: Runtime>(
|
||||
) -> Result<Server, String> {
|
||||
// Retrieve the server from the cache
|
||||
let cached_server = {
|
||||
let cache = SERVER_CACHE.read().unwrap();
|
||||
let cache = SERVER_LIST_CACHE.read().await;
|
||||
cache.get(&id).cloned()
|
||||
};
|
||||
|
||||
@@ -313,19 +341,16 @@ pub async fn refresh_coco_server_info<R: Runtime>(
|
||||
let profile = server.profile;
|
||||
|
||||
// Send request to fetch updated server info
|
||||
let response = HttpClient::get(&id, "/provider/_info", None)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to contact the server: {}", e));
|
||||
|
||||
if response.is_err() {
|
||||
let _ = mark_server_as_offline(app_handle, &id).await;
|
||||
return Err(response.err().unwrap());
|
||||
let response = match HttpClient::get(&id, "/provider/_info", None).await {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
mark_server_as_offline(app_handle, &id).await;
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
let response = response?;
|
||||
};
|
||||
|
||||
if !response.status().is_success() {
|
||||
let _ = mark_server_as_offline(app_handle, &id).await;
|
||||
mark_server_as_offline(app_handle, &id).await;
|
||||
return Err(format!("Request failed with status: {}", response.status()));
|
||||
}
|
||||
|
||||
@@ -336,19 +361,26 @@ pub async fn refresh_coco_server_info<R: Runtime>(
|
||||
let mut updated_server: Server = serde_json::from_str(&body)
|
||||
.map_err(|e| format!("Failed to deserialize the response: {}", e))?;
|
||||
|
||||
// Mark server as online
|
||||
let _ = mark_server_as_online(app_handle.clone(), &id).await;
|
||||
|
||||
// Restore local state
|
||||
updated_server.id = id.clone();
|
||||
updated_server.builtin = is_builtin;
|
||||
updated_server.enabled = is_enabled;
|
||||
updated_server.available = true;
|
||||
updated_server.available = {
|
||||
if server.public {
|
||||
// Public Coco servers are available as long as they are online.
|
||||
true
|
||||
} else {
|
||||
// For non-public Coco servers, we still need to check if it is
|
||||
// logged in, i.e., has a token stored in `SERVER_TOKEN_LIST_CACHE`.
|
||||
get_server_token(&id).await.is_some()
|
||||
}
|
||||
};
|
||||
updated_server.profile = profile;
|
||||
trim_endpoint_last_forward_slash(&mut updated_server);
|
||||
|
||||
// Save and persist
|
||||
save_server(&updated_server);
|
||||
save_server(&updated_server).await;
|
||||
try_register_server_to_search_source(app_handle.clone(), &updated_server).await;
|
||||
persist_servers(&app_handle)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to persist servers: {}", e))?;
|
||||
@@ -371,10 +403,10 @@ pub async fn add_coco_server<R: Runtime>(
|
||||
|
||||
let endpoint = endpoint.trim_end_matches('/');
|
||||
|
||||
if check_endpoint_exists(endpoint) {
|
||||
if check_endpoint_exists(endpoint).await {
|
||||
log::debug!(
|
||||
"This Coco server has already been registered: {:?}",
|
||||
&endpoint
|
||||
"trying to register a Coco server [{}] that has already been registered",
|
||||
endpoint
|
||||
);
|
||||
return Err("This Coco server has already been registered.".into());
|
||||
}
|
||||
@@ -386,6 +418,15 @@ pub async fn add_coco_server<R: Runtime>(
|
||||
|
||||
log::debug!("Get provider info response: {:?}", &response);
|
||||
|
||||
if response.status() != StatusCode::OK {
|
||||
log::debug!(
|
||||
"trying to register a Coco server [{}] that is possibly down",
|
||||
endpoint
|
||||
);
|
||||
|
||||
return Err("This Coco server is possibly down".into());
|
||||
}
|
||||
|
||||
let body = get_response_body_text(response).await?;
|
||||
|
||||
let mut server: Server = serde_json::from_str(&body)
|
||||
@@ -393,15 +434,32 @@ pub async fn add_coco_server<R: Runtime>(
|
||||
|
||||
trim_endpoint_last_forward_slash(&mut server);
|
||||
|
||||
// The JSON returned from `provider/_info` won't have this field, serde will set
|
||||
// it to an empty string during deserialization, we need to set a valid value here.
|
||||
if server.id.is_empty() {
|
||||
server.id = pizza_common::utils::uuid::Uuid::new().to_string();
|
||||
}
|
||||
|
||||
// Use the default name, if it is not set.
|
||||
if server.name.is_empty() {
|
||||
server.name = "Coco Server".to_string();
|
||||
}
|
||||
|
||||
save_server(&server);
|
||||
// Update the `available` field
|
||||
if server.public {
|
||||
// Serde already sets this to true, but just to make the code clear, do it again.
|
||||
server.available = true;
|
||||
} else {
|
||||
let opt_token = get_server_token(&server.id).await;
|
||||
assert!(
|
||||
opt_token.is_none(),
|
||||
"this Coco server is newly-added, we should have no token stored for it!"
|
||||
);
|
||||
// This is a non-public Coco server, and it is not logged in, so it is unavailable.
|
||||
server.available = false;
|
||||
}
|
||||
|
||||
save_server(&server).await;
|
||||
try_register_server_to_search_source(app_handle.clone(), &server).await;
|
||||
|
||||
persist_servers(&app_handle)
|
||||
@@ -413,6 +471,7 @@ pub async fn add_coco_server<R: Runtime>(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[function_name::named]
|
||||
pub async fn remove_coco_server<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
id: String,
|
||||
@@ -420,24 +479,47 @@ pub async fn remove_coco_server<R: Runtime>(
|
||||
let registry = app_handle.state::<SearchSourceRegistry>();
|
||||
registry.remove_source(id.as_str()).await;
|
||||
|
||||
remove_server_token(id.as_str());
|
||||
remove_server_by_id(id);
|
||||
|
||||
let opt_server = remove_server_by_id(id.as_str()).await;
|
||||
let Some(server) = opt_server else {
|
||||
panic!(
|
||||
"[{}()] invoked with a server [{}] that does not exist! Mismatched states between frontend and backend!",
|
||||
function_name!(),
|
||||
id
|
||||
);
|
||||
};
|
||||
persist_servers(&app_handle)
|
||||
.await
|
||||
.expect("failed to save servers");
|
||||
persist_servers_token(&app_handle).expect("failed to save server tokens");
|
||||
|
||||
// Only non-public Coco servers require tokens
|
||||
if !server.public {
|
||||
// If is logged in, clear the token as well.
|
||||
let deleted = remove_server_token(id.as_str()).await;
|
||||
if deleted {
|
||||
persist_servers_token(&app_handle)
|
||||
.await
|
||||
.expect("failed to save server tokens");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[function_name::named]
|
||||
pub async fn enable_server<R: Runtime>(app_handle: AppHandle<R>, id: String) -> Result<(), ()> {
|
||||
println!("enable_server: {}", id);
|
||||
let opt_server = get_server_by_id(id.as_str()).await;
|
||||
|
||||
let Some(mut server) = opt_server else {
|
||||
panic!(
|
||||
"[{}()] invoked with a server [{}] that does not exist! Mismatched states between frontend and backend!",
|
||||
function_name!(),
|
||||
id
|
||||
);
|
||||
};
|
||||
|
||||
let server = get_server_by_id(id.as_str());
|
||||
if let Some(mut server) = server {
|
||||
server.enabled = true;
|
||||
save_server(&server);
|
||||
save_server(&server).await;
|
||||
|
||||
// Register the server to the search source
|
||||
try_register_server_to_search_source(app_handle.clone(), &server).await;
|
||||
@@ -445,26 +527,56 @@ pub async fn enable_server<R: Runtime>(app_handle: AppHandle<R>, id: String) ->
|
||||
persist_servers(&app_handle)
|
||||
.await
|
||||
.expect("failed to save servers");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[function_name::named]
|
||||
pub async fn disable_server<R: Runtime>(app_handle: AppHandle<R>, id: String) -> Result<(), ()> {
|
||||
let opt_server = get_server_by_id(id.as_str()).await;
|
||||
|
||||
let Some(mut server) = opt_server else {
|
||||
panic!(
|
||||
"[{}()] invoked with a server [{}] that does not exist! Mismatched states between frontend and backend!",
|
||||
function_name!(),
|
||||
id
|
||||
);
|
||||
};
|
||||
|
||||
server.enabled = false;
|
||||
|
||||
let registry = app_handle.state::<SearchSourceRegistry>();
|
||||
registry.remove_source(id.as_str()).await;
|
||||
|
||||
save_server(&server).await;
|
||||
persist_servers(&app_handle)
|
||||
.await
|
||||
.expect("failed to save servers");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// For non-public Coco servers, we add it to the search source as long as it is
|
||||
/// enabled.
|
||||
///
|
||||
/// For public Coco server, an extra token is required.
|
||||
pub async fn try_register_server_to_search_source(
|
||||
app_handle: AppHandle<impl Runtime>,
|
||||
server: &Server,
|
||||
) {
|
||||
if server.enabled {
|
||||
log::trace!(
|
||||
"Server {} is public: {} and available: {}",
|
||||
"Server [name: {}, id: {}] is public: {} and available: {}",
|
||||
&server.name,
|
||||
&server.id,
|
||||
&server.public,
|
||||
&server.available
|
||||
);
|
||||
|
||||
if !server.public {
|
||||
let token = get_server_token(&server.id).await;
|
||||
let opt_token = get_server_token(&server.id).await;
|
||||
|
||||
if !token.is_ok() || token.is_ok() && token.unwrap().is_none() {
|
||||
if opt_token.is_none() {
|
||||
log::debug!("Server {} is not public and no token was found", &server.id);
|
||||
return;
|
||||
}
|
||||
@@ -476,113 +588,110 @@ pub async fn try_register_server_to_search_source(
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mark_server_as_online<R: Runtime>(
|
||||
app_handle: AppHandle<R>, id: &str) -> Result<(), ()> {
|
||||
// println!("server_is_offline: {}", id);
|
||||
let server = get_server_by_id(id);
|
||||
#[function_name::named]
|
||||
#[allow(unused)]
|
||||
async fn mark_server_as_online<R: Runtime>(app_handle: AppHandle<R>, id: &str) {
|
||||
let server = get_server_by_id(id).await;
|
||||
if let Some(mut server) = server {
|
||||
server.available = true;
|
||||
server.health = None;
|
||||
save_server(&server);
|
||||
save_server(&server).await;
|
||||
|
||||
try_register_server_to_search_source(app_handle.clone(), &server).await;
|
||||
} else {
|
||||
log::warn!(
|
||||
"[{}()] invoked with a server [{}] that does not exist!",
|
||||
function_name!(),
|
||||
id
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn mark_server_as_offline<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
id: &str,
|
||||
) -> Result<(), ()> {
|
||||
// println!("server_is_offline: {}", id);
|
||||
let server = get_server_by_id(id);
|
||||
#[function_name::named]
|
||||
pub(crate) async fn mark_server_as_offline<R: Runtime>(app_handle: AppHandle<R>, id: &str) {
|
||||
let server = get_server_by_id(id).await;
|
||||
if let Some(mut server) = server {
|
||||
server.available = false;
|
||||
server.health = None;
|
||||
save_server(&server);
|
||||
save_server(&server).await;
|
||||
|
||||
let registry = app_handle.state::<SearchSourceRegistry>();
|
||||
registry.remove_source(id).await;
|
||||
} else {
|
||||
log::warn!(
|
||||
"[{}()] invoked with a server [{}] that does not exist!",
|
||||
function_name!(),
|
||||
id
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn disable_server<R: Runtime>(app_handle: AppHandle<R>, id: String) -> Result<(), ()> {
|
||||
let server = get_server_by_id(id.as_str());
|
||||
if let Some(mut server) = server {
|
||||
server.enabled = false;
|
||||
|
||||
let registry = app_handle.state::<SearchSourceRegistry>();
|
||||
registry.remove_source(id.as_str()).await;
|
||||
|
||||
save_server(&server);
|
||||
persist_servers(&app_handle)
|
||||
.await
|
||||
.expect("failed to save servers");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[function_name::named]
|
||||
pub async fn logout_coco_server<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
id: String,
|
||||
) -> Result<(), String> {
|
||||
log::debug!("Attempting to log out server by id: {}", &id);
|
||||
|
||||
// Check if server token exists
|
||||
if let Some(_token) = get_server_token(id.as_str()).await? {
|
||||
log::debug!("Found server token for id: {}", &id);
|
||||
|
||||
// Remove the server token from cache
|
||||
remove_server_token(id.as_str());
|
||||
|
||||
// Persist the updated tokens
|
||||
if let Err(e) = persist_servers_token(&app_handle) {
|
||||
log::debug!("Failed to save tokens for id: {}. Error: {:?}", &id, &e);
|
||||
return Err(format!("Failed to save tokens: {}", &e));
|
||||
}
|
||||
} else {
|
||||
// Log the case where server token is not found
|
||||
log::debug!("No server token found for id: {}", &id);
|
||||
}
|
||||
|
||||
// Check if the server exists
|
||||
if let Some(mut server) = get_server_by_id(id.as_str()) {
|
||||
log::debug!("Found server for id: {}", &id);
|
||||
let Some(mut server) = get_server_by_id(id.as_str()).await else {
|
||||
panic!(
|
||||
"[{}()] invoked with a server [{}] that does not exist! Mismatched states between frontend and backend!",
|
||||
function_name!(),
|
||||
id
|
||||
);
|
||||
};
|
||||
|
||||
// Clear server profile
|
||||
server.profile = None;
|
||||
let _ = mark_server_as_offline(app_handle.clone(), id.as_str()).await;
|
||||
|
||||
// Logging out from a non-public Coco server makes it unavailable
|
||||
if !server.public {
|
||||
server.available = false;
|
||||
}
|
||||
// Save the updated server data
|
||||
save_server(&server);
|
||||
|
||||
save_server(&server).await;
|
||||
// Persist the updated server data
|
||||
if let Err(e) = persist_servers(&app_handle).await {
|
||||
log::debug!("Failed to save server for id: {}. Error: {:?}", &id, &e);
|
||||
return Err(format!("Failed to save server: {}", &e));
|
||||
}
|
||||
|
||||
let has_token = get_server_token(id.as_str()).await.is_some();
|
||||
if server.public {
|
||||
if has_token {
|
||||
panic!("Public Coco server won't have token")
|
||||
}
|
||||
} else {
|
||||
// Log the case where server is not found
|
||||
log::debug!("No server found for id: {}", &id);
|
||||
return Err(format!("No server found for id: {}", id));
|
||||
assert!(
|
||||
has_token,
|
||||
"This is a non-public Coco server, and it is logged in, we should have a token"
|
||||
);
|
||||
// Remove the server token from cache
|
||||
remove_server_token(id.as_str()).await;
|
||||
|
||||
// Persist the updated tokens
|
||||
if let Err(e) = persist_servers_token(&app_handle).await {
|
||||
log::debug!("Failed to save tokens for id: {}. Error: {:?}", &id, &e);
|
||||
return Err(format!("Failed to save tokens: {}", &e));
|
||||
}
|
||||
}
|
||||
|
||||
// Remove it from the search source if it becomes unavailable
|
||||
if !server.available {
|
||||
let registry = app_handle.state::<SearchSourceRegistry>();
|
||||
registry.remove_source(id.as_str()).await;
|
||||
}
|
||||
|
||||
log::debug!("Successfully logged out server with id: {}", &id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes the trailing slash from the server's endpoint if present.
|
||||
/// Helper function to remove the trailing slash from the server's endpoint if present.
|
||||
fn trim_endpoint_last_forward_slash(server: &mut Server) {
|
||||
if server.endpoint.ends_with('/') {
|
||||
server.endpoint.pop(); // Remove the last character
|
||||
while server.endpoint.ends_with('/') {
|
||||
server.endpoint.pop();
|
||||
}
|
||||
let endpoint = &mut server.endpoint;
|
||||
while endpoint.ends_with('/') {
|
||||
endpoint.pop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,8 +700,12 @@ fn provider_info_url(endpoint: &str) -> String {
|
||||
format!("{endpoint}/provider/_info")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trim_endpoint_last_forward_slash() {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_trim_endpoint_last_forward_slash() {
|
||||
let mut server = Server {
|
||||
id: "test".to_string(),
|
||||
builtin: false,
|
||||
@@ -629,4 +742,5 @@ fn test_trim_endpoint_last_forward_slash() {
|
||||
trim_endpoint_last_forward_slash(&mut server);
|
||||
|
||||
assert_eq!(server.endpoint, "https://example.com");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::server::http_client::HttpClient;
|
||||
use futures_util::StreamExt;
|
||||
use http::Method;
|
||||
use serde_json::json;
|
||||
use tauri::{command, AppHandle, Emitter, Runtime};
|
||||
use tauri::{AppHandle, Emitter, Runtime, command};
|
||||
|
||||
#[command]
|
||||
pub async fn synthesize<R: Runtime>(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::common::http::get_response_body_text;
|
||||
use crate::server::http_client::HttpClient;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{from_str, Value};
|
||||
use serde_json::{Value, from_str};
|
||||
use tauri::command;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
||||
@@ -4,12 +4,12 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
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::sync::{Mutex, mpsc};
|
||||
use tokio_tungstenite::MaybeTlsStream;
|
||||
use tokio_tungstenite::WebSocketStream;
|
||||
use tokio_tungstenite::{connect_async_tls_with_config, Connector};
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
|
||||
use tokio_tungstenite::{Connector, connect_async_tls_with_config};
|
||||
#[derive(Default)]
|
||||
pub struct WebSocketManager {
|
||||
connections: Arc<Mutex<HashMap<String, Arc<WebSocketInstance>>>>,
|
||||
@@ -53,9 +53,11 @@ pub async fn connect_to_server<R: Runtime>(
|
||||
// Disconnect old connection first
|
||||
disconnect(client_id.clone(), state.clone()).await.ok();
|
||||
|
||||
let server = get_server_by_id(&id).ok_or(format!("Server with ID {} not found", id))?;
|
||||
let server = get_server_by_id(&id)
|
||||
.await
|
||||
.ok_or(format!("Server with ID {} not found", id))?;
|
||||
let endpoint = convert_to_websocket(&server.endpoint)?;
|
||||
let token = get_server_token(&id).await?.map(|t| t.access_token.clone());
|
||||
let token = get_server_token(&id).await.map(|t| t.access_token.clone());
|
||||
|
||||
let mut request =
|
||||
tokio_tungstenite::tungstenite::client::IntoClientRequest::into_client_request(&endpoint)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
//credits to: https://github.com/ayangweb/ayangweb-EcoPaste/blob/169323dbe6365ffe4abb64d867439ed2ea84c6d1/src-tauri/src/core/setup/mac.rs
|
||||
use tauri::{App, Emitter, EventTarget, WebviewWindow};
|
||||
use tauri_nspanel::{cocoa::appkit::NSWindowCollectionBehavior, panel_delegate, WebviewWindowExt};
|
||||
//! credits to: https://github.com/ayangweb/ayangweb-EcoPaste/blob/169323dbe6365ffe4abb64d867439ed2ea84c6d1/src-tauri/src/core/setup/mac.rs
|
||||
|
||||
use cocoa::appkit::NSWindow;
|
||||
use tauri::Manager;
|
||||
use tauri::{App, AppHandle, Emitter, EventTarget, WebviewWindow};
|
||||
use tauri_nspanel::{WebviewWindowExt, cocoa::appkit::NSWindowCollectionBehavior, panel_delegate};
|
||||
|
||||
use crate::common::MAIN_WINDOW_LABEL;
|
||||
|
||||
@@ -29,7 +32,7 @@ pub fn platform(
|
||||
|
||||
// Share the window across all desktop spaces and full screen
|
||||
panel.set_collection_behaviour(
|
||||
NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces
|
||||
NSWindowCollectionBehavior::NSWindowCollectionBehaviorMoveToActiveSpace
|
||||
| NSWindowCollectionBehavior::NSWindowCollectionBehaviorStationary
|
||||
| NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary,
|
||||
);
|
||||
@@ -78,3 +81,50 @@ pub fn platform(
|
||||
// Set the delegate object for the window to handle window events
|
||||
panel.set_delegate(delegate);
|
||||
}
|
||||
|
||||
/// Change NS window attribute between `NSWindowCollectionBehaviorCanJoinAllSpaces`
|
||||
/// and `NSWindowCollectionBehaviorMoveToActiveSpace` accordingly.
|
||||
///
|
||||
/// NOTE: this tauri command is not async because we should run it in the main
|
||||
/// thread, or `ns_window.setCollectionBehavior_(collection_behavior)` would lead
|
||||
/// to UB.
|
||||
#[tauri::command]
|
||||
pub(crate) fn toggle_move_to_active_space_attribute(tauri_app_hanlde: AppHandle) {
|
||||
use cocoa::appkit::NSWindowCollectionBehavior;
|
||||
use cocoa::base::id;
|
||||
|
||||
let main_window = tauri_app_hanlde
|
||||
.get_webview_window(MAIN_WINDOW_LABEL)
|
||||
.unwrap();
|
||||
let ns_window = main_window.ns_window().unwrap() as id;
|
||||
let mut collection_behavior = unsafe { ns_window.collectionBehavior() };
|
||||
let join_all_spaces = collection_behavior
|
||||
.contains(NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces);
|
||||
let move_to_active_space = collection_behavior
|
||||
.contains(NSWindowCollectionBehavior::NSWindowCollectionBehaviorMoveToActiveSpace);
|
||||
|
||||
match (join_all_spaces, move_to_active_space) {
|
||||
(true, false) => {
|
||||
collection_behavior
|
||||
.remove(NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces);
|
||||
collection_behavior
|
||||
.insert(NSWindowCollectionBehavior::NSWindowCollectionBehaviorMoveToActiveSpace);
|
||||
}
|
||||
(false, true) => {
|
||||
collection_behavior
|
||||
.remove(NSWindowCollectionBehavior::NSWindowCollectionBehaviorMoveToActiveSpace);
|
||||
collection_behavior
|
||||
.insert(NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces);
|
||||
}
|
||||
_ => {
|
||||
panic!(
|
||||
"invalid NS window attribute, NSWindowCollectionBehaviorCanJoinAllSpaces is set [{}], NSWindowCollectionBehaviorMoveToActiveSpace is set [{}]",
|
||||
join_all_spaces, move_to_active_space
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
ns_window.setCollectionBehavior_(collection_behavior);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{hide_coco, show_coco, COCO_TAURI_STORE};
|
||||
use tauri::{async_runtime, App, AppHandle, Manager, Runtime};
|
||||
use crate::{COCO_TAURI_STORE, hide_coco, show_coco};
|
||||
use tauri::{App, AppHandle, Manager, Runtime, async_runtime};
|
||||
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState};
|
||||
use tauri_plugin_store::{JsonValue, StoreExt};
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Copy)]
|
||||
pub(crate) enum FileType {
|
||||
Folder,
|
||||
@@ -51,7 +50,6 @@ pub(crate) enum FileType {
|
||||
Unknown,
|
||||
}
|
||||
|
||||
|
||||
async fn get_file_type(path: &str) -> FileType {
|
||||
let path = camino::Utf8Path::new(path);
|
||||
|
||||
@@ -116,7 +114,6 @@ async fn get_file_type(path: &str) -> FileType {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn type_to_icon(ty: FileType) -> &'static str {
|
||||
match ty {
|
||||
FileType::Folder => "font_file_folder",
|
||||
@@ -170,7 +167,6 @@ fn type_to_icon(ty: FileType) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn get_file_icon(path: String) -> &'static str {
|
||||
let ty = get_file_type(path.as_str()).await;
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
pub(crate) mod app_lang;
|
||||
pub(crate) mod file;
|
||||
pub(crate) mod platform;
|
||||
pub(crate) mod app_lang;
|
||||
pub(crate) mod updater;
|
||||
|
||||
use std::{path::Path, process::Command};
|
||||
use tauri::{AppHandle, Runtime};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
/// We use this env variable to determine the DE on Linux.
|
||||
const XDG_CURRENT_DESKTOP: &str = "XDG_CURRENT_DESKTOP";
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum LinuxDesktopEnvironment {
|
||||
Gnome,
|
||||
Kde,
|
||||
Unsupported { xdg_current_desktop: String },
|
||||
}
|
||||
|
||||
impl LinuxDesktopEnvironment {
|
||||
@@ -34,6 +40,14 @@ impl LinuxDesktopEnvironment {
|
||||
.arg(path)
|
||||
.output()
|
||||
.map_err(|e| e.to_string())?,
|
||||
Self::Unsupported {
|
||||
xdg_current_desktop,
|
||||
} => {
|
||||
return Err(format!(
|
||||
"Cannot open apps as this Linux desktop environment [{}] is not supported",
|
||||
xdg_current_desktop
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if !cmd_output.status.success() {
|
||||
@@ -48,20 +62,23 @@ impl LinuxDesktopEnvironment {
|
||||
}
|
||||
}
|
||||
|
||||
/// None means that it is likely that we do not have a desktop environment.
|
||||
fn get_linux_desktop_environment() -> Option<LinuxDesktopEnvironment> {
|
||||
let de_os_str = std::env::var_os("XDG_CURRENT_DESKTOP")?;
|
||||
let de_str = de_os_str
|
||||
.into_string()
|
||||
.expect("$XDG_CURRENT_DESKTOP should be UTF-8 encoded");
|
||||
let de_os_str = std::env::var_os(XDG_CURRENT_DESKTOP)?;
|
||||
let de_str = de_os_str.into_string().unwrap_or_else(|_os_string| {
|
||||
panic!("${} should be UTF-8 encoded", XDG_CURRENT_DESKTOP);
|
||||
});
|
||||
|
||||
let de = match de_str.as_str() {
|
||||
"GNOME" => LinuxDesktopEnvironment::Gnome,
|
||||
// Ubuntu uses "ubuntu:GNOME" instead of just "GNOME", they really love
|
||||
// their distro name.
|
||||
"ubuntu:GNOME" => LinuxDesktopEnvironment::Gnome,
|
||||
"KDE" => LinuxDesktopEnvironment::Kde,
|
||||
|
||||
unsupported_de => unimplemented!(
|
||||
"This desktop environment [{}] has not been supported yet",
|
||||
unsupported_de
|
||||
),
|
||||
_ => LinuxDesktopEnvironment::Unsupported {
|
||||
xdg_current_desktop: de_str,
|
||||
},
|
||||
};
|
||||
|
||||
Some(de)
|
||||
@@ -76,7 +93,7 @@ pub async fn open<R: Runtime>(app_handle: AppHandle<R>, path: String) -> Result<
|
||||
let borrowed_path = Path::new(&path);
|
||||
if let Some(file_extension) = borrowed_path.extension() {
|
||||
if file_extension == "desktop" {
|
||||
let desktop_environment = get_linux_desktop_environment().expect("The Linux OS is running without a desktop, Coco could never run in such a environment");
|
||||
let desktop_environment = get_linux_desktop_environment().expect("The Linux OS is running without a desktop, Coco could never run in such an environment");
|
||||
return desktop_environment.launch_app_via_desktop_file(path);
|
||||
}
|
||||
}
|
||||
@@ -87,3 +104,55 @@ pub async fn open<R: Runtime>(app_handle: AppHandle<R>, path: String) -> Result<
|
||||
.open(path, None)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// This test modifies env var XDG_CURRENT_DESKTOP, which is kinda unsafe
|
||||
// but considering this is just test, it is ok to do so.
|
||||
#[test]
|
||||
fn test_get_linux_desktop_environment() {
|
||||
// SAFETY: Rust code won't modify/read XDG_CURRENT_DESKTOP concurrently, we
|
||||
// have no guarantee from the underlying C code.
|
||||
unsafe {
|
||||
// Save the original value if it exists
|
||||
let original_value = std::env::var_os(XDG_CURRENT_DESKTOP);
|
||||
|
||||
// Test when XDG_CURRENT_DESKTOP is not set
|
||||
std::env::remove_var(XDG_CURRENT_DESKTOP);
|
||||
assert!(get_linux_desktop_environment().is_none());
|
||||
|
||||
// Test GNOME
|
||||
std::env::set_var(XDG_CURRENT_DESKTOP, "GNOME");
|
||||
let result = get_linux_desktop_environment();
|
||||
assert_eq!(result.unwrap(), LinuxDesktopEnvironment::Gnome);
|
||||
|
||||
// Test ubuntu:GNOME
|
||||
std::env::set_var(XDG_CURRENT_DESKTOP, "ubuntu:GNOME");
|
||||
let result = get_linux_desktop_environment();
|
||||
assert_eq!(result.unwrap(), LinuxDesktopEnvironment::Gnome);
|
||||
|
||||
// Test KDE
|
||||
std::env::set_var(XDG_CURRENT_DESKTOP, "KDE");
|
||||
let result = get_linux_desktop_environment();
|
||||
assert_eq!(result.unwrap(), LinuxDesktopEnvironment::Kde);
|
||||
|
||||
// Test unsupported desktop environment
|
||||
std::env::set_var(XDG_CURRENT_DESKTOP, "XFCE");
|
||||
let result = get_linux_desktop_environment();
|
||||
assert_eq!(
|
||||
result.unwrap(),
|
||||
LinuxDesktopEnvironment::Unsupported {
|
||||
xdg_current_desktop: "XFCE".into()
|
||||
}
|
||||
);
|
||||
|
||||
// Restore the original value
|
||||
match original_value {
|
||||
Some(value) => std::env::set_var(XDG_CURRENT_DESKTOP, value),
|
||||
None => std::env::remove_var(XDG_CURRENT_DESKTOP),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Copy, Clone, Hash, PartialEq, Eq, Display)]
|
||||
@@ -13,7 +13,6 @@ pub(crate) enum Platform {
|
||||
Windows,
|
||||
}
|
||||
|
||||
|
||||
impl Platform {
|
||||
/// Helper function to determine the current platform.
|
||||
pub(crate) fn current() -> Platform {
|
||||
@@ -26,16 +25,10 @@ impl Platform {
|
||||
/// Return the `X-OS-NAME` HTTP request header.
|
||||
pub(crate) fn to_os_name_http_header_str(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
Self::Macos => {
|
||||
Cow::Borrowed("macos")
|
||||
}
|
||||
Self::Windows => {
|
||||
Cow::Borrowed("windows")
|
||||
}
|
||||
Self::Macos => Cow::Borrowed("macos"),
|
||||
Self::Windows => Cow::Borrowed("windows"),
|
||||
// For Linux, we need the actual distro `ID`, not just a "linux".
|
||||
Self::Linux => {
|
||||
Cow::Owned(sysinfo::System::distribution_id())
|
||||
}
|
||||
Self::Linux => Cow::Owned(sysinfo::System::distribution_id()),
|
||||
}
|
||||
}
|
||||
}
|
||||
67
src-tauri/src/util/updater.rs
Normal file
67
src-tauri/src/util/updater.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use semver::Version;
|
||||
use tauri_plugin_updater::RemoteRelease;
|
||||
|
||||
/// Helper function to extract the build number out of `version`.
|
||||
///
|
||||
/// If the version string is in the `x.y.z` format and does not include a build
|
||||
/// number, we assume a build number of 0.
|
||||
fn extract_version_number(version: &Version) -> u32 {
|
||||
let pre = &version.pre;
|
||||
|
||||
if pre.is_empty() {
|
||||
// A special value for the versions that do not have array
|
||||
0
|
||||
} else {
|
||||
let pre_str = pre.as_str();
|
||||
let build_number_str = {
|
||||
match pre_str.strip_prefix("SNAPSHOT-") {
|
||||
Some(str) => str,
|
||||
None => pre_str,
|
||||
}
|
||||
};
|
||||
let build_number : u32 = build_number_str.parse().unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"invalid build number, cannot parse [{}] to a valid build number, error [{}], version [{}]",
|
||||
build_number_str, e, version
|
||||
)
|
||||
});
|
||||
|
||||
build_number
|
||||
}
|
||||
}
|
||||
|
||||
/// # Local version format
|
||||
///
|
||||
/// Packages built in our CI use the following format:
|
||||
///
|
||||
/// * `x.y.z-SNAPSHOT-<build number>`
|
||||
/// * `x.y.z-<build number>`
|
||||
///
|
||||
/// If you build Coco from src, the version will be in format `x.y.z`
|
||||
///
|
||||
/// # Remote version format
|
||||
///
|
||||
/// `x.y.z-<build number>`
|
||||
///
|
||||
/// # How we compare versions
|
||||
///
|
||||
/// We compare versions based solely on the build number.
|
||||
/// If the version string is in the `x.y.z` format and does not include a build number,
|
||||
/// we assume a build number of 0. As a result, such versions are considered older
|
||||
/// than any version with an explicit build number.
|
||||
pub(crate) fn custom_version_comparator(local: Version, remote_release: RemoteRelease) -> bool {
|
||||
let remote = remote_release.version;
|
||||
|
||||
let local_build_number = extract_version_number(&local);
|
||||
let remote_build_number = extract_version_number(&remote);
|
||||
|
||||
let should_update = remote_build_number > local_build_number;
|
||||
log::debug!(
|
||||
"custom version comparator invoked, local version [{}], remote version [{}], should update [{}]",
|
||||
local,
|
||||
remote,
|
||||
should_update
|
||||
);
|
||||
|
||||
should_update
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
|
||||
import {
|
||||
ServerTokenResponse,
|
||||
Server,
|
||||
Connector,
|
||||
DataSource,
|
||||
@@ -17,6 +17,24 @@ import {
|
||||
} from "@/types/commands";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
|
||||
export function handleLogout(serverId?: string) {
|
||||
const setIsCurrentLogin = useAuthStore.getState().setIsCurrentLogin;
|
||||
const { currentService, setCurrentService, serverList, setServerList } =
|
||||
useConnectStore.getState();
|
||||
const id = serverId || currentService?.id;
|
||||
if (!id) return;
|
||||
setIsCurrentLogin(false);
|
||||
emit("login_or_logout", false);
|
||||
if (currentService?.id === id) {
|
||||
setCurrentService({ ...currentService, profile: null });
|
||||
}
|
||||
const updatedServerList = serverList.map((server) =>
|
||||
server.id === id ? { ...server, profile: null } : server
|
||||
);
|
||||
setServerList(updatedServerList);
|
||||
}
|
||||
|
||||
// Endpoints that don't require authentication
|
||||
const WHITELIST_SERVERS = [
|
||||
@@ -37,7 +55,14 @@ async function invokeWithErrorHandler<T>(
|
||||
args?: Record<string, any>
|
||||
): Promise<T> {
|
||||
const isCurrentLogin = useAuthStore.getState().isCurrentLogin;
|
||||
if (!WHITELIST_SERVERS.includes(command) && !isCurrentLogin) {
|
||||
const currentService = useConnectStore.getState().currentService;
|
||||
|
||||
// Not logged in
|
||||
console.log(command, isCurrentLogin, currentService?.profile);
|
||||
if (
|
||||
!WHITELIST_SERVERS.includes(command) &&
|
||||
(!isCurrentLogin || !currentService?.profile)
|
||||
) {
|
||||
console.error("This command requires authentication");
|
||||
throw new Error("This command requires authentication");
|
||||
}
|
||||
@@ -67,15 +92,16 @@ async function invokeWithErrorHandler<T>(
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
const errorMessage = error || "Command execution failed";
|
||||
// 401 Unauthorized
|
||||
if (errorMessage.includes("Unauthorized")) {
|
||||
handleLogout();
|
||||
} else {
|
||||
addError(command + ":" + errorMessage, "error");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function get_server_token(id: string): Promise<ServerTokenResponse> {
|
||||
return invokeWithErrorHandler(`get_server_token`, { id });
|
||||
}
|
||||
|
||||
export function list_coco_servers(): Promise<Server[]> {
|
||||
return invokeWithErrorHandler(`list_coco_servers`);
|
||||
}
|
||||
@@ -221,13 +247,16 @@ export function open_session_chat({
|
||||
export function cancel_session_chat({
|
||||
serverId,
|
||||
sessionId,
|
||||
queryParams,
|
||||
}: {
|
||||
serverId: string;
|
||||
sessionId: string;
|
||||
queryParams?: Record<string, any>;
|
||||
}): Promise<string> {
|
||||
return invokeWithErrorHandler(`cancel_session_chat`, {
|
||||
serverId,
|
||||
sessionId,
|
||||
queryParams,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -254,15 +283,18 @@ export function chat_create({
|
||||
serverId,
|
||||
message,
|
||||
queryParams,
|
||||
clientId,
|
||||
}: {
|
||||
serverId: string;
|
||||
message: string;
|
||||
queryParams?: Record<string, any>;
|
||||
clientId: string;
|
||||
}): Promise<GetResponse> {
|
||||
return invokeWithErrorHandler(`chat_create`, {
|
||||
serverId,
|
||||
message,
|
||||
queryParams,
|
||||
clientId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -293,17 +325,20 @@ export function chat_chat({
|
||||
sessionId,
|
||||
message,
|
||||
queryParams,
|
||||
clientId,
|
||||
}: {
|
||||
serverId: string;
|
||||
sessionId: string;
|
||||
message: string;
|
||||
queryParams?: Record<string, any>;
|
||||
clientId: string;
|
||||
}): Promise<string> {
|
||||
return invokeWithErrorHandler(`chat_chat`, {
|
||||
serverId,
|
||||
sessionId,
|
||||
message,
|
||||
queryParams,
|
||||
clientId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -382,3 +417,7 @@ export const query_coco_fusion = (payload: {
|
||||
...payload,
|
||||
});
|
||||
};
|
||||
|
||||
export const get_app_search_source = () => {
|
||||
return invokeWithErrorHandler<void>("get_app_search_source");
|
||||
};
|
||||
@@ -35,3 +35,7 @@ export function show_check(): Promise<void> {
|
||||
export function hide_check(): Promise<void> {
|
||||
return invoke('hide_check');
|
||||
}
|
||||
|
||||
export function toggle_move_to_active_space_attribute(): Promise<void> {
|
||||
return invoke('toggle_move_to_active_space_attribute');
|
||||
}
|
||||
@@ -40,6 +40,7 @@ interface ChatAIProps {
|
||||
assistantIDs?: string[];
|
||||
startPage?: StartPage;
|
||||
formatUrl?: (data: any) => string;
|
||||
instanceId?: string;
|
||||
}
|
||||
|
||||
export interface ChatAIRef {
|
||||
@@ -66,6 +67,7 @@ const ChatAI = memo(
|
||||
assistantIDs,
|
||||
startPage,
|
||||
formatUrl,
|
||||
instanceId,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
@@ -75,7 +77,8 @@ const ChatAI = memo(
|
||||
clearChat: clearChat,
|
||||
}));
|
||||
|
||||
const { curChatEnd, setCurChatEnd } = useChatStore();
|
||||
const curChatEnd = useChatStore((state) => state.curChatEnd);
|
||||
const setCurChatEnd = useChatStore((state) => state.setCurChatEnd);
|
||||
|
||||
const isTauri = useAppStore((state) => state.isTauri);
|
||||
|
||||
@@ -92,6 +95,7 @@ const ChatAI = memo(
|
||||
const [timedoutShow, setTimedoutShow] = useState(false);
|
||||
|
||||
const curIdRef = useRef("");
|
||||
const curSessionIdRef = useRef("");
|
||||
|
||||
const [isSidebarOpenChat, setIsSidebarOpenChat] = useState(isSidebarOpen);
|
||||
const [chats, setChats] = useState<Chat[]>([]);
|
||||
@@ -175,17 +179,21 @@ const ChatAI = memo(
|
||||
clearAllChunkData,
|
||||
setQuestion,
|
||||
curIdRef,
|
||||
curSessionIdRef,
|
||||
setChats,
|
||||
dealMsgRef,
|
||||
setLoadingStep,
|
||||
isChatPage,
|
||||
isSearchActive,
|
||||
isDeepThinkActive,
|
||||
isMCPActive,
|
||||
changeInput,
|
||||
showChatHistory
|
||||
showChatHistory,
|
||||
);
|
||||
|
||||
const { dealMsg } = useMessageHandler(
|
||||
curIdRef,
|
||||
curSessionIdRef,
|
||||
setCurChatEnd,
|
||||
setTimedoutShow,
|
||||
(chat) => cancelChat(chat || activeChat),
|
||||
@@ -229,7 +237,7 @@ const ChatAI = memo(
|
||||
return;
|
||||
}
|
||||
if (!activeChat?._id) {
|
||||
await createNewChat(value, activeChat);
|
||||
await createNewChat(value);
|
||||
} else {
|
||||
await handleSendMessage(value, activeChat);
|
||||
}
|
||||
@@ -254,7 +262,8 @@ const ChatAI = memo(
|
||||
const onSelectChat = useCallback(
|
||||
async (chat: Chat) => {
|
||||
setTimedoutShow(false);
|
||||
clearAllChunkData();
|
||||
|
||||
await clearAllChunkData();
|
||||
await cancelChat(activeChat);
|
||||
await chatClose(activeChat);
|
||||
const response = await openSessionChat(chat);
|
||||
@@ -359,6 +368,7 @@ const ChatAI = memo(
|
||||
)}
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
data-chat-instance={instanceId}
|
||||
className={`flex flex-col rounded-md h-full overflow-hidden relative`}
|
||||
>
|
||||
<ChatHeader
|
||||
@@ -376,7 +386,6 @@ const ChatAI = memo(
|
||||
<>
|
||||
<ChatContent
|
||||
activeChat={activeChat}
|
||||
curChatEnd={curChatEnd}
|
||||
query_intent={query_intent}
|
||||
tools={tools}
|
||||
fetch_source={fetch_source}
|
||||
@@ -392,6 +401,7 @@ const ChatAI = memo(
|
||||
}
|
||||
getFileUrl={getFileUrl}
|
||||
formatUrl={formatUrl}
|
||||
curIdRef={curIdRef}
|
||||
/>
|
||||
<Splash assistantIDs={assistantIDs} startPage={startPage} />
|
||||
</>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ChatMessage } from "@/components/ChatMessage";
|
||||
import { Greetings } from "./Greetings";
|
||||
// import FileList from "@/components/Assistant/FileList";
|
||||
import { useChatScroll } from "@/hooks/useChatScroll";
|
||||
// import { useChatStore } from "@/stores/chatStore";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import type { Chat, IChunkData } from "@/types/chat";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
// import SessionFile from "./SessionFile";
|
||||
@@ -13,7 +13,6 @@ import ScrollToBottom from "@/components/Common/ScrollToBottom";
|
||||
|
||||
interface ChatContentProps {
|
||||
activeChat?: Chat;
|
||||
curChatEnd: boolean;
|
||||
query_intent?: IChunkData;
|
||||
tools?: IChunkData;
|
||||
fetch_source?: IChunkData;
|
||||
@@ -27,11 +26,11 @@ interface ChatContentProps {
|
||||
handleSendMessage: (content: string, newChat?: Chat) => void;
|
||||
getFileUrl: (path: string) => string;
|
||||
formatUrl?: (data: any) => string;
|
||||
curIdRef: React.MutableRefObject<string>;
|
||||
}
|
||||
|
||||
export const ChatContent = ({
|
||||
activeChat,
|
||||
curChatEnd,
|
||||
query_intent,
|
||||
tools,
|
||||
fetch_source,
|
||||
@@ -60,6 +59,8 @@ export const ChatContent = ({
|
||||
const [isAtBottom, setIsAtBottom] = useState(true);
|
||||
const visibleStartPage = useConnectStore((state) => state.visibleStartPage);
|
||||
|
||||
const curChatEnd = useChatStore((state) => state.curChatEnd);
|
||||
|
||||
useEffect(() => {
|
||||
setIsAtBottom(true);
|
||||
setCurrentSessionId(activeChat?._id);
|
||||
@@ -68,7 +69,7 @@ export const ChatContent = ({
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [
|
||||
activeChat?.id,
|
||||
activeChat?._id,
|
||||
query_intent?.message_chunk,
|
||||
fetch_source?.message_chunk,
|
||||
pick_source?.message_chunk,
|
||||
@@ -122,7 +123,7 @@ export const ChatContent = ({
|
||||
deep_read ||
|
||||
think ||
|
||||
response) &&
|
||||
activeChat?._id ? (
|
||||
activeChat?._source?.id ? (
|
||||
<ChatMessage
|
||||
key={"current"}
|
||||
message={{
|
||||
|
||||
@@ -7,12 +7,12 @@ import PinIcon from "@/icons/Pin";
|
||||
import WindowsFullIcon from "@/icons/WindowsFull";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import type { Chat } from "@/types/chat";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import VisibleKey from "../Common/VisibleKey";
|
||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||
import { HISTORY_PANEL_ID } from "@/constants";
|
||||
import { AssistantList } from "./AssistantList";
|
||||
import { ServerList } from "./ServerList";
|
||||
import { useTogglePin } from "@/hooks/useTogglePin";
|
||||
|
||||
interface ChatHeaderProps {
|
||||
clearChat: () => void;
|
||||
@@ -35,32 +35,11 @@ export function ChatHeader({
|
||||
showChatHistory = true,
|
||||
assistantIDs,
|
||||
}: ChatHeaderProps) {
|
||||
const isPinned = useAppStore((state) => state.isPinned);
|
||||
const setIsPinned = useAppStore((state) => state.setIsPinned);
|
||||
const { isTauri } = useAppStore();
|
||||
const { isPinned, togglePin } = useTogglePin();
|
||||
|
||||
const isTauri = useAppStore((state) => state.isTauri);
|
||||
const historicalRecords = useShortcutsStore((state) => {
|
||||
return state.historicalRecords;
|
||||
});
|
||||
const newSession = useShortcutsStore((state) => {
|
||||
return state.newSession;
|
||||
});
|
||||
const fixedWindow = useShortcutsStore((state) => {
|
||||
return state.fixedWindow;
|
||||
});
|
||||
|
||||
const external = useShortcutsStore((state) => state.external);
|
||||
|
||||
const togglePin = async () => {
|
||||
try {
|
||||
const newPinned = !isPinned;
|
||||
await platformAdapter.setAlwaysOnTop(newPinned);
|
||||
setIsPinned(newPinned);
|
||||
} catch (err) {
|
||||
console.error("Failed to toggle window pin state:", err);
|
||||
setIsPinned(isPinned);
|
||||
}
|
||||
};
|
||||
const { historicalRecords, newSession, fixedWindow, external } =
|
||||
useShortcutsStore();
|
||||
|
||||
return (
|
||||
<header
|
||||
|
||||
@@ -51,13 +51,19 @@ export function ServerList({ clearChat }: ServerListProps) {
|
||||
|
||||
const fetchServers = useCallback(
|
||||
async (resetSelection: boolean) => {
|
||||
platformAdapter
|
||||
.commands("list_coco_servers")
|
||||
.then((res: any) => {
|
||||
const enabledServers = (res as IServer[]).filter(
|
||||
platformAdapter.commands("list_coco_servers").then((res: any) => {
|
||||
console.log("list_coco_servers", res);
|
||||
if (!Array.isArray(res)) {
|
||||
// If res is not an array, it might be an error message or something else.
|
||||
// Log it and don't proceed.
|
||||
// console.log("list_coco_servers did not return an array:", res);
|
||||
setServerList([]); // Clear the list or handle as appropriate
|
||||
return;
|
||||
}
|
||||
const enabledServers = (res as IServer[])?.filter(
|
||||
(server) => server.enabled && server.available
|
||||
);
|
||||
//console.log("list_coco_servers", enabledServers);
|
||||
|
||||
setServerList(enabledServers);
|
||||
|
||||
if (resetSelection && enabledServers.length > 0) {
|
||||
@@ -71,9 +77,6 @@ export function ServerList({ clearChat }: ServerListProps) {
|
||||
switchServer(enabledServers[enabledServers.length - 1]);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.error(err);
|
||||
});
|
||||
},
|
||||
[currentService?.id]
|
||||
@@ -147,7 +150,9 @@ export function ServerList({ clearChat }: ServerListProps) {
|
||||
}
|
||||
};
|
||||
|
||||
useKeyPress(["uparrow", "downarrow", "enter"], (event, key) => {
|
||||
useKeyPress(
|
||||
["uparrow", "downarrow", "enter"],
|
||||
(event, key) => {
|
||||
const isClose = isNil(serverListButtonRef.current?.dataset["open"]);
|
||||
const length = serverList.length;
|
||||
|
||||
@@ -157,7 +162,9 @@ export function ServerList({ clearChat }: ServerListProps) {
|
||||
event.preventDefault();
|
||||
|
||||
const currentIndex = serverList.findIndex((server) => {
|
||||
return server.id === (highlightId === '' ? currentService?.id : highlightId);
|
||||
return (
|
||||
server.id === (highlightId === "" ? currentService?.id : highlightId)
|
||||
);
|
||||
});
|
||||
|
||||
let nextIndex = currentIndex;
|
||||
@@ -176,9 +183,11 @@ export function ServerList({ clearChat }: ServerListProps) {
|
||||
serverListButtonRef.current?.click();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
},
|
||||
{
|
||||
target: popoverRef,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const handleMouseMove = useCallback(() => {
|
||||
setHighlightId("");
|
||||
@@ -199,7 +208,8 @@ export function ServerList({ clearChat }: ServerListProps) {
|
||||
|
||||
<PopoverPanel
|
||||
onMouseMove={handleMouseMove}
|
||||
className="absolute right-0 z-10 mt-2 min-w-[240px] bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700">
|
||||
className="absolute right-0 z-10 mt-2 min-w-[240px] bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<div className="p-3">
|
||||
<div className="flex items-center justify-between mb-3 whitespace-nowrap">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
@@ -221,7 +231,8 @@ export function ServerList({ clearChat }: ServerListProps) {
|
||||
>
|
||||
<VisibleKey shortcut="R" onKeyPress={handleRefresh}>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 text-[#0287FF] transition-transform duration-1000 ${isRefreshing ? "animate-spin" : ""
|
||||
className={`h-4 w-4 text-[#0287FF] transition-transform duration-1000 ${
|
||||
isRefreshing ? "animate-spin" : ""
|
||||
}`}
|
||||
/>
|
||||
</VisibleKey>
|
||||
@@ -235,7 +246,9 @@ export function ServerList({ clearChat }: ServerListProps) {
|
||||
key={server.id}
|
||||
onClick={() => switchServer(server)}
|
||||
className={`w-full flex items-center justify-between gap-1 p-2 rounded-lg transition-colors whitespace-nowrap
|
||||
${currentService?.id === server.id || highlightId === server.id
|
||||
${
|
||||
currentService?.id === server.id ||
|
||||
highlightId === server.id
|
||||
? "bg-gray-100 dark:bg-gray-800"
|
||||
: "hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||
}`}
|
||||
|
||||
@@ -43,6 +43,7 @@ export const QueryIntent = ({
|
||||
useEffect(() => {
|
||||
if (!ChunkData?.message_chunk) return;
|
||||
if (!loading) {
|
||||
try {
|
||||
const cleanContent = ChunkData.message_chunk.replace(/^"|"$/g, "");
|
||||
const allMatches = cleanContent.match(/<JSON>([\s\S]*?)<\/JSON>/g);
|
||||
if (allMatches) {
|
||||
@@ -55,6 +56,9 @@ export const QueryIntent = ({
|
||||
}
|
||||
setData(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to process message chunk in QueryIntent:", error);
|
||||
}
|
||||
}
|
||||
}, [ChunkData?.message_chunk, loading]);
|
||||
|
||||
@@ -79,14 +83,22 @@ export const QueryIntent = ({
|
||||
<>
|
||||
<Loader className="w-4 h-4 animate-spin text-[#1990FF]" />
|
||||
<span className="text-xs text-[#999999] italic">
|
||||
{t(`assistant.message.steps.${ChunkData?.chunk_type || Detail.type}`)}
|
||||
{t(
|
||||
`assistant.message.steps.${
|
||||
ChunkData?.chunk_type || Detail.type
|
||||
}`
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UnderstandIcon className="w-4 h-4 text-[#38C200]" />
|
||||
<span className="text-xs text-[#999999]">
|
||||
{t(`assistant.message.steps.${ChunkData?.chunk_type || Detail.type}`)}
|
||||
{t(
|
||||
`assistant.message.steps.${
|
||||
ChunkData?.chunk_type || Detail.type
|
||||
}`
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const UserMessage = ({ messageContent }: UserMessageProps) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex gap-1 items-center justify-end"
|
||||
className="max-w-full flex gap-1 items-center justify-end"
|
||||
onMouseEnter={() => setShowCopyButton(true)}
|
||||
onMouseLeave={() => setShowCopyButton(false)}
|
||||
>
|
||||
|
||||
@@ -176,13 +176,13 @@ export const ChatMessage = memo(function ChatMessage({
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"py-8 flex",
|
||||
"w-full py-8 flex",
|
||||
[isAssistant ? "justify-start" : "justify-end"],
|
||||
rootClassName
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={`px-4 flex gap-4 ${
|
||||
className={`w-full px-4 flex gap-4 ${
|
||||
isAssistant ? "w-full" : "flex-row-reverse"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -6,13 +6,9 @@ import { Sidebar } from "./Sidebar";
|
||||
import { Connect } from "./Connect";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import {
|
||||
list_coco_servers,
|
||||
add_coco_server,
|
||||
refresh_coco_server_info,
|
||||
} from "@/commands";
|
||||
import ServiceInfo from "./ServiceInfo";
|
||||
import ServiceAuth from "./ServiceAuth";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
|
||||
export default function Cloud() {
|
||||
const SidebarRef = useRef<{ refreshData: () => void }>(null);
|
||||
@@ -21,11 +17,8 @@ export default function Cloud() {
|
||||
|
||||
const [isConnect, setIsConnect] = useState(true);
|
||||
|
||||
const currentService = useConnectStore((state) => state.currentService);
|
||||
const setCurrentService = useConnectStore((state) => state.setCurrentService);
|
||||
|
||||
const serverList = useConnectStore((state) => state.serverList);
|
||||
const setServerList = useConnectStore((state) => state.setServerList);
|
||||
const { currentService, setCurrentService, serverList, setServerList } =
|
||||
useConnectStore();
|
||||
|
||||
const [refreshLoading, setRefreshLoading] = useState(false);
|
||||
|
||||
@@ -41,7 +34,8 @@ export default function Cloud() {
|
||||
}, [JSON.stringify(currentService)]);
|
||||
|
||||
const fetchServers = async (resetSelection: boolean) => {
|
||||
list_coco_servers()
|
||||
platformAdapter
|
||||
.commands("list_coco_servers")
|
||||
.then((res: any) => {
|
||||
if (errors.length > 0) {
|
||||
res = (res || []).map((item: any) => {
|
||||
@@ -54,7 +48,7 @@ export default function Cloud() {
|
||||
return item;
|
||||
});
|
||||
}
|
||||
// console.log("list_coco_servers", res);
|
||||
console.log("list_coco_servers", res);
|
||||
setServerList(res);
|
||||
|
||||
if (resetSelection && res.length > 0) {
|
||||
@@ -69,9 +63,6 @@ export default function Cloud() {
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
const addServer = (endpointLink: string) => {
|
||||
@@ -87,7 +78,8 @@ export default function Cloud() {
|
||||
|
||||
setRefreshLoading(true);
|
||||
|
||||
return add_coco_server(endpointLink)
|
||||
return platformAdapter
|
||||
.commands("add_coco_server", endpointLink)
|
||||
.then((res: any) => {
|
||||
// console.log("add_coco_server", res);
|
||||
fetchServers(false).then((r) => {
|
||||
@@ -103,7 +95,8 @@ export default function Cloud() {
|
||||
const refreshClick = useCallback(
|
||||
(id: string) => {
|
||||
setRefreshLoading(true);
|
||||
refresh_coco_server_info(id)
|
||||
platformAdapter
|
||||
.commands("refresh_coco_server_info", id)
|
||||
.then((res: any) => {
|
||||
console.log("refresh_coco_server_info", id, res);
|
||||
fetchServers(false).then((r) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { RefreshCcw } from "lucide-react";
|
||||
|
||||
import { DataSourceItem } from "./DataSourceItem";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { get_connectors_by_server, datasource_search } from "@/commands";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
|
||||
export function DataSourcesList({ server }: { server: string }) {
|
||||
const { t } = useTranslation();
|
||||
@@ -17,7 +17,8 @@ export function DataSourcesList({ server }: { server: string }) {
|
||||
function initServerAppData() {
|
||||
setRefreshLoading(true);
|
||||
// fetch connectors data
|
||||
get_connectors_by_server(server)
|
||||
platformAdapter
|
||||
.commands("get_connectors_by_server", server)
|
||||
.then((res: any) => {
|
||||
// console.log("get_connectors_by_server", res);
|
||||
setConnectorData(res, server);
|
||||
@@ -25,7 +26,8 @@ export function DataSourcesList({ server }: { server: string }) {
|
||||
.finally(() => {});
|
||||
|
||||
// fetch datasource data
|
||||
datasource_search({ id: server })
|
||||
platformAdapter
|
||||
.commands("datasource_search", { id: server })
|
||||
.then((res: any) => {
|
||||
// console.log("datasource_search", res);
|
||||
setDatasourceData(res, server);
|
||||
|
||||
@@ -2,7 +2,6 @@ import { FC, memo, useCallback, useEffect, useState } from "react";
|
||||
import { Copy } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import {
|
||||
getCurrent as getCurrentDeepLinkUrls,
|
||||
onOpenUrl,
|
||||
@@ -13,8 +12,9 @@ import { UserProfile } from "./UserProfile";
|
||||
import { OpenURLWithBrowser } from "@/utils";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { logout_coco_server, handle_sso_callback } from "@/commands";
|
||||
import { copyToClipboard } from "@/utils";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { handleLogout } from "@/commands/servers";
|
||||
|
||||
interface ServiceAuthProps {
|
||||
setRefreshLoading: (loading: boolean) => void;
|
||||
@@ -31,11 +31,6 @@ const ServiceAuth = memo(
|
||||
const addError = useAppStore((state) => state.addError);
|
||||
|
||||
const currentService = useConnectStore((state) => state.currentService);
|
||||
const setCurrentService = useConnectStore(
|
||||
(state) => state.setCurrentService
|
||||
);
|
||||
const serverList = useConnectStore((state) => state.serverList);
|
||||
const setServerList = useConnectStore((state) => state.setServerList);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -57,27 +52,18 @@ const ServiceAuth = memo(
|
||||
setLoading(true);
|
||||
}, [ssoRequestID, loading, currentService]);
|
||||
|
||||
const onLogout = useCallback(
|
||||
(id: string) => {
|
||||
const onLogout = useCallback((id: string) => {
|
||||
setRefreshLoading(true);
|
||||
logout_coco_server(id)
|
||||
platformAdapter
|
||||
.commands("logout_coco_server", id)
|
||||
.then((res: any) => {
|
||||
console.log("logout_coco_server", id, JSON.stringify(res));
|
||||
emit("login_or_logout", false);
|
||||
// update server profile
|
||||
setCurrentService({ ...currentService, profile: null });
|
||||
const updatedServerList = serverList.map((server) =>
|
||||
server.id === id ? { ...server, profile: null } : server
|
||||
);
|
||||
console.log("updatedServerList", updatedServerList);
|
||||
setServerList(updatedServerList);
|
||||
handleLogout(id);
|
||||
})
|
||||
.finally(() => {
|
||||
setRefreshLoading(false);
|
||||
});
|
||||
},
|
||||
[currentService, serverList]
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleOAuthCallback = useCallback(
|
||||
async (code: string | null, serverId: string | null) => {
|
||||
@@ -88,7 +74,7 @@ const ServiceAuth = memo(
|
||||
|
||||
try {
|
||||
console.log("Handling OAuth callback:", { code, serverId });
|
||||
await handle_sso_callback({
|
||||
await platformAdapter.commands("handle_sso_callback", {
|
||||
serverId: serverId, // Make sure 'server_id' is the correct argument
|
||||
requestId: ssoRequestID, // Make sure 'request_id' is the correct argument
|
||||
code: code,
|
||||
|
||||
@@ -7,7 +7,7 @@ import Tooltip from "@/components/Common/Tooltip";
|
||||
import SettingsToggle from "@/components/Settings/SettingsToggle";
|
||||
import { OpenURLWithBrowser } from "@/utils";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import { enable_server, disable_server, remove_coco_server } from "@/commands";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
|
||||
interface ServiceHeaderProps {
|
||||
refreshLoading?: boolean;
|
||||
@@ -27,9 +27,9 @@ const ServiceHeader = memo(
|
||||
const enable_coco_server = useCallback(
|
||||
async (enabled: boolean) => {
|
||||
if (enabled) {
|
||||
await enable_server(currentService?.id);
|
||||
await platformAdapter.commands("enable_server", currentService?.id);
|
||||
} else {
|
||||
await disable_server(currentService?.id);
|
||||
await platformAdapter.commands("disable_server", currentService?.id);
|
||||
}
|
||||
|
||||
setCurrentService({ ...currentService, enabled });
|
||||
@@ -40,7 +40,7 @@ const ServiceHeader = memo(
|
||||
);
|
||||
|
||||
const removeServer = (id: string) => {
|
||||
remove_coco_server(id).then((res: any) => {
|
||||
platformAdapter.commands("remove_coco_server", id).then((res: any) => {
|
||||
console.log("remove_coco_server", id, JSON.stringify(res));
|
||||
fetchServers(true).then((r) => {
|
||||
console.log("fetchServers", r);
|
||||
|
||||
@@ -6,12 +6,14 @@ import { useAppStore } from "@/stores/appStore";
|
||||
interface ErrorNotificationProps {
|
||||
duration?: number;
|
||||
autoClose?: boolean;
|
||||
isTauri?: boolean;
|
||||
}
|
||||
|
||||
const ErrorNotification = ({
|
||||
duration = 3000,
|
||||
autoClose = true
|
||||
}: ErrorNotificationProps) => {
|
||||
autoClose = true,
|
||||
isTauri = true,
|
||||
}: ErrorNotificationProps) => {
|
||||
const errors = useAppStore((state) => state.errors);
|
||||
const removeError = useAppStore((state) => state.removeError);
|
||||
|
||||
@@ -33,7 +35,11 @@ const ErrorNotification = ({
|
||||
if (errors.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-10 right-4 z-50 max-w-[calc(100%-32px)] space-y-2">
|
||||
<div
|
||||
className={`${
|
||||
isTauri ? "fixed" : "absolute"
|
||||
} bottom-10 right-4 z-50 max-w-[calc(100%-32px)] space-y-2`}
|
||||
>
|
||||
{errors.map((error) => (
|
||||
<div
|
||||
key={error.id}
|
||||
|
||||
@@ -107,6 +107,8 @@ const HistoryListItem: FC<HistoryListItemProps> = ({
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter") return;
|
||||
|
||||
event.stopPropagation();
|
||||
|
||||
const value = event.currentTarget.value;
|
||||
|
||||
onRename(item._id || "", value);
|
||||
|
||||
@@ -19,6 +19,7 @@ import source_default_dark_img from "@/assets/images/source_default_dark.png";
|
||||
import { useThemeStore } from "@/stores/themeStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import FontIcon from "../Icons/FontIcon";
|
||||
import { useTogglePin } from "@/hooks/useTogglePin";
|
||||
|
||||
interface FooterProps {
|
||||
setIsPinnedWeb?: (value: boolean) => void;
|
||||
@@ -37,28 +38,16 @@ export default function Footer({ setIsPinnedWeb }: FooterProps) {
|
||||
|
||||
const isDark = useThemeStore((state) => state.isDark);
|
||||
|
||||
const { isTauri, isPinned, setIsPinned } = useAppStore();
|
||||
const { isTauri } = useAppStore();
|
||||
|
||||
const { isPinned, togglePin } = useTogglePin({
|
||||
onPinChange: setIsPinnedWeb,
|
||||
});
|
||||
|
||||
const { setVisible, updateInfo } = useUpdateStore();
|
||||
|
||||
const { fixedWindow, modifierKey } = useShortcutsStore();
|
||||
|
||||
const setWindowAlwaysOnTop = useCallback(async (isPinned: boolean) => {
|
||||
setIsPinnedWeb?.(isPinned);
|
||||
return platformAdapter.setAlwaysOnTop(isPinned);
|
||||
}, []);
|
||||
|
||||
const togglePin = async () => {
|
||||
try {
|
||||
const newPinned = !isPinned;
|
||||
await setWindowAlwaysOnTop(newPinned);
|
||||
setIsPinned(newPinned);
|
||||
} catch (err) {
|
||||
console.error("Failed to toggle window pin state:", err);
|
||||
setIsPinned(isPinned);
|
||||
}
|
||||
};
|
||||
|
||||
const openSetting = useCallback(() => {
|
||||
return platformAdapter.emitEvent("open_settings", "");
|
||||
}, []);
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
useReactive,
|
||||
useUnmount,
|
||||
} from "ahooks";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { FC, useEffect, useRef, useState } from "react";
|
||||
import { noop } from "lodash-es";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
@@ -17,6 +17,10 @@ import { useAppStore } from "@/stores/appStore";
|
||||
import { useExtensionsStore } from "@/stores/extensionsStore";
|
||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||
|
||||
interface AskAiProps {
|
||||
isChatMode: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
serverId?: string;
|
||||
assistantId?: string;
|
||||
@@ -24,7 +28,9 @@ interface State {
|
||||
copyButtonId: string;
|
||||
}
|
||||
|
||||
const AskAi = () => {
|
||||
const AskAi: FC<AskAiProps> = (props) => {
|
||||
const { isChatMode } = props;
|
||||
|
||||
const {
|
||||
askAiMessage,
|
||||
setGoAskAi,
|
||||
@@ -162,7 +168,7 @@ const AskAi = () => {
|
||||
useAsyncEffect(async () => {
|
||||
if (!askAiMessage || !state.serverId || !state.assistantId) return;
|
||||
|
||||
clearAllChunkData();
|
||||
await clearAllChunkData();
|
||||
|
||||
const { serverId, assistantId } = state;
|
||||
|
||||
@@ -186,7 +192,7 @@ const AskAi = () => {
|
||||
useKeyPress(
|
||||
`${modifierKey}.enter`,
|
||||
async () => {
|
||||
if (isTyping) return;
|
||||
if (isChatMode || isTyping) return;
|
||||
|
||||
const { serverId, assistantId } = state;
|
||||
|
||||
@@ -204,7 +210,7 @@ const AskAi = () => {
|
||||
useKeyPress(
|
||||
"enter",
|
||||
() => {
|
||||
if (isTyping || !state.copyButtonId) return;
|
||||
if (isChatMode || isTyping || !state.copyButtonId) return;
|
||||
|
||||
const copyButton = document.getElementById(state.copyButtonId);
|
||||
|
||||
|
||||
@@ -134,10 +134,18 @@ export function useAssistantManager({
|
||||
return handleAskAi();
|
||||
}
|
||||
|
||||
if (key === "Enter" && !shiftKey && !isChatMode && isTauri) {
|
||||
if (key === "Enter" && !shiftKey) {
|
||||
e.preventDefault();
|
||||
|
||||
goAskAi ? handleAskAi() : handleSubmit();
|
||||
if (isTauri && !isChatMode && goAskAi) {
|
||||
if (!isEmpty(value)) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
return handleAskAi();
|
||||
}
|
||||
|
||||
handleSubmit();
|
||||
}
|
||||
},
|
||||
[
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { useBoolean, useDebounceFn } from "ahooks";
|
||||
import { useBoolean } from "ahooks";
|
||||
import {
|
||||
useRef,
|
||||
useImperativeHandle,
|
||||
forwardRef,
|
||||
KeyboardEvent,
|
||||
useEffect,
|
||||
useCallback,
|
||||
ChangeEvent,
|
||||
useRef,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const LINE_HEIGHT = 24; // 1.5rem
|
||||
const MAX_FIRST_LINE_WIDTH = 470; // Width in pixels for first line
|
||||
const MAX_HEIGHT = 240; // 15rem
|
||||
const MAX_HEIGHT = 240;
|
||||
|
||||
interface AutoResizeTextareaProps {
|
||||
isChatMode: boolean;
|
||||
@@ -21,6 +20,7 @@ interface AutoResizeTextareaProps {
|
||||
chatPlaceholder?: string;
|
||||
lineCount?: number;
|
||||
onLineCountChange?: (lineCount: number) => void;
|
||||
firstLineMaxWidth: number;
|
||||
}
|
||||
|
||||
// Forward ref to allow parent to interact with this component
|
||||
@@ -35,87 +35,15 @@ const AutoResizeTextarea = forwardRef<
|
||||
setInput,
|
||||
handleKeyDown,
|
||||
chatPlaceholder,
|
||||
lineCount = 1,
|
||||
onLineCountChange,
|
||||
firstLineMaxWidth,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [isComposition, { setTrue, setFalse }] = useBoolean();
|
||||
|
||||
// Memoize resize logic
|
||||
const { run: debouncedResize } = useDebounceFn(
|
||||
() => {
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) return;
|
||||
if (typeof window === "undefined" || typeof document === "undefined")
|
||||
return;
|
||||
|
||||
// Reset height to auto to get the correct scrollHeight
|
||||
textarea.style.height = "auto";
|
||||
|
||||
// Create a hidden span to measure first line width
|
||||
const span = document.createElement("span");
|
||||
span.style.visibility = "hidden";
|
||||
span.style.position = "absolute";
|
||||
span.style.whiteSpace = "pre";
|
||||
span.style.font = window.getComputedStyle(textarea).font;
|
||||
|
||||
// Get first line content
|
||||
const content = textarea.value;
|
||||
const firstLineEnd =
|
||||
content.indexOf("\n") === -1 ? content.length : content.indexOf("\n");
|
||||
span.textContent = content.slice(0, firstLineEnd);
|
||||
document.body.appendChild(span);
|
||||
|
||||
// Calculate lines based on first line width
|
||||
const firstLineWidth = span.offsetWidth;
|
||||
document.body.removeChild(span);
|
||||
|
||||
// Start with 1 line
|
||||
let lines = 1;
|
||||
|
||||
// Add a line if first line exceeds max width
|
||||
if (firstLineWidth > MAX_FIRST_LINE_WIDTH) {
|
||||
lines += 1;
|
||||
}
|
||||
|
||||
// Add lines based on scrollHeight for remaining content
|
||||
const scrollHeight = textarea.scrollHeight;
|
||||
const remainingLines = Math.floor(
|
||||
(scrollHeight - LINE_HEIGHT) / LINE_HEIGHT
|
||||
);
|
||||
lines += Math.max(0, remainingLines);
|
||||
|
||||
// Calculate final height
|
||||
const newHeight = Math.min(lines * LINE_HEIGHT, MAX_HEIGHT);
|
||||
|
||||
// Only update if height actually changed
|
||||
if (textarea.style.height !== `${newHeight}px`) {
|
||||
textarea.style.height = `${newHeight}px`;
|
||||
onLineCountChange?.(lines);
|
||||
}
|
||||
},
|
||||
{ wait: 100 }
|
||||
);
|
||||
|
||||
// Handle input changes and initial setup
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
debouncedResize();
|
||||
}
|
||||
}, [input, debouncedResize]);
|
||||
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
requestAnimationFrame(() => {
|
||||
// Set cursor position to end
|
||||
const length = textareaRef.current?.value.length || 0;
|
||||
textareaRef.current?.setSelectionRange(length, length);
|
||||
});
|
||||
}
|
||||
}, [lineCount]);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const calcRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Expose methods to the parent via ref
|
||||
useImperativeHandle(ref, () => ({
|
||||
@@ -135,14 +63,46 @@ const AutoResizeTextarea = forwardRef<
|
||||
handleKeyDown?.(event);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const textarea = textareaRef.current;
|
||||
|
||||
if (!textarea || !calcRef.current) return;
|
||||
|
||||
if (!calcRef.current) return;
|
||||
|
||||
textarea.style.height = "auto";
|
||||
|
||||
const computedStyle = getComputedStyle(textarea);
|
||||
const lineHeight = parseInt(computedStyle.lineHeight);
|
||||
let height = lineHeight;
|
||||
let minHeight = lineHeight;
|
||||
const hasNewline = /[\r\n]/.test(input);
|
||||
const firstLineExceeds =
|
||||
calcRef.current?.offsetWidth >= firstLineMaxWidth - 32;
|
||||
|
||||
if (hasNewline || firstLineExceeds) {
|
||||
minHeight = lineHeight * 2;
|
||||
height = Math.min(
|
||||
Math.max(minHeight, textarea.scrollHeight),
|
||||
MAX_HEIGHT
|
||||
);
|
||||
}
|
||||
|
||||
textarea.style.height = `${height}px`;
|
||||
textarea.style.minHeight = `${minHeight}px`;
|
||||
|
||||
onLineCountChange?.(height / lineHeight);
|
||||
}, [input, firstLineMaxWidth]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setInput(e.target.value);
|
||||
(event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setInput(event.currentTarget.value);
|
||||
},
|
||||
[setInput]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
id={isChatMode ? "chat-textarea" : "search-textarea"}
|
||||
@@ -150,7 +110,7 @@ const AutoResizeTextarea = forwardRef<
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
spellCheck="false"
|
||||
className="text-base flex-1 outline-none w-full min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent custom-scrollbar"
|
||||
className="text-base flex-1 outline-none w-full min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent custom-scrollbar resize-none overflow-y-auto"
|
||||
placeholder={chatPlaceholder || t("search.textarea.placeholder")}
|
||||
aria-label={t("search.textarea.ariaLabel")}
|
||||
value={input}
|
||||
@@ -161,14 +121,12 @@ const AutoResizeTextarea = forwardRef<
|
||||
setTimeout(setFalse, 0);
|
||||
}}
|
||||
rows={1}
|
||||
style={{
|
||||
resize: "none", // Prevent manual resize
|
||||
overflow: "auto",
|
||||
minHeight: "1.5rem",
|
||||
maxHeight: "13.5rem", // Limit height to 9 rows (9 * 1.5 line-height)
|
||||
lineHeight: "1.5rem", // Line height to match row height
|
||||
}}
|
||||
/>
|
||||
|
||||
<div ref={calcRef} className="absolute whitespace-nowrap -z-10">
|
||||
{input}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -142,7 +142,9 @@ const ContextMenu = ({ formatUrl }: ContextMenuProps) => {
|
||||
type === "AI Assistant" ||
|
||||
id === "Extension Store",
|
||||
clickEvent() {
|
||||
copyToClipboard(formatUrl && formatUrl(selectedSearchContent) || url);
|
||||
copyToClipboard(
|
||||
(formatUrl && formatUrl(selectedSearchContent)) || url
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -169,7 +171,7 @@ const ContextMenu = ({ formatUrl }: ContextMenuProps) => {
|
||||
name: t("search.contextMenu.copyQuestionAndAnswer"),
|
||||
icon: <Copy />,
|
||||
keys: isMac ? ["⌘", "L"] : ["Ctrl", "L"],
|
||||
shortcut: isMac ? "meta.l" : "ctrl+l",
|
||||
shortcut: isMac ? "meta.l" : "ctrl.l",
|
||||
hide: category !== "Calculator",
|
||||
clickEvent() {
|
||||
copyToClipboard(`${query.value} = ${result.value}`);
|
||||
@@ -198,7 +200,7 @@ const ContextMenu = ({ formatUrl }: ContextMenuProps) => {
|
||||
}
|
||||
}, [selectedSearchContent]);
|
||||
|
||||
useOSKeyPress(["meta.k", "ctrl+k"], () => {
|
||||
useOSKeyPress(["meta.k", "ctrl.k"], () => {
|
||||
if (isNil(selectedSearchContent) && isNil(selectedExtension)) return;
|
||||
|
||||
setVisibleContextMenu(!visibleContextMenu);
|
||||
@@ -224,19 +226,29 @@ const ContextMenu = ({ formatUrl }: ContextMenuProps) => {
|
||||
}
|
||||
});
|
||||
|
||||
useOSKeyPress(shortcuts, (_, key) => {
|
||||
useOSKeyPress(
|
||||
shortcuts,
|
||||
(event, key) => {
|
||||
if (!visibleContextMenu) return;
|
||||
|
||||
event.stopPropagation();
|
||||
|
||||
let matched;
|
||||
|
||||
if (key === "enter") {
|
||||
matched = searchMenus.find((_, index) => index === state.activeMenuIndex);
|
||||
matched = searchMenus.find((_, index) => {
|
||||
return index === state.activeMenuIndex;
|
||||
});
|
||||
} else {
|
||||
matched = searchMenus.find((item) => item.shortcut === key);
|
||||
}
|
||||
|
||||
handleClick(matched?.clickEvent);
|
||||
});
|
||||
},
|
||||
{
|
||||
target: document.body,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setOpenPopover(visibleContextMenu);
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useConnectStore } from "@/stores/connectStore";
|
||||
import SearchEmpty from "../Common/SearchEmpty";
|
||||
import { Data } from "ahooks/lib/useInfiniteScroll/types";
|
||||
import { nanoid } from "nanoid";
|
||||
import { isNil } from "lodash-es";
|
||||
|
||||
interface DocumentListProps {
|
||||
onSelectDocument: (id: string) => void;
|
||||
@@ -48,13 +49,29 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const [isKeyboardMode, setIsKeyboardMode] = useState(false);
|
||||
const taskIdRef = useRef(nanoid());
|
||||
const [data, setData] = useState<Data>();
|
||||
const [data, setData] = useState<Data>({ list: [] });
|
||||
|
||||
const loadingFromRef = useRef<number>(-1);
|
||||
|
||||
const querySourceTimeoutRef = useRef(querySourceTimeout);
|
||||
useEffect(() => {
|
||||
querySourceTimeoutRef.current = querySourceTimeout;
|
||||
}, [querySourceTimeout]);
|
||||
|
||||
const setSelectedSearchContent = useSearchStore((state) => {
|
||||
return state.setSelectedSearchContent;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isNil(selectedItem)) return;
|
||||
|
||||
const hit = data.list[selectedItem];
|
||||
|
||||
const item = { ...hit?.document, querySource: hit?.source };
|
||||
|
||||
setSelectedSearchContent(item);
|
||||
}, [selectedItem, data]);
|
||||
|
||||
const getData = async (taskId: string, data?: Data) => {
|
||||
const from = data?.list?.length || 0;
|
||||
|
||||
@@ -108,27 +125,56 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
||||
}
|
||||
|
||||
console.log("_docs", from, queryStrings, response);
|
||||
const list = response?.hits || [];
|
||||
const total = response?.total_hits || 0;
|
||||
setTotal(total);
|
||||
const list = response?.hits ?? [];
|
||||
const allTotal = response?.total_hits ?? 0;
|
||||
// set first select hover
|
||||
if (from === 0 && list.length > 0) {
|
||||
setSelectedItem(0);
|
||||
getDocDetail(list[0]?.document);
|
||||
}
|
||||
|
||||
if (taskId === taskIdRef.current) {
|
||||
setData({ list });
|
||||
// Prevent the last data from being 0
|
||||
setTotal((prevTotal) => {
|
||||
if (list.length === 0) {
|
||||
return data?.list?.length === 0 ? 0 : prevTotal;
|
||||
}
|
||||
return allTotal;
|
||||
});
|
||||
setData((prev) => ({
|
||||
...prev,
|
||||
list: prev.list.concat(list),
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
list: list,
|
||||
hasMore: list.length === PAGE_SIZE && from + list.length < total,
|
||||
hasMore: list.length === PAGE_SIZE && from + list.length < allTotal,
|
||||
};
|
||||
};
|
||||
|
||||
const { loading } = useInfiniteScroll(
|
||||
(data) => {
|
||||
const taskId = nanoid();
|
||||
// Prevent repeated requests for the same from value
|
||||
const currentFrom = data?.list?.length || 0;
|
||||
|
||||
// If it starts from 0, it means it is a new search, reset the anti-duplicate flag
|
||||
if (currentFrom === 0) {
|
||||
loadingFromRef.current = -1;
|
||||
}
|
||||
|
||||
if (loadingFromRef.current === currentFrom) {
|
||||
return Promise.resolve({ list: [], hasMore: false });
|
||||
}
|
||||
|
||||
loadingFromRef.current = currentFrom;
|
||||
|
||||
const taskId = nanoid();
|
||||
taskIdRef.current = taskId;
|
||||
|
||||
return getData(taskId, data);
|
||||
return getData(taskId, data).finally(() => {
|
||||
loadingFromRef.current = -1; // reset
|
||||
});
|
||||
},
|
||||
{
|
||||
target: containerRef,
|
||||
@@ -160,6 +206,15 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
||||
setIsKeyboardMode(false);
|
||||
}, [isChatMode, input]);
|
||||
|
||||
useEffect(() => {
|
||||
setTotal(0);
|
||||
setData((prev) => ({
|
||||
...prev,
|
||||
list: [],
|
||||
}));
|
||||
loadingFromRef.current = -1;
|
||||
}, [input]);
|
||||
|
||||
const { visibleContextMenu } = useSearchStore();
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
|
||||
@@ -80,7 +80,6 @@ const ExtensionStore = () => {
|
||||
setSelectedExtension,
|
||||
installingExtensions,
|
||||
setInstallingExtensions,
|
||||
uninstallingExtensions,
|
||||
setUninstallingExtensions,
|
||||
visibleExtensionDetail,
|
||||
setVisibleExtensionDetail,
|
||||
@@ -149,11 +148,7 @@ const ExtensionStore = () => {
|
||||
useKeyPress(
|
||||
`${modifierKey}.enter`,
|
||||
() => {
|
||||
if (
|
||||
visibleContextMenu ||
|
||||
visibleExtensionDetail ||
|
||||
selectedExtension?.installed
|
||||
) {
|
||||
if (visibleContextMenu || visibleExtensionDetail) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -181,42 +176,49 @@ const ExtensionStore = () => {
|
||||
setSelectedExtension(list[nextIndex]);
|
||||
});
|
||||
|
||||
const toggleInstall = (installed = true) => {
|
||||
if (!selectedExtension) return;
|
||||
const toggleInstall = (extension: SearchExtensionItem) => {
|
||||
if (!extension) return;
|
||||
|
||||
const { id } = selectedExtension;
|
||||
const { id, installed } = extension;
|
||||
|
||||
setList((prev) => {
|
||||
return prev.map((item) => {
|
||||
if (item.id === id) {
|
||||
return { ...item, installed };
|
||||
return { ...item, installed: !installed };
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
});
|
||||
|
||||
const { selectedExtension } = useSearchStore.getState();
|
||||
|
||||
if (selectedExtension?.id === id) {
|
||||
setSelectedExtension({
|
||||
...selectedExtension,
|
||||
installed,
|
||||
installed: !installed,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleInstall = async () => {
|
||||
const { selectedExtension, installingExtensions } =
|
||||
useSearchStore.getState();
|
||||
|
||||
if (!selectedExtension) return;
|
||||
|
||||
const { id, name, installed } = selectedExtension;
|
||||
|
||||
try {
|
||||
if (installed || installingExtensions.includes(id)) return;
|
||||
|
||||
try {
|
||||
setInstallingExtensions(installingExtensions.concat(id));
|
||||
|
||||
await platformAdapter.invokeBackend("install_extension_from_store", { id });
|
||||
await platformAdapter.invokeBackend("install_extension_from_store", {
|
||||
id,
|
||||
});
|
||||
|
||||
toggleInstall();
|
||||
toggleInstall(selectedExtension);
|
||||
|
||||
addError(
|
||||
`${name} ${t("extensionStore.hints.installationCompleted")}`,
|
||||
@@ -225,6 +227,8 @@ const ExtensionStore = () => {
|
||||
} catch (error) {
|
||||
addError(String(error), "error");
|
||||
} finally {
|
||||
const { installingExtensions } = useSearchStore.getState();
|
||||
|
||||
setInstallingExtensions(
|
||||
installingExtensions.filter((item) => item !== id)
|
||||
);
|
||||
@@ -232,13 +236,16 @@ const ExtensionStore = () => {
|
||||
};
|
||||
|
||||
const handleUnInstall = async () => {
|
||||
const { selectedExtension, uninstallingExtensions } =
|
||||
useSearchStore.getState();
|
||||
|
||||
if (!selectedExtension) return;
|
||||
|
||||
const { id, name, installed, developer } = selectedExtension;
|
||||
|
||||
try {
|
||||
if (!installed || uninstallingExtensions.includes(id)) return;
|
||||
|
||||
try {
|
||||
setUninstallingExtensions(uninstallingExtensions.concat(id));
|
||||
|
||||
await platformAdapter.invokeBackend("uninstall_extension", {
|
||||
@@ -246,7 +253,7 @@ const ExtensionStore = () => {
|
||||
extensionId: id,
|
||||
});
|
||||
|
||||
toggleInstall(false);
|
||||
toggleInstall(selectedExtension);
|
||||
|
||||
addError(
|
||||
`${name} ${t("extensionStore.hints.uninstallationCompleted")}`,
|
||||
@@ -255,6 +262,8 @@ const ExtensionStore = () => {
|
||||
} catch (error) {
|
||||
addError(String(error), "error");
|
||||
} finally {
|
||||
const { uninstallingExtensions } = useSearchStore.getState();
|
||||
|
||||
setUninstallingExtensions(
|
||||
uninstallingExtensions.filter((item) => item !== id)
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useKeyPress } from "ahooks";
|
||||
import { useKeyPress, useSize } from "ahooks";
|
||||
import clsx from "clsx";
|
||||
|
||||
import AutoResizeTextarea from "./AutoResizeTextarea";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
@@ -128,11 +129,7 @@ export default function ChatInput({
|
||||
}
|
||||
}, [inputValue, disabled, onSend]);
|
||||
|
||||
useKeyboardHandlers({
|
||||
isChatMode,
|
||||
handleSubmit,
|
||||
curChatEnd,
|
||||
});
|
||||
useKeyboardHandlers();
|
||||
|
||||
useKeyPress(`${modifierKey}.${returnToInput}`, handleToggleFocus);
|
||||
|
||||
@@ -203,21 +200,30 @@ export default function ChatInput({
|
||||
|
||||
const { currentService } = useConnectStore();
|
||||
const [visibleAudioInput, setVisibleAudioInput] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const containerSize = useSize(containerRef);
|
||||
const searchIconRef = useRef<HTMLDivElement>(null);
|
||||
const searchIconSize = useSize(searchIconRef);
|
||||
const extraIconRef = useRef<HTMLDivElement>(null);
|
||||
const extraIconSize = useSize(extraIconRef);
|
||||
|
||||
useEffect(() => {
|
||||
setVisibleAudioInput(isDefaultServer());
|
||||
}, [currentService]);
|
||||
|
||||
const renderSearchIcon = () => (
|
||||
<div ref={searchIconRef} className="w-fit">
|
||||
<SearchIcons
|
||||
lineCount={lineCount}
|
||||
isChatMode={isChatMode}
|
||||
assistant={askAIRef.current}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderExtraIcon = () => (
|
||||
<div className="flex items-center gap-2">
|
||||
<div ref={extraIconRef} className="flex items-center gap-2 w-fit">
|
||||
{isChatMode && (
|
||||
<ChatIcons
|
||||
lineCount={lineCount}
|
||||
isChatMode={isChatMode}
|
||||
@@ -226,6 +232,7 @@ export default function ChatInput({
|
||||
onSend={onSend}
|
||||
disabledChange={disabledChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isChatMode &&
|
||||
(sourceData || visibleExtensionStore || selectedExtension) && (
|
||||
@@ -295,7 +302,8 @@ export default function ChatInput({
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderTextarea = () => (
|
||||
const renderTextarea = () => {
|
||||
return (
|
||||
<VisibleKey
|
||||
shortcut={returnToInput}
|
||||
rootClassName="flex-1 flex items-center justify-center"
|
||||
@@ -316,35 +324,42 @@ export default function ChatInput({
|
||||
}
|
||||
lineCount={lineCount}
|
||||
onLineCountChange={setLineCount}
|
||||
firstLineMaxWidth={
|
||||
(containerSize?.width ?? 0) -
|
||||
(searchIconSize?.width ?? 0) -
|
||||
(extraIconSize?.width ?? 0)
|
||||
}
|
||||
/>
|
||||
</VisibleKey>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`w-full relative`}>
|
||||
<div
|
||||
className={`p-2 flex items-center dark:text-[#D8D8D8] bg-[#ededed] dark:bg-[#202126] rounded-md transition-all relative overflow-hidden`}
|
||||
>
|
||||
{lineCount === 1 ? (
|
||||
<div className="relative flex items-center gap-2 w-full">
|
||||
{renderSearchIcon()}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx("relative w-full", {
|
||||
"flex items-center gap-2": lineCount === 1,
|
||||
})}
|
||||
>
|
||||
{lineCount === 1 && renderSearchIcon()}
|
||||
|
||||
{renderTextarea()}
|
||||
|
||||
{renderExtraIcon()}
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative w-full">
|
||||
{renderTextarea()}
|
||||
{lineCount === 1 && renderExtraIcon()}
|
||||
|
||||
{lineCount > 1 && (
|
||||
<div className="flex items-center mt-2">
|
||||
<div className="flex-1">{renderSearchIcon()}</div>
|
||||
|
||||
<div className="self-end">{renderExtraIcon()}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<InputControls
|
||||
isChatMode={isChatMode}
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function MCPPopover({
|
||||
query: debouncedKeyword,
|
||||
});
|
||||
|
||||
console.log("getMCPByServer", res);
|
||||
// console.log("getMCPByServer", res);
|
||||
|
||||
if (res?.length === 0) {
|
||||
setDataList([]);
|
||||
|
||||
@@ -78,7 +78,7 @@ const SearchResultsPanel = memo<{
|
||||
}, [visibleExtensionStore, visibleExtensionDetail]);
|
||||
|
||||
if (visibleExtensionStore) return <ExtensionStore />;
|
||||
if (goAskAi) return <AskAi />;
|
||||
if (goAskAi) return <AskAi isChatMode={isChatMode} />;
|
||||
if (suggests.length === 0) return <NoResults />;
|
||||
|
||||
return sourceData ? (
|
||||
@@ -127,7 +127,7 @@ function Search({
|
||||
|
||||
<Footer setIsPinnedWeb={setIsPinned} />
|
||||
|
||||
<ContextMenu formatUrl={formatUrl}/>
|
||||
<ContextMenu formatUrl={formatUrl} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export default function SearchPopover({
|
||||
}
|
||||
);
|
||||
|
||||
console.log("getDataSourcesByServer", res);
|
||||
//console.log("getDataSourcesByServer", res);
|
||||
|
||||
if (res?.length === 0) {
|
||||
setDataSourceList([]);
|
||||
|
||||
@@ -8,6 +8,7 @@ import VisibleKey from "@/components/Common/VisibleKey";
|
||||
import source_default_img from "@/assets/images/source_default.png";
|
||||
import source_default_dark_img from "@/assets/images/source_default_dark.png";
|
||||
import type { QueryHits } from "@/types/search";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
|
||||
interface SearchSourceProps {
|
||||
sourceName: string;
|
||||
@@ -29,6 +30,8 @@ export const SearchSource: React.FC<SearchSourceProps> = ({
|
||||
items[0]?.document.category === "Calculator" ||
|
||||
items[0]?.document.category === "AI Overview";
|
||||
|
||||
const isTauri = useAppStore((state) => state.isTauri);
|
||||
|
||||
return (
|
||||
<div className="p-2 text-xs text-[#999] dark:text-[#666] flex items-center gap-2.5 relative">
|
||||
<CommonIcon
|
||||
@@ -38,7 +41,7 @@ export const SearchSource: React.FC<SearchSourceProps> = ({
|
||||
defaultIcon={isDark ? source_default_dark_img : source_default_img}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
{sourceName} {items[0]?.source?.name && `- ${items[0].source.name}`}
|
||||
{sourceName} {isTauri && items[0]?.source?.name && `- ${items[0].source.name}`}
|
||||
<div className="flex-1 border-b border-b-[#e6e6e6] dark:border-b-[#272626]"></div>
|
||||
{!hideArrow && (
|
||||
<>
|
||||
|
||||
@@ -125,7 +125,7 @@ function SearchChat({
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
await initializeListeners_auth();
|
||||
await platformAdapter.invokeBackend("get_app_search_source");
|
||||
await platformAdapter.commands("get_app_search_source");
|
||||
};
|
||||
|
||||
init();
|
||||
@@ -271,6 +271,7 @@ function SearchChat({
|
||||
<ChatAI
|
||||
ref={chatAIRef}
|
||||
key="ChatAI"
|
||||
instanceId="search-chat"
|
||||
changeInput={setInput}
|
||||
isSearchActive={isSearchActive}
|
||||
isDeepThinkActive={isDeepThinkActive}
|
||||
|
||||
@@ -28,7 +28,7 @@ interface UpdateAppProps {
|
||||
|
||||
const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
|
||||
const { t } = useTranslation();
|
||||
const isDark = useThemeStore((state) => state.isDark);
|
||||
const { isDark } = useThemeStore();
|
||||
const {
|
||||
visible,
|
||||
setVisible,
|
||||
@@ -38,14 +38,14 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
|
||||
updateInfo,
|
||||
setUpdateInfo,
|
||||
} = useUpdateStore();
|
||||
const addError = useAppStore((state) => state.addError);
|
||||
const snapshotUpdate = useAppearanceStore((state) => state.snapshotUpdate);
|
||||
const { addError } = useAppStore();
|
||||
const { snapshotUpdate } = useAppearanceStore();
|
||||
|
||||
const checkUpdate = useCallback(async () => {
|
||||
const checkUpdate = useCallback(() => {
|
||||
return platformAdapter.checkUpdate();
|
||||
}, []);
|
||||
|
||||
const relaunchApp = useCallback(async () => {
|
||||
const relaunchApp = useCallback(() => {
|
||||
return platformAdapter.relaunchApp();
|
||||
}, []);
|
||||
|
||||
@@ -57,6 +57,18 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
|
||||
});
|
||||
}, [snapshotUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = platformAdapter.listenEvent("check-update", () => {
|
||||
if (!isCheckPage) return;
|
||||
|
||||
checkUpdateStatus();
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten.then((fn) => fn());
|
||||
};
|
||||
}, []);
|
||||
|
||||
const state = useReactive<State>({ download: 0 });
|
||||
|
||||
useInterval(() => checkUpdateStatus(), 1000 * 60 * 60 * 24, {
|
||||
@@ -198,7 +210,9 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
|
||||
) : (
|
||||
<div className={clsx("text-xs text-[#999]", cursorClassName)}>
|
||||
{t("update.latest", {
|
||||
replace: [updateInfo?.version || process.env.VERSION || "N/A"],
|
||||
replace: [
|
||||
updateInfo?.version || process.env.VERSION || "N/A",
|
||||
],
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState, useRef } from "react";
|
||||
import { useCallback, useEffect, useState, useRef, useMemo } from "react";
|
||||
|
||||
import type { Chat } from "@/types/chat";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
@@ -14,11 +14,14 @@ export function useChatActions(
|
||||
setActiveChat: (chat: Chat | undefined) => void,
|
||||
setCurChatEnd: (value: boolean) => void,
|
||||
setTimedoutShow: (value: boolean) => void,
|
||||
clearAllChunkData: () => void,
|
||||
clearAllChunkData: () => Promise<void>,
|
||||
setQuestion: (value: string) => void,
|
||||
curIdRef: React.MutableRefObject<string>,
|
||||
curSessionIdRef: React.MutableRefObject<string>,
|
||||
setChats: (chats: Chat[]) => void,
|
||||
dealMsgRef: React.MutableRefObject<((msg: string) => void) | null>,
|
||||
setLoadingStep: (loading: Record<string, boolean>) => void,
|
||||
isChatPage?: boolean,
|
||||
isSearchActive?: boolean,
|
||||
isDeepThinkActive?: boolean,
|
||||
isMCPActive?: boolean,
|
||||
@@ -40,6 +43,23 @@ export function useChatActions(
|
||||
|
||||
const [keyword, setKeyword] = useState("");
|
||||
|
||||
// Add a ref at the beginning of the useChatActions function to store the listener.
|
||||
const unlistenersRef = useRef<{
|
||||
message?: () => void;
|
||||
chatMessage?: () => void;
|
||||
error?: () => void;
|
||||
}>({});
|
||||
|
||||
const cleanupListeners = useCallback(() => {
|
||||
if (unlistenersRef.current.chatMessage) {
|
||||
unlistenersRef.current.chatMessage();
|
||||
}
|
||||
if (unlistenersRef.current.error) {
|
||||
unlistenersRef.current.error();
|
||||
}
|
||||
unlistenersRef.current = {};
|
||||
}, []);
|
||||
|
||||
const chatClose = useCallback(
|
||||
async (activeChat?: Chat) => {
|
||||
if (!activeChat?._id) return;
|
||||
@@ -61,9 +81,30 @@ export function useChatActions(
|
||||
[currentService?.id, isTauri]
|
||||
);
|
||||
|
||||
const resetChatState = useCallback(() => {
|
||||
setCurChatEnd(true);
|
||||
|
||||
// Stop listening for streaming data.
|
||||
cleanupListeners();
|
||||
|
||||
setLoadingStep({
|
||||
query_intent: false,
|
||||
tools: false,
|
||||
fetch_source: false,
|
||||
pick_source: false,
|
||||
deep_read: false,
|
||||
think: false,
|
||||
response: false,
|
||||
});
|
||||
}, [cleanupListeners]);
|
||||
|
||||
// 1. onSelectChat
|
||||
// 2. dealMsg setTimedoutShow
|
||||
// 3. disabledChange Manual shutdown
|
||||
const cancelChat = useCallback(
|
||||
async (activeChat?: Chat) => {
|
||||
setCurChatEnd(true);
|
||||
resetChatState();
|
||||
|
||||
if (!activeChat?._id) return;
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
@@ -71,12 +112,15 @@ export function useChatActions(
|
||||
response = await platformAdapter.commands("cancel_session_chat", {
|
||||
serverId: currentService?.id,
|
||||
sessionId: activeChat?._id,
|
||||
queryParams: {
|
||||
message_id: curIdRef.current,
|
||||
},
|
||||
});
|
||||
response = response ? JSON.parse(response) : null;
|
||||
} else {
|
||||
const [_error, res] = await Post(
|
||||
`/chat/${activeChat?._id}/_cancel`,
|
||||
{}
|
||||
`/chat/${activeChat?._id}/_cancel?message_id=${curIdRef.current}`,
|
||||
undefined
|
||||
);
|
||||
response = res;
|
||||
}
|
||||
@@ -93,6 +137,7 @@ export function useChatActions(
|
||||
async (chat: Chat, callback?: (chat: Chat) => void) => {
|
||||
if (!chat?._id) return;
|
||||
|
||||
curSessionIdRef.current = chat?._id;
|
||||
let response: any;
|
||||
if (isTauri) {
|
||||
if (!currentService?.id) return;
|
||||
@@ -100,13 +145,13 @@ export function useChatActions(
|
||||
serverId: currentService?.id,
|
||||
sessionId: chat?._id,
|
||||
from: 0,
|
||||
size: 100,
|
||||
size: 1000,
|
||||
});
|
||||
response = response ? JSON.parse(response) : null;
|
||||
} else {
|
||||
const [_error, res] = await Get(`/chat/${chat?._id}/_history`, {
|
||||
from: 0,
|
||||
size: 100,
|
||||
size: 1000,
|
||||
});
|
||||
response = res;
|
||||
}
|
||||
@@ -134,14 +179,144 @@ export function useChatActions(
|
||||
[currentService?.id, isTauri, assistantList]
|
||||
);
|
||||
|
||||
const createNewChat = useCallback(
|
||||
async (value: string = "", activeChat?: Chat) => {
|
||||
setTimedoutShow(false);
|
||||
await chatClose(activeChat);
|
||||
clearAllChunkData();
|
||||
setQuestion(value);
|
||||
// Modify the clientId generation logic to include the instance ID.
|
||||
const clientId = useMemo(() => {
|
||||
const pageType = isChatPage ? "standalone-chat" : "search-chat";
|
||||
return `${pageType}`;
|
||||
}, [isChatPage]);
|
||||
|
||||
const handleChatCreateStreamMessage = useCallback(
|
||||
(msg: string) => {
|
||||
if (
|
||||
msg.includes(`"user"`) &&
|
||||
msg.includes("_source") &&
|
||||
msg.includes("result")
|
||||
) {
|
||||
try {
|
||||
const response = JSON.parse(msg);
|
||||
console.log("first", response);
|
||||
|
||||
let updatedChat: Chat;
|
||||
if (Array.isArray(response)) {
|
||||
curIdRef.current = response[0]?._id;
|
||||
curSessionIdRef.current = response[0]?._source?.session_id;
|
||||
console.log(
|
||||
"curIdRef-curSessionIdRef-Array",
|
||||
curIdRef.current,
|
||||
curSessionIdRef.current
|
||||
);
|
||||
updatedChat = {
|
||||
...updatedChatRef.current,
|
||||
messages: [
|
||||
...(updatedChatRef.current?.messages || []),
|
||||
...(response || []),
|
||||
],
|
||||
};
|
||||
console.log("array", updatedChat, updatedChatRef.current?.messages);
|
||||
} else {
|
||||
const newChat: Chat = response;
|
||||
curIdRef.current = response?.payload?.id;
|
||||
curSessionIdRef.current = response?.payload?.session_id;
|
||||
console.log(
|
||||
"curIdRef-curSessionIdRef",
|
||||
curIdRef.current,
|
||||
curSessionIdRef.current
|
||||
);
|
||||
|
||||
newChat._source = {
|
||||
...response?.payload,
|
||||
};
|
||||
updatedChat = {
|
||||
...newChat,
|
||||
messages: [newChat],
|
||||
};
|
||||
}
|
||||
|
||||
setActiveChat(updatedChat);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error("Failed to parse JSON:", error, "Raw message:", msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
dealMsgRef.current?.(msg);
|
||||
},
|
||||
[changeInput, setActiveChat, setCurChatEnd, setVisibleStartPage]
|
||||
);
|
||||
|
||||
const setupListeners = useCallback(
|
||||
async (timestamp: number) => {
|
||||
cleanupListeners();
|
||||
|
||||
console.log("setupListeners", clientId, timestamp);
|
||||
const unlisten_chat_message = await platformAdapter.listenEvent(
|
||||
`chat-stream-${clientId}-${timestamp}`,
|
||||
(event) => {
|
||||
const msg = event.payload as string;
|
||||
try {
|
||||
// console.log("msg:", JSON.parse(msg));
|
||||
// console.log("user:", msg.includes(`"user"`));
|
||||
// console.log("_source:", msg.includes("_source"));
|
||||
// console.log("result:", msg.includes("result"));
|
||||
// console.log("");
|
||||
// console.log("");
|
||||
// console.log("");
|
||||
// console.log("");
|
||||
// console.log("");
|
||||
} catch (error) {
|
||||
console.error("Failed to parse JSON in listener:", error);
|
||||
}
|
||||
|
||||
handleChatCreateStreamMessage(msg);
|
||||
}
|
||||
);
|
||||
|
||||
const unlisten_error = await platformAdapter.listenEvent(
|
||||
`chat-create-error`,
|
||||
(event) => {
|
||||
console.error("chat-create-error", event.payload);
|
||||
}
|
||||
);
|
||||
|
||||
// Store the listener references.
|
||||
unlistenersRef.current = {
|
||||
chatMessage: unlisten_chat_message,
|
||||
error: unlisten_error,
|
||||
};
|
||||
},
|
||||
[currentService?.id, clientId, handleChatCreateStreamMessage]
|
||||
);
|
||||
|
||||
const prepareChatSession = useCallback(
|
||||
async (value: string, timestamp: number) => {
|
||||
// 1. Cleaning and preparation
|
||||
await clearAllChunkData();
|
||||
|
||||
// 2. Update the status again
|
||||
await new Promise<void>((resolve) => {
|
||||
changeInput && changeInput("");
|
||||
setVisibleStartPage(false);
|
||||
setTimedoutShow(false);
|
||||
setQuestion(value);
|
||||
setCurChatEnd(false);
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
|
||||
// 4. Set up the listener first
|
||||
await setupListeners(timestamp);
|
||||
},
|
||||
[setupListeners]
|
||||
);
|
||||
|
||||
const createNewChat = useCallback(
|
||||
async (value: string = "") => {
|
||||
if (!value) return;
|
||||
|
||||
const timestamp = Date.now();
|
||||
|
||||
await prepareChatSession(value, timestamp);
|
||||
|
||||
//console.log("sourceDataIds", sourceDataIds, MCPIds, id);
|
||||
const queryParams = {
|
||||
search: isSearchActive,
|
||||
deep_thinking: isDeepThinkActive,
|
||||
@@ -150,13 +325,18 @@ export function useChatActions(
|
||||
mcp_servers: MCPIds?.join(",") || "",
|
||||
assistant_id: currentAssistant?._id || "",
|
||||
};
|
||||
|
||||
if (isTauri) {
|
||||
if (!currentService?.id) return;
|
||||
console.log("chat_create", clientId, timestamp);
|
||||
await platformAdapter.commands("chat_create", {
|
||||
serverId: currentService?.id,
|
||||
message: value,
|
||||
queryParams,
|
||||
clientId: `chat-stream-${clientId}-${timestamp}`,
|
||||
});
|
||||
console.log("_create end", value);
|
||||
resetChatState();
|
||||
} else {
|
||||
await streamPost({
|
||||
url: "/chat/_create",
|
||||
@@ -169,7 +349,6 @@ export function useChatActions(
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("_create", currentService?.id, value, queryParams);
|
||||
},
|
||||
[
|
||||
isTauri,
|
||||
@@ -179,9 +358,9 @@ export function useChatActions(
|
||||
isSearchActive,
|
||||
isDeepThinkActive,
|
||||
isMCPActive,
|
||||
curIdRef,
|
||||
currentAssistant,
|
||||
chatClose,
|
||||
clientId,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -189,7 +368,9 @@ export function useChatActions(
|
||||
async (content: string, newChat: Chat) => {
|
||||
if (!newChat?._id || !content) return;
|
||||
|
||||
clearAllChunkData();
|
||||
const timestamp = Date.now();
|
||||
|
||||
await prepareChatSession(content, timestamp);
|
||||
|
||||
const queryParams = {
|
||||
search: isSearchActive,
|
||||
@@ -199,14 +380,19 @@ export function useChatActions(
|
||||
mcp_servers: MCPIds?.join(",") || "",
|
||||
assistant_id: currentAssistant?._id || "",
|
||||
};
|
||||
|
||||
if (isTauri) {
|
||||
if (!currentService?.id) return;
|
||||
console.log("chat_chat", clientId, timestamp);
|
||||
await platformAdapter.commands("chat_chat", {
|
||||
serverId: currentService?.id,
|
||||
sessionId: newChat?._id,
|
||||
queryParams,
|
||||
message: content,
|
||||
clientId: `chat-stream-${clientId}-${timestamp}`,
|
||||
});
|
||||
console.log("chat_chat end", content, clientId);
|
||||
resetChatState();
|
||||
} else {
|
||||
await streamPost({
|
||||
url: `/chat/${newChat?._id}/_chat`,
|
||||
@@ -219,14 +405,6 @@ export function useChatActions(
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log(
|
||||
"chat_chat",
|
||||
currentService?.id,
|
||||
newChat?._id,
|
||||
queryParams,
|
||||
content
|
||||
);
|
||||
},
|
||||
[
|
||||
isTauri,
|
||||
@@ -236,101 +414,28 @@ export function useChatActions(
|
||||
isSearchActive,
|
||||
isDeepThinkActive,
|
||||
isMCPActive,
|
||||
curIdRef,
|
||||
changeInput,
|
||||
currentAssistant,
|
||||
clientId,
|
||||
]
|
||||
);
|
||||
|
||||
const handleSendMessage = useCallback(
|
||||
async (content: string, activeChat?: Chat) => {
|
||||
if (!activeChat?._id || !content) return;
|
||||
setQuestion(content);
|
||||
|
||||
setTimedoutShow(false);
|
||||
|
||||
await chatHistory(activeChat, (chat) => sendMessage(content, chat));
|
||||
},
|
||||
[chatHistory, sendMessage]
|
||||
);
|
||||
|
||||
const handleChatCreateStreamMessage = useCallback(
|
||||
(msg: string) => {
|
||||
if (
|
||||
msg.includes("_id") &&
|
||||
msg.includes("_source") &&
|
||||
msg.includes("result")
|
||||
) {
|
||||
const response = JSON.parse(msg);
|
||||
console.log("first", response);
|
||||
let updatedChat: Chat;
|
||||
if (Array.isArray(response)) {
|
||||
curIdRef.current = response[0]?._id;
|
||||
updatedChat = {
|
||||
...updatedChatRef.current,
|
||||
messages: [
|
||||
...(updatedChatRef.current?.messages || []),
|
||||
...(response || []),
|
||||
],
|
||||
};
|
||||
console.log("array", updatedChat, updatedChatRef.current?.messages);
|
||||
} else {
|
||||
const newChat: Chat = response;
|
||||
curIdRef.current = response?.payload?.id;
|
||||
|
||||
newChat._source = {
|
||||
...response?.payload,
|
||||
};
|
||||
updatedChat = {
|
||||
...newChat,
|
||||
messages: [newChat],
|
||||
};
|
||||
}
|
||||
|
||||
changeInput && changeInput("");
|
||||
setActiveChat(updatedChat);
|
||||
setCurChatEnd(false);
|
||||
setVisibleStartPage(false);
|
||||
return;
|
||||
}
|
||||
|
||||
dealMsgRef.current?.(msg);
|
||||
},
|
||||
[
|
||||
curIdRef,
|
||||
updatedChatRef,
|
||||
changeInput,
|
||||
setActiveChat,
|
||||
setCurChatEnd,
|
||||
setVisibleStartPage,
|
||||
dealMsgRef,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTauri || !currentService?.id) return;
|
||||
|
||||
const unlisten_message = platformAdapter.listenEvent(
|
||||
`chat-create-stream`,
|
||||
(event) => {
|
||||
const msg = event.payload as string;
|
||||
//console.log("chat-create-stream", msg);
|
||||
handleChatCreateStreamMessage(msg);
|
||||
}
|
||||
);
|
||||
|
||||
const unlisten_error = platformAdapter.listenEvent(
|
||||
`chat-create-error`,
|
||||
(event) => {
|
||||
console.error("chat-create-error", event.payload);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unlisten_message.then((fn) => fn());
|
||||
unlisten_error.then((fn) => fn());
|
||||
cleanupListeners();
|
||||
};
|
||||
}, [currentService?.id, dealMsgRef, updatedChatRef.current]);
|
||||
}, [currentService?.id]);
|
||||
|
||||
const openSessionChat = useCallback(
|
||||
async (chat: Chat) => {
|
||||
|
||||
@@ -3,17 +3,7 @@ import { useCallback, useEffect } from "react";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||
|
||||
interface KeyboardHandlersProps {
|
||||
isChatMode: boolean;
|
||||
handleSubmit: () => void;
|
||||
curChatEnd?: boolean;
|
||||
}
|
||||
|
||||
export function useKeyboardHandlers({
|
||||
isChatMode,
|
||||
handleSubmit,
|
||||
curChatEnd,
|
||||
}: KeyboardHandlersProps) {
|
||||
export function useKeyboardHandlers() {
|
||||
const {
|
||||
setSourceData,
|
||||
visibleExtensionStore,
|
||||
@@ -47,21 +37,8 @@ export function useKeyboardHandlers({
|
||||
|
||||
return setSourceData(void 0);
|
||||
}
|
||||
|
||||
// Handle Enter without meta key requirement
|
||||
if (e.code === "Enter" && !e.shiftKey && isChatMode) {
|
||||
e.preventDefault();
|
||||
curChatEnd && handleSubmit();
|
||||
}
|
||||
},
|
||||
[
|
||||
isChatMode,
|
||||
handleSubmit,
|
||||
setSourceData,
|
||||
curChatEnd,
|
||||
modifierKey,
|
||||
visibleExtensionDetail,
|
||||
]
|
||||
[setSourceData, modifierKey, visibleExtensionDetail]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -78,6 +78,7 @@ export default function useMessageChunkData() {
|
||||
};
|
||||
|
||||
const clearAllChunkData = () => {
|
||||
return new Promise<void>((resolve) => {
|
||||
setQuery_intent(undefined);
|
||||
setTools(undefined);
|
||||
setFetch_source(undefined);
|
||||
@@ -85,10 +86,20 @@ export default function useMessageChunkData() {
|
||||
setDeep_read(undefined);
|
||||
setThink(undefined);
|
||||
setResponse(undefined);
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
data: { query_intent, tools, fetch_source, pick_source, deep_read, think, response },
|
||||
data: {
|
||||
query_intent,
|
||||
tools,
|
||||
fetch_source,
|
||||
pick_source,
|
||||
deep_read,
|
||||
think,
|
||||
response,
|
||||
},
|
||||
handlers,
|
||||
clearAllChunkData,
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useConnectStore } from "@/stores/connectStore";
|
||||
|
||||
export function useMessageHandler(
|
||||
curIdRef: React.MutableRefObject<string>,
|
||||
curSessionIdRef: React.MutableRefObject<string>,
|
||||
setCurChatEnd: (value: boolean) => void,
|
||||
setTimedoutShow: (value: boolean) => void,
|
||||
onCancel: (chat?: Chat) => void,
|
||||
@@ -41,8 +42,20 @@ export function useMessageHandler(
|
||||
|
||||
try {
|
||||
const chunkData = JSON.parse(msg);
|
||||
// console.log("chunkData", chunkData);
|
||||
|
||||
// console.log(
|
||||
// "reply_to_message",
|
||||
// chunkData.reply_to_message,
|
||||
// curIdRef.current
|
||||
// );
|
||||
// console.log(
|
||||
// "session_id",
|
||||
// chunkData.session_id,
|
||||
// curSessionIdRef.current
|
||||
// );
|
||||
if (chunkData.reply_to_message !== curIdRef.current) return;
|
||||
if (chunkData.session_id !== curSessionIdRef.current) return;
|
||||
|
||||
setLoadingStep(() => ({
|
||||
query_intent: false,
|
||||
@@ -55,8 +68,6 @@ export function useMessageHandler(
|
||||
[chunkData.chunk_type]: true,
|
||||
}));
|
||||
|
||||
|
||||
|
||||
if (chunkData.chunk_type === "query_intent") {
|
||||
handlers.deal_query_intent(chunkData);
|
||||
} else if (chunkData.chunk_type === "tools") {
|
||||
@@ -87,7 +98,7 @@ export function useMessageHandler(
|
||||
}
|
||||
|
||||
if (inThinkRef.current) {
|
||||
handlers.deal_think({...chunkData, chunk_type: "think"});
|
||||
handlers.deal_think({ ...chunkData, chunk_type: "think" });
|
||||
} else {
|
||||
handlers.deal_response(chunkData);
|
||||
}
|
||||
@@ -105,13 +116,7 @@ export function useMessageHandler(
|
||||
console.error("parse error:", error);
|
||||
}
|
||||
},
|
||||
[
|
||||
onCancel,
|
||||
setCurChatEnd,
|
||||
setTimedoutShow,
|
||||
curIdRef.current,
|
||||
connectionTimeout,
|
||||
]
|
||||
[onCancel, setCurChatEnd, setTimedoutShow, connectionTimeout]
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -113,7 +113,7 @@ export const useStreamChat = (options: Options) => {
|
||||
useAsyncEffect(async () => {
|
||||
if (!message || !server || !assistant) return;
|
||||
|
||||
clearAllChunkData();
|
||||
await clearAllChunkData();
|
||||
|
||||
state.messageId = nanoid();
|
||||
|
||||
|
||||
@@ -113,7 +113,6 @@ export const useSyncStore = () => {
|
||||
const setAiOverviewMinQuantity = useExtensionsStore((state) => {
|
||||
return state.setAiOverviewMinQuantity;
|
||||
});
|
||||
const setCurrentService = useConnectStore((state) => state.setCurrentService);
|
||||
const setShowTooltip = useAppStore((state) => state.setShowTooltip);
|
||||
const setEndpoint = useAppStore((state) => state.setEndpoint);
|
||||
const setLanguage = useAppStore((state) => state.setLanguage);
|
||||
@@ -180,12 +179,8 @@ export const useSyncStore = () => {
|
||||
}),
|
||||
|
||||
platformAdapter.listenEvent("change-connect-store", ({ payload }) => {
|
||||
const {
|
||||
connectionTimeout,
|
||||
querySourceTimeout,
|
||||
allowSelfSignature,
|
||||
currentService,
|
||||
} = payload;
|
||||
const { connectionTimeout, querySourceTimeout, allowSelfSignature } =
|
||||
payload;
|
||||
if (isNumber(connectionTimeout)) {
|
||||
setConnectionTimeout(connectionTimeout);
|
||||
}
|
||||
@@ -193,8 +188,6 @@ export const useSyncStore = () => {
|
||||
setQueryTimeout(querySourceTimeout);
|
||||
}
|
||||
setAllowSelfSignature(allowSelfSignature);
|
||||
|
||||
setCurrentService(currentService);
|
||||
}),
|
||||
|
||||
platformAdapter.listenEvent("change-appearance-store", ({ payload }) => {
|
||||
|
||||
35
src/hooks/useTogglePin.ts
Normal file
35
src/hooks/useTogglePin.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { toggle_move_to_active_space_attribute } from "@/commands/system";
|
||||
import { isMac } from "@/utils/platform";
|
||||
|
||||
interface UseTogglePinOptions {
|
||||
onPinChange?: (isPinned: boolean) => void;
|
||||
}
|
||||
|
||||
export const useTogglePin = (options?: UseTogglePinOptions) => {
|
||||
const { isPinned, setIsPinned } = useAppStore();
|
||||
|
||||
const togglePin = useCallback(async () => {
|
||||
try {
|
||||
const newPinned = !isPinned;
|
||||
|
||||
if (options?.onPinChange) {
|
||||
options.onPinChange(newPinned);
|
||||
}
|
||||
|
||||
await platformAdapter.setAlwaysOnTop(newPinned);
|
||||
setIsPinned(newPinned);
|
||||
isMac && toggle_move_to_active_space_attribute();
|
||||
} catch (err) {
|
||||
console.error("Failed to toggle window pin state:", err);
|
||||
}
|
||||
}, [isPinned, setIsPinned, options?.onPinChange]);
|
||||
|
||||
return {
|
||||
isPinned,
|
||||
togglePin,
|
||||
};
|
||||
};
|
||||
@@ -7,7 +7,6 @@ import { exit } from "@tauri-apps/plugin-process";
|
||||
|
||||
import { isMac } from "@/utils/platform";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useUpdateStore } from "@/stores/updateStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { show_coco, show_settings, show_check } from "@/commands";
|
||||
|
||||
@@ -53,7 +52,7 @@ export const useTray = () => {
|
||||
text: t("tray.showCoco"),
|
||||
accelerator: showCocoShortcuts.join("+"),
|
||||
action: () => {
|
||||
show_coco()
|
||||
show_coco();
|
||||
},
|
||||
}),
|
||||
PredefinedMenuItem.new({ item: "Separator" }),
|
||||
@@ -61,18 +60,15 @@ export const useTray = () => {
|
||||
text: t("tray.settings"),
|
||||
// accelerator: "CommandOrControl+,",
|
||||
action: () => {
|
||||
show_settings()
|
||||
show_settings();
|
||||
},
|
||||
}),
|
||||
MenuItem.new({
|
||||
text: t("tray.checkUpdate"),
|
||||
action: async () => {
|
||||
const update = await platformAdapter.checkUpdate();
|
||||
if (update) {
|
||||
useUpdateStore.getState().setUpdateInfo(update);
|
||||
useUpdateStore.getState().setVisible(true);
|
||||
}
|
||||
show_check();
|
||||
await show_check();
|
||||
|
||||
platformAdapter.emitEvent("check-update");
|
||||
},
|
||||
}),
|
||||
PredefinedMenuItem.new({ item: "Separator" }),
|
||||
|
||||
@@ -489,7 +489,7 @@
|
||||
},
|
||||
"tray": {
|
||||
"showCoco": "Show Coco",
|
||||
"settings": "Settings...",
|
||||
"settings": "Settings",
|
||||
"quitCoco": "Quit Coco",
|
||||
"checkUpdate": "Check for Updates"
|
||||
},
|
||||
|
||||
@@ -18,22 +18,15 @@ import ChatAI, { ChatAIRef } from "@/components/Assistant/Chat";
|
||||
import type { Chat as typeChat } from "@/types/chat";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import InputBox from "@/components/Search/InputBox";
|
||||
import {
|
||||
chat_history,
|
||||
session_chat_history,
|
||||
close_session_chat,
|
||||
open_session_chat,
|
||||
delete_session_chat,
|
||||
update_session_chat,
|
||||
} from "@/commands";
|
||||
import HistoryList from "@/components/Common/HistoryList";
|
||||
import { useSyncStore } from "@/hooks/useSyncStore";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { unrequitable } from "@/utils";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
|
||||
interface ChatProps {}
|
||||
interface StandaloneChatProps {}
|
||||
|
||||
export default function Chat({}: ChatProps) {
|
||||
export default function StandaloneChat({}: StandaloneChatProps) {
|
||||
const setIsTauri = useAppStore((state) => state.setIsTauri);
|
||||
useEffect(() => {
|
||||
setIsTauri(true);
|
||||
@@ -74,7 +67,7 @@ export default function Chat({}: ChatProps) {
|
||||
return setChats([]);
|
||||
}
|
||||
|
||||
let response: any = await chat_history({
|
||||
let response: any = await platformAdapter.commands("chat_history", {
|
||||
serverId: currentService?.id,
|
||||
from: 0,
|
||||
size: 100,
|
||||
@@ -118,12 +111,15 @@ export default function Chat({}: ChatProps) {
|
||||
|
||||
const chatHistory = async (chat: typeChat) => {
|
||||
try {
|
||||
let response: any = await session_chat_history({
|
||||
let response: any = await platformAdapter.commands(
|
||||
"session_chat_history",
|
||||
{
|
||||
serverId: currentService?.id,
|
||||
sessionId: chat?._id || "",
|
||||
from: 0,
|
||||
size: 100,
|
||||
});
|
||||
size: 500,
|
||||
}
|
||||
);
|
||||
response = response ? JSON.parse(response) : null;
|
||||
console.log("id_history", response);
|
||||
const hits = response?.hits?.hits || [];
|
||||
@@ -149,7 +145,7 @@ export default function Chat({}: ChatProps) {
|
||||
const chatClose = async () => {
|
||||
if (!activeChat?._id) return;
|
||||
try {
|
||||
let response: any = await close_session_chat({
|
||||
let response: any = await platformAdapter.commands("close_session_chat", {
|
||||
serverId: currentService?.id,
|
||||
sessionId: activeChat?._id,
|
||||
});
|
||||
@@ -163,7 +159,7 @@ export default function Chat({}: ChatProps) {
|
||||
const onSelectChat = async (chat: any) => {
|
||||
chatClose();
|
||||
try {
|
||||
let response: any = await open_session_chat({
|
||||
let response: any = await platformAdapter.commands("open_session_chat", {
|
||||
serverId: currentService?.id,
|
||||
sessionId: chat?._id,
|
||||
});
|
||||
@@ -262,7 +258,7 @@ export default function Chat({}: ChatProps) {
|
||||
});
|
||||
}
|
||||
|
||||
update_session_chat({
|
||||
platformAdapter.commands("update_session_chat", {
|
||||
serverId: currentService.id,
|
||||
sessionId: chatId,
|
||||
title,
|
||||
@@ -272,7 +268,7 @@ export default function Chat({}: ChatProps) {
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!currentService?.id) return;
|
||||
|
||||
await delete_session_chat(currentService.id, id);
|
||||
await platformAdapter.commands("delete_session_chat", currentService.id, id);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -304,6 +300,7 @@ export default function Chat({}: ChatProps) {
|
||||
<ChatAI
|
||||
ref={chatAIRef}
|
||||
key="ChatAI"
|
||||
instanceId="standalone-chat"
|
||||
activeChatProp={activeChat}
|
||||
isSearchActive={isSearchActive}
|
||||
isDeepThinkActive={isDeepThinkActive}
|
||||
|
||||
@@ -2,19 +2,18 @@ import { useEffect } from "react";
|
||||
|
||||
import { useUpdateStore } from "@/stores/updateStore";
|
||||
import UpdateApp from "@/components/UpdateApp";
|
||||
import { useSyncStore } from "@/hooks/useSyncStore";
|
||||
|
||||
const CheckApp = () => {
|
||||
const setVisible = useUpdateStore((state) => state.setVisible);
|
||||
const { setVisible } = useUpdateStore();
|
||||
|
||||
useSyncStore();
|
||||
|
||||
useEffect(() => {
|
||||
setVisible(true)
|
||||
}, [])
|
||||
setVisible(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<UpdateApp isCheckPage={true} />
|
||||
</div>
|
||||
);
|
||||
return <UpdateApp isCheckPage />;
|
||||
};
|
||||
|
||||
export default CheckApp;
|
||||
@@ -7,7 +7,7 @@ import { listen } from "@tauri-apps/api/event";
|
||||
import SettingsPanel from "@/components/Settings/SettingsPanel";
|
||||
import GeneralSettings from "@/components/Settings/GeneralSettings";
|
||||
import AboutView from "@/components/Settings/AboutView";
|
||||
import Cloud from "@/components/Cloud/Cloud.tsx";
|
||||
import Cloud from "@/components/Cloud/Cloud";
|
||||
import Footer from "@/components/Common/UI/SettingsFooter";
|
||||
import { useTray } from "@/hooks/useTray";
|
||||
import Advanced from "@/components/Settings/Advanced";
|
||||
@@ -83,10 +83,6 @@ function SettingsPage() {
|
||||
<div>
|
||||
<div className="min-h-screen pb-8 bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
||||
<div className="max-w-6xl mx-auto p-4">
|
||||
{/* <div className="flex items-center justify-center mb-2">
|
||||
<h1 className="text-xl font-bold">Coco Settings</h1>
|
||||
</div> */}
|
||||
|
||||
<TabGroup
|
||||
selectedIndex={defaultIndex}
|
||||
onChange={(index) => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import useEscape from "@/hooks/useEscape";
|
||||
import { useViewportHeight } from "@/hooks/useViewportHeight";
|
||||
import { useIconfontScript } from "@/hooks/useScript";
|
||||
import type { StartPage } from "@/types/chat";
|
||||
import ErrorNotification from "@/components/Common/ErrorNotification";
|
||||
|
||||
import "@/i18n";
|
||||
import "@/web.css";
|
||||
@@ -87,7 +88,7 @@ function WebApp({
|
||||
return (
|
||||
<div
|
||||
id="searchChat-container"
|
||||
className={`coco-container ${theme}`}
|
||||
className={`coco-container relative ${theme}`}
|
||||
data-theme={theme}
|
||||
style={{
|
||||
maxWidth: `${width}px`,
|
||||
@@ -127,6 +128,7 @@ function WebApp({
|
||||
startPage={startPage}
|
||||
formatUrl={formatUrl}
|
||||
/>
|
||||
<ErrorNotification isTauri={false}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import Layout from "./layout";
|
||||
import ErrorPage from "@/pages/error/index";
|
||||
import DesktopApp from "@/pages/main/index";
|
||||
import SettingsPage from "@/pages/settings/index";
|
||||
import ChatAI from "@/pages/chat/index";
|
||||
import StandaloneChat from "@/pages/chat/index";
|
||||
import WebPage from "@/pages/web/index";
|
||||
import CheckPage from "@/pages/check/index";
|
||||
|
||||
@@ -25,7 +25,7 @@ export const router = createBrowserRouter(
|
||||
children: [
|
||||
{ path: "/ui", element: <DesktopApp /> },
|
||||
{ path: "/ui/settings", element: <SettingsPage /> },
|
||||
{ path: "/ui/chat", element: <ChatAI /> },
|
||||
{ path: "/ui/chat", element: <StandaloneChat /> },
|
||||
{ path: "/ui/check", element: <CheckPage /> },
|
||||
{ path: "/web", element: <WebPage /> },
|
||||
],
|
||||
|
||||
@@ -14,9 +14,10 @@ export interface ISource {
|
||||
message?: any;
|
||||
title?: string;
|
||||
question?: string;
|
||||
details?: any[];
|
||||
details?: any[] | null;
|
||||
assistant_id?: string;
|
||||
assistant_item?: any;
|
||||
[key: string]: any;
|
||||
}
|
||||
export interface Chat {
|
||||
_id?: string;
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
export interface ServerTokenResponse {
|
||||
access_token?: string;
|
||||
}
|
||||
|
||||
interface Provider {
|
||||
name: string;
|
||||
icon: string;
|
||||
|
||||
@@ -48,9 +48,10 @@ export interface EventPayloads {
|
||||
"install-extension": void;
|
||||
"uninstall-extension": void;
|
||||
"config-extension": string;
|
||||
"chat-create-stream": string;
|
||||
[key: `chat-stream-${string}`]: string;
|
||||
"chat-create-error": string;
|
||||
[key: `synthesize-${string}`]: any;
|
||||
"check-update": any;
|
||||
}
|
||||
|
||||
// Window operation interface
|
||||
@@ -118,9 +119,15 @@ export interface SystemOperations {
|
||||
commands: <T>(commandName: string, ...args: any[]) => Promise<T>;
|
||||
isWindows10: () => Promise<boolean>;
|
||||
revealItemInDir: (path: string) => Promise<unknown>;
|
||||
openSearchItem: (data: SearchDocument, formatUrl?: (item: SearchDocument) => string) => Promise<unknown>;
|
||||
openSearchItem: (
|
||||
data: SearchDocument,
|
||||
formatUrl?: (item: SearchDocument) => string
|
||||
) => Promise<unknown>;
|
||||
searchMCPServers: (serverId: string, queryParams: string[]) => Promise<any[]>;
|
||||
searchDataSources: (serverId: string, queryParams: string[]) => Promise<any[]>;
|
||||
searchDataSources: (
|
||||
serverId: string,
|
||||
queryParams: string[]
|
||||
) => Promise<any[]>;
|
||||
fetchAssistant: (serverId: string, queryParams: string[]) => Promise<any>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user