mirror of
https://github.com/infinilabs/coco-app.git
synced 2026-02-24 04:01:27 +01:00
feat: add togglePin & switch server (#166)
* feat: add togglePin * proxy websocket poc * feat: switch server * merge: merge switch ws server * feat: add switch server --------- Co-authored-by: medcl <m@medcl.net>
This commit is contained in:
319
src-tauri/Cargo.lock
generated
319
src-tauri/Cargo.lock
generated
@@ -74,6 +74,56 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.89"
|
||||
@@ -660,9 +710,13 @@ dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.13.1",
|
||||
"dirs 5.0.1",
|
||||
"env_logger",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"fuzzy_prefix_search",
|
||||
"hostname",
|
||||
"http 1.1.0",
|
||||
"hyper 0.14.32",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"notify",
|
||||
@@ -693,6 +747,10 @@ dependencies = [
|
||||
"tauri-plugin-websocket",
|
||||
"thiserror 1.0.64",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-tungstenite 0.20.1",
|
||||
"tungstenite 0.24.0",
|
||||
"url",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -726,6 +784,12 @@ dependencies = [
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
@@ -1296,6 +1360,29 @@ dependencies = [
|
||||
"syn 2.0.90",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"env_filter",
|
||||
"humantime",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
@@ -1949,7 +2036,7 @@ dependencies = [
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http",
|
||||
"http 1.1.0",
|
||||
"indexmap 2.6.0",
|
||||
"slab",
|
||||
"tokio",
|
||||
@@ -2034,6 +2121,17 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa 1.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.1.0"
|
||||
@@ -2045,6 +2143,17 @@ dependencies = [
|
||||
"itoa 1.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http 0.2.12",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.0.1"
|
||||
@@ -2052,7 +2161,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"http 1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2063,8 +2172,8 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.1",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
@@ -2080,6 +2189,40 @@ version = "1.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa 1.0.11",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.4.1"
|
||||
@@ -2090,8 +2233,8 @@ dependencies = [
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.1",
|
||||
"httparse",
|
||||
"itoa 1.0.11",
|
||||
"pin-project-lite",
|
||||
@@ -2107,15 +2250,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http",
|
||||
"hyper",
|
||||
"http 1.1.0",
|
||||
"hyper 1.4.1",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"rustls 0.23.15",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.26.0",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
"webpki-roots 0.26.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2126,7 +2269,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper 1.4.1",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
@@ -2143,9 +2286,9 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.1",
|
||||
"hyper 1.4.1",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.7",
|
||||
"tokio",
|
||||
@@ -2330,6 +2473,12 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.8"
|
||||
@@ -3653,7 +3802,7 @@ dependencies = [
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls 0.23.15",
|
||||
"socket2 0.5.7",
|
||||
"thiserror 1.0.64",
|
||||
"tokio",
|
||||
@@ -3670,7 +3819,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls 0.23.15",
|
||||
"slab",
|
||||
"thiserror 1.0.64",
|
||||
"tinyvec",
|
||||
@@ -3860,10 +4009,10 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper 1.4.1",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
@@ -3876,7 +4025,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls 0.23.15",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
@@ -3886,7 +4035,7 @@ dependencies = [
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.26.0",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-service",
|
||||
@@ -3895,7 +4044,7 @@ dependencies = [
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"webpki-roots 0.26.6",
|
||||
"windows-registry 0.2.0",
|
||||
]
|
||||
|
||||
@@ -3996,6 +4145,18 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.21.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring",
|
||||
"rustls-webpki 0.101.7",
|
||||
"sct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.15"
|
||||
@@ -4005,7 +4166,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"rustls-webpki 0.102.8",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -4025,6 +4186,16 @@ version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.102.8"
|
||||
@@ -4099,6 +4270,16 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "sct"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
@@ -4665,7 +4846,7 @@ dependencies = [
|
||||
"glob",
|
||||
"gtk",
|
||||
"heck 0.5.0",
|
||||
"http",
|
||||
"http 1.1.0",
|
||||
"http-range",
|
||||
"image",
|
||||
"jni",
|
||||
@@ -4909,7 +5090,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c752aee1b00ec3c4d4f440095995d9bd2c640b478f2067d1fba388900b82eb96"
|
||||
dependencies = [
|
||||
"data-url",
|
||||
"http",
|
||||
"http 1.1.0",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"schemars",
|
||||
@@ -5036,7 +5217,7 @@ dependencies = [
|
||||
"dirs 6.0.0",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http 1.1.0",
|
||||
"infer",
|
||||
"minisign-verify",
|
||||
"osakit",
|
||||
@@ -5064,7 +5245,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14055b796521e0facf6582256dea9322453595917851082610c557293862c966"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http",
|
||||
"http 1.1.0",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
@@ -5073,7 +5254,7 @@ dependencies = [
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.6",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tokio-tungstenite 0.24.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5084,7 +5265,7 @@ checksum = "cce18d43f80d4aba3aa8a0c953bbe835f3d0f2370aca75e8dbb14bd4bab27958"
|
||||
dependencies = [
|
||||
"dpi",
|
||||
"gtk",
|
||||
"http",
|
||||
"http 1.1.0",
|
||||
"jni",
|
||||
"raw-window-handle",
|
||||
"serde",
|
||||
@@ -5102,7 +5283,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f442a38863e10129ffe2cec7bd09c2dcf8a098a3a27801a476a304d5bb991d2"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"http 1.1.0",
|
||||
"jni",
|
||||
"log",
|
||||
"objc2",
|
||||
@@ -5133,7 +5314,7 @@ dependencies = [
|
||||
"dunce",
|
||||
"glob",
|
||||
"html5ever",
|
||||
"http",
|
||||
"http 1.1.0",
|
||||
"infer",
|
||||
"json-patch",
|
||||
"kuchikiki",
|
||||
@@ -5313,6 +5494,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio 1.0.2",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.5.7",
|
||||
@@ -5342,17 +5524,42 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
|
||||
dependencies = [
|
||||
"rustls 0.21.12",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"rustls 0.23.15",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"rustls 0.21.12",
|
||||
"tokio",
|
||||
"tokio-rustls 0.24.1",
|
||||
"tungstenite 0.20.1",
|
||||
"webpki-roots 0.25.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.24.0"
|
||||
@@ -5361,12 +5568,12 @@ checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"rustls",
|
||||
"rustls 0.23.15",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tungstenite",
|
||||
"webpki-roots",
|
||||
"tokio-rustls 0.26.0",
|
||||
"tungstenite 0.24.0",
|
||||
"webpki-roots 0.26.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5543,6 +5750,26 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http 0.2.12",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"rustls 0.21.12",
|
||||
"sha1",
|
||||
"thiserror 1.0.64",
|
||||
"url",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.24.0"
|
||||
@@ -5552,11 +5779,11 @@ dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http",
|
||||
"http 1.1.0",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"rustls",
|
||||
"rustls 0.23.15",
|
||||
"rustls-pki-types",
|
||||
"sha1",
|
||||
"thiserror 1.0.64",
|
||||
@@ -5690,6 +5917,12 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.10.0"
|
||||
@@ -5969,6 +6202,12 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.25.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.6"
|
||||
@@ -6472,7 +6711,7 @@ dependencies = [
|
||||
"gdkx11",
|
||||
"gtk",
|
||||
"html5ever",
|
||||
"http",
|
||||
"http 1.1.0",
|
||||
"javascriptcore-rs",
|
||||
"jni",
|
||||
"kuchikiki",
|
||||
|
||||
@@ -39,12 +39,14 @@ tauri-plugin-updater = "2"
|
||||
tauri-plugin-process = "2"
|
||||
tauri-plugin-drag = "2"
|
||||
|
||||
tokio-native-tls = "0.3" # For wss connections
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-tungstenite = { version = "0.20", features = ["rustls-tls-webpki-roots"] }
|
||||
hyper = { version = "0.14", features = ["client"] }
|
||||
reqwest = "0.12.12"
|
||||
futures = "0.3.31"
|
||||
ordered-float = { version = "4.6.0", default-features = false }
|
||||
lazy_static = "1.5.0"
|
||||
log = "0.4.22"
|
||||
tokio = "1.40.0"
|
||||
once_cell = "1.20.2"
|
||||
notify = "5.0"
|
||||
async-trait = "0.1.82"
|
||||
@@ -55,6 +57,13 @@ plist = "1.7"
|
||||
base64 = "0.13"
|
||||
walkdir = "2"
|
||||
fuzzy_prefix_search = "0.2"
|
||||
log = "0.4"
|
||||
|
||||
futures-util = "0.3.31"
|
||||
url = "2.5.2"
|
||||
http = "1.1.0"
|
||||
tungstenite = "0.24.0"
|
||||
env_logger = "0.11.5"
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"core:window:allow-set-size",
|
||||
"core:window:allow-get-all-windows",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-set-always-on-top",
|
||||
"core:app:allow-set-app-theme",
|
||||
"shell:default",
|
||||
"http:default",
|
||||
|
||||
@@ -52,7 +52,9 @@ struct Payload {
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let mut ctx = tauri::generate_context!();
|
||||
|
||||
// Initialize logger
|
||||
env_logger::init();
|
||||
|
||||
let mut app_builder = tauri::Builder::default();
|
||||
|
||||
#[cfg(desktop)]
|
||||
@@ -63,7 +65,8 @@ pub fn run() {
|
||||
}));
|
||||
}
|
||||
|
||||
app_builder = app_builder.plugin(tauri_plugin_http::init())
|
||||
app_builder = app_builder
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_autostart::init(
|
||||
MacosLauncher::AppleScript,
|
||||
@@ -102,12 +105,15 @@ pub fn run() {
|
||||
// server::get_coco_servers_health_info,
|
||||
// server::get_user_profiles,
|
||||
// server::get_coco_server_datasources,
|
||||
// server::get_coco_server_connectors
|
||||
// server::get_coco_server_connectors,
|
||||
server::websocket::connect_to_server,
|
||||
server::websocket::disconnect,
|
||||
])
|
||||
.setup(|app| {
|
||||
let registry = SearchSourceRegistry::default();
|
||||
|
||||
app.manage(registry); // Store registry in Tauri's app state
|
||||
app.manage(server::websocket::WebSocketManager::default());
|
||||
|
||||
// Get app handle
|
||||
let app_handle = app.handle().clone();
|
||||
|
||||
@@ -17,3 +17,4 @@ pub mod datasource;
|
||||
pub mod http_client;
|
||||
pub mod profile;
|
||||
pub mod search;
|
||||
pub mod websocket;
|
||||
|
||||
175
src-tauri/src/server/websocket.rs
Normal file
175
src-tauri/src/server/websocket.rs
Normal file
@@ -0,0 +1,175 @@
|
||||
use crate::server::servers::{get_server_by_id, get_server_token};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use http::{HeaderMap, HeaderName, HeaderValue};
|
||||
use std::sync::Arc;
|
||||
use tauri::Emitter;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
||||
use tokio_tungstenite::tungstenite::Error;
|
||||
use tokio_tungstenite::{connect_async, tungstenite::protocol::Message, MaybeTlsStream, WebSocketStream};
|
||||
use tungstenite::handshake::client::generate_key;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct WebSocketManager {
|
||||
ws_connection: Arc<Mutex<Option<WebSocketStream<MaybeTlsStream<TcpStream>>>>>,
|
||||
cancel_tx: Arc<Mutex<Option<mpsc::Sender<()>>>>,
|
||||
}
|
||||
|
||||
// Function to convert the HTTP endpoint to WebSocket endpoint
|
||||
fn convert_to_websocket(endpoint: &str) -> Result<String, String> {
|
||||
let url = url::Url::parse(endpoint).map_err(|e| format!("Invalid URL: {}", e))?;
|
||||
|
||||
// Determine WebSocket protocol based on the scheme
|
||||
let ws_protocol = if url.scheme() == "https" { "wss://" } else { "ws://" };
|
||||
|
||||
// Extract host and port (if present)
|
||||
let host = url.host_str().ok_or_else(|| "No host found in URL")?;
|
||||
let port = url.port_or_known_default().unwrap_or(if url.scheme() == "https" { 443 } else { 80 });
|
||||
|
||||
// Build WebSocket URL, include the port if not the default
|
||||
let ws_endpoint = if port == 80 || port == 443 {
|
||||
format!("{}{}{}", ws_protocol, host, "/ws")
|
||||
} else {
|
||||
format!("{}{}:{}/ws", ws_protocol, host, port)
|
||||
};
|
||||
|
||||
Ok(ws_endpoint)
|
||||
}
|
||||
|
||||
// Function to build a HeaderMap from a vector of key-value pairs
|
||||
fn build_header_map(headers: Vec<(String, String)>) -> Result<HeaderMap, String> {
|
||||
let mut header_map = HeaderMap::new();
|
||||
for (key, value) in headers {
|
||||
let header_name = HeaderName::from_bytes(key.as_bytes())
|
||||
.map_err(|e| format!("Invalid header name: {}", e))?;
|
||||
let header_value = HeaderValue::from_str(&value)
|
||||
.map_err(|e| format!("Invalid header value: {}", e))?;
|
||||
header_map.insert(header_name, header_value);
|
||||
}
|
||||
Ok(header_map)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn connect_to_server(
|
||||
id: String,
|
||||
state: tauri::State<'_, WebSocketManager>,
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> Result<(), String> {
|
||||
dbg!("Connecting to server",id.as_str());
|
||||
|
||||
// Disconnect any existing connection first
|
||||
disconnect(state.clone()).await?;
|
||||
|
||||
dbg!("Disconnected from previous server",id.as_str());
|
||||
|
||||
// Retrieve server details
|
||||
let server = get_server_by_id(id.as_str())
|
||||
.ok_or_else(|| format!("Server with ID {} not found", id))?;
|
||||
let mut endpoint = convert_to_websocket(server.endpoint.as_str())?;
|
||||
|
||||
dbg!("Server endpoint",endpoint.as_str());
|
||||
|
||||
// Retrieve the token for the server (token is optional)
|
||||
let token = get_server_token(id.as_str()).map(|t| t.access_token.clone());
|
||||
|
||||
dbg!("Server token",token.as_ref().unwrap_or(&"".to_string()).as_str());
|
||||
|
||||
// Create the WebSocket request
|
||||
let mut request = tokio_tungstenite::tungstenite::client::IntoClientRequest::into_client_request(
|
||||
&endpoint
|
||||
).map_err(|e| format!("Failed to create WebSocket request: {}", e))?;
|
||||
|
||||
dbg!("WebSocket request");
|
||||
|
||||
// Add necessary headers
|
||||
request.headers_mut().insert("Connection", "Upgrade".parse().unwrap());
|
||||
request.headers_mut().insert("Upgrade", "websocket".parse().unwrap());
|
||||
request.headers_mut().insert("Sec-WebSocket-Version", "13".parse().unwrap());
|
||||
request.headers_mut().insert(
|
||||
"Sec-WebSocket-Key",
|
||||
generate_key().parse().unwrap(),
|
||||
);
|
||||
|
||||
dbg!("WebSocket headers",request.headers().iter().map(|(k, v)| format!("{}: {}", k.as_str(), v.to_str().unwrap())).collect::<Vec<String>>().join("\n"));
|
||||
|
||||
// If a token exists, add it to the headers
|
||||
if let Some(token) = token {
|
||||
request.headers_mut().insert("X-API-TOKEN", token.parse().unwrap());
|
||||
}
|
||||
|
||||
dbg!("WebSocket headers with token",request.headers().iter().map(|(k, v)| format!("{}: {}", k.as_str(), v.to_str().unwrap())).collect::<Vec<String>>().join("\n"));
|
||||
|
||||
// Establish the WebSocket connection
|
||||
dbg!(&request);
|
||||
let (mut ws_remote, _) = connect_async(request).await
|
||||
.map_err(|e| {
|
||||
dbg!("WebSocket connection error",&e);
|
||||
match e {
|
||||
Error::ConnectionClosed => "WebSocket connection was closed".to_string(),
|
||||
Error::Protocol(protocol_error) => format!("Protocol error: {}", protocol_error),
|
||||
Error::Utf8 => "UTF-8 error in WebSocket data".to_string(),
|
||||
_ => format!("Unknown error: {:?}", e),
|
||||
}
|
||||
})?;
|
||||
|
||||
dbg!("Connected to server1234",id.as_str());
|
||||
|
||||
// Create cancellation channel
|
||||
let (cancel_tx, mut cancel_rx) = mpsc::channel(1);
|
||||
|
||||
dbg!("try to lock Connected to server",id.as_str());
|
||||
|
||||
// Store connection and cancellation sender
|
||||
*state.ws_connection.lock().await = Some(ws_remote);
|
||||
*state.cancel_tx.lock().await = Some(cancel_tx);
|
||||
|
||||
dbg!("locked Connected to server",id.as_str());
|
||||
|
||||
// Spawn listener task with cancellation
|
||||
let app_handle_clone = app_handle.clone();
|
||||
let connection_clone = state.ws_connection.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut connection = connection_clone.lock().await;
|
||||
if let Some(ws) = connection.as_mut() {
|
||||
loop {
|
||||
dbg!("try to select Connected to server",id.as_str());
|
||||
|
||||
tokio::select! {
|
||||
msg = ws.next() => {
|
||||
match msg {
|
||||
Some(Ok(Message::Text(text))) => {
|
||||
let _ = app_handle_clone.emit("ws-message", text);
|
||||
},
|
||||
Some(Err(_)) | None => break,
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
_ = cancel_rx.recv() => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dbg!("END Connected to server");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn disconnect(state: tauri::State<'_, WebSocketManager>) -> Result<(), String> {
|
||||
// Send cancellation signal
|
||||
if let Some(cancel_tx) = state.cancel_tx.lock().await.take() {
|
||||
let _ = cancel_tx.send(()).await;
|
||||
}
|
||||
|
||||
// Close connection
|
||||
let mut connection = state.ws_connection.lock().await;
|
||||
if let Some(mut ws) = connection.take() {
|
||||
let _ = ws.close(None).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -23,7 +23,6 @@
|
||||
"maximizable": false,
|
||||
"skipTaskbar": true,
|
||||
"resizable": false,
|
||||
"alwaysOnTop": true,
|
||||
"acceptFirstMouse": true,
|
||||
"shadow": true,
|
||||
"transparent": true,
|
||||
|
||||
@@ -39,7 +39,10 @@ export const tauriFetch = async <T = any>({
|
||||
const addLog = useLogStore.getState().addLog;
|
||||
|
||||
try {
|
||||
console.log("baseURL", baseURL)
|
||||
const appStore = JSON.parse(localStorage.getItem("app-store") || "{}");
|
||||
console.log("baseURL", appStore.state?.endpoint_http)
|
||||
|
||||
baseURL = appStore.state?.endpoint_http || baseURL;
|
||||
|
||||
const authStore = JSON.parse(localStorage.getItem("auth-store") || "{}")
|
||||
const auth = authStore?.state?.auth
|
||||
@@ -58,7 +61,8 @@ export const tauriFetch = async <T = any>({
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
|
||||
const res: any = await invoke("get_server_token", {id: "default_coco_server"});
|
||||
const server_id = appStore.state?.activeServer?.id || "default_coco_server"
|
||||
const res: any = await invoke("get_server_token", {id: server_id});
|
||||
|
||||
headers["X-API-TOKEN"] = headers["X-API-TOKEN"] || res?.access_token || undefined;
|
||||
|
||||
|
||||
@@ -11,15 +11,17 @@ import {
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { debounce } from "lodash-es";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import { ChatMessage } from "./ChatMessage";
|
||||
import type { Chat } from "./types";
|
||||
import { tauriFetch } from "@/api/tauriFetchClient";
|
||||
import { useWebSocket } from "@/hooks/useWebSocket";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import { useWindows } from "@/hooks/useWindows";
|
||||
import { clientEnv } from "@/utils/env";
|
||||
import { ChatHeader } from "./ChatHeader";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
|
||||
interface ChatAIProps {
|
||||
isTransitioned: boolean;
|
||||
isSearchActive?: boolean;
|
||||
@@ -50,7 +52,10 @@ const ChatAI = memo(
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
if (!isTransitioned) return null;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
init: init,
|
||||
cancelChat: cancelChat,
|
||||
@@ -61,7 +66,9 @@ const ChatAI = memo(
|
||||
|
||||
const { createWin } = useWindows();
|
||||
|
||||
const { curChatEnd, setCurChatEnd, setConnected } = useChatStore();
|
||||
const { curChatEnd, setCurChatEnd, connected, setConnected, messages, setMessages } =
|
||||
useChatStore();
|
||||
const activeServer = useAppStore((state) => state.activeServer);
|
||||
|
||||
const [activeChat, setActiveChat] = useState<Chat>();
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
@@ -85,56 +92,75 @@ const ChatAI = memo(
|
||||
setCurMessage((prev) => prev + chunk);
|
||||
}, []);
|
||||
|
||||
const reconnect = async () => {
|
||||
if (!activeServer?.id) return;
|
||||
try {
|
||||
await invoke("connect_to_server", { id: activeServer?.id });
|
||||
setConnected(true);
|
||||
} catch (error) {
|
||||
console.error("Failed to connect:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const messageTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
const dealMsg = useCallback((msg: string) => {
|
||||
// console.log("msg:", msg);
|
||||
if (messageTimeoutRef.current) {
|
||||
clearTimeout(messageTimeoutRef.current);
|
||||
}
|
||||
const dealMsg = useCallback(
|
||||
(msg: string) => {
|
||||
// console.log("msg:", msg);
|
||||
if (messageTimeoutRef.current) {
|
||||
clearTimeout(messageTimeoutRef.current);
|
||||
}
|
||||
|
||||
if (msg.includes("websocket-session-id")) {
|
||||
const array = msg.split(" ");
|
||||
websocketIdRef.current = array[2];
|
||||
return "";
|
||||
} else if (msg.includes("PRIVATE")) {
|
||||
messageTimeoutRef.current = setTimeout(() => {
|
||||
if (!curChatEnd && isTyping) {
|
||||
console.log("AI response timeout");
|
||||
setTimedoutShow(true);
|
||||
cancelChat();
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
if (msg.includes("assistant finished output")) {
|
||||
if (messageTimeoutRef.current) {
|
||||
clearTimeout(messageTimeoutRef.current);
|
||||
}
|
||||
// console.log("AI finished output");
|
||||
simulateAssistantResponse();
|
||||
setCurChatEnd(true);
|
||||
} else {
|
||||
const cleanedData = msg.replace(/^PRIVATE /, "");
|
||||
try {
|
||||
// console.log("cleanedData", cleanedData);
|
||||
const chunkData = JSON.parse(cleanedData);
|
||||
if (chunkData.reply_to_message === curIdRef.current) {
|
||||
handleMessageChunk(chunkData.message_chunk);
|
||||
setMessages((prev) => prev + chunkData.message_chunk);
|
||||
return chunkData.message_chunk;
|
||||
if (msg.includes("websocket-session-id")) {
|
||||
const array = msg.split(" ");
|
||||
websocketIdRef.current = array[2];
|
||||
return "";
|
||||
} else if (msg.includes("PRIVATE")) {
|
||||
messageTimeoutRef.current = setTimeout(() => {
|
||||
if (!curChatEnd && isTyping) {
|
||||
console.log("AI response timeout");
|
||||
setTimedoutShow(true);
|
||||
cancelChat();
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
if (msg.includes("assistant finished output")) {
|
||||
if (messageTimeoutRef.current) {
|
||||
clearTimeout(messageTimeoutRef.current);
|
||||
}
|
||||
console.log("AI finished output");
|
||||
setCurChatEnd(true);
|
||||
} else {
|
||||
const cleanedData = msg.replace(/^PRIVATE /, "");
|
||||
try {
|
||||
// console.log("cleanedData", cleanedData);
|
||||
const chunkData = JSON.parse(cleanedData);
|
||||
if (chunkData.reply_to_message === curIdRef.current) {
|
||||
handleMessageChunk(chunkData.message_chunk);
|
||||
return chunkData.message_chunk;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("parse error:", error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("parse error:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [curChatEnd, isTyping]);
|
||||
|
||||
const { messages, setMessages, connected, reconnect } = useWebSocket(
|
||||
clientEnv.COCO_WEBSOCKET_URL,
|
||||
dealMsg
|
||||
},
|
||||
[curChatEnd, isTyping]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = listen("ws-message", (event) => {
|
||||
const data = dealMsg(String(event.payload));
|
||||
if (data) {
|
||||
setMessages((prev) => prev + data);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten.then((fn) => fn());
|
||||
};
|
||||
}, []);
|
||||
|
||||
const assistantMessage = useMemo(() => {
|
||||
if (!activeChat?._id || (!curMessage && !messages)) return null;
|
||||
return {
|
||||
@@ -153,11 +179,11 @@ const ChatAI = memo(
|
||||
messages: [...(activeChat.messages || []), assistantMessage],
|
||||
};
|
||||
}, [activeChat, assistantMessage]);
|
||||
|
||||
|
||||
const simulateAssistantResponse = useCallback(() => {
|
||||
if (!updatedChat) return;
|
||||
|
||||
// console.log("updatedChat:", updatedChat);
|
||||
console.log("updatedChat:", updatedChat);
|
||||
setActiveChat(updatedChat);
|
||||
setMessages("");
|
||||
setCurMessage("");
|
||||
@@ -170,10 +196,6 @@ const ChatAI = memo(
|
||||
}
|
||||
}, [curChatEnd]);
|
||||
|
||||
useEffect(() => {
|
||||
setConnected(connected);
|
||||
}, [connected]);
|
||||
|
||||
const scrollToBottom = useCallback(
|
||||
debounce(() => {
|
||||
messagesEndRef.current?.scrollIntoView({
|
||||
@@ -216,6 +238,7 @@ const ChatAI = memo(
|
||||
|
||||
const handleSendMessage = useCallback(
|
||||
async (content: string, newChat?: Chat) => {
|
||||
console.log("11111111", isSearchActive, isDeepThinkActive);
|
||||
newChat = newChat || activeChat;
|
||||
if (!newChat?._id || !content) return;
|
||||
setTimedoutShow(false);
|
||||
@@ -240,7 +263,7 @@ const ChatAI = memo(
|
||||
};
|
||||
|
||||
changeInput && changeInput("");
|
||||
// console.log("updatedChat2", updatedChat);
|
||||
console.log("updatedChat2", updatedChat);
|
||||
setActiveChat(updatedChat);
|
||||
setIsTyping(true);
|
||||
setCurChatEnd(false);
|
||||
@@ -248,7 +271,7 @@ const ChatAI = memo(
|
||||
console.error("Failed to fetch user data:", error);
|
||||
}
|
||||
},
|
||||
[activeChat?._id, isSearchActive, isDeepThinkActive]
|
||||
[activeChat, isSearchActive, isDeepThinkActive]
|
||||
);
|
||||
|
||||
const chatClose = async () => {
|
||||
@@ -318,8 +341,6 @@ const ChatAI = memo(
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!isTransitioned) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
|
||||
@@ -1,13 +1,35 @@
|
||||
import { MessageSquarePlus, PanelLeft, Pin, MoreHorizontal } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Listbox, Popover } from "@headlessui/react";
|
||||
import {
|
||||
MessageSquarePlus,
|
||||
PictureInPicture2,
|
||||
Pin,
|
||||
PinOff,
|
||||
MoreHorizontal,
|
||||
ChevronDownIcon,
|
||||
Settings,
|
||||
RefreshCw,
|
||||
Check,
|
||||
PanelRightClose,
|
||||
} from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuItems,
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverPanel,
|
||||
} from "@headlessui/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
|
||||
import logoImg from "@/assets/icon.svg";
|
||||
import { useAppStore, IServer } from "@/stores/appStore";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
|
||||
|
||||
interface Server {
|
||||
id: string;
|
||||
name: string;
|
||||
status: 'online' | 'offline';
|
||||
assistantCount: number;
|
||||
}
|
||||
|
||||
interface ChatHeaderProps {
|
||||
onCreateNewChat: () => void;
|
||||
@@ -15,64 +37,131 @@ interface ChatHeaderProps {
|
||||
}
|
||||
|
||||
export function ChatHeader({ onCreateNewChat, onOpenChatAI }: ChatHeaderProps) {
|
||||
const [isPinned, setIsPinned] = useState(false);
|
||||
const [showAI] = useState(false);
|
||||
const [servers] = useState<Server[]>([
|
||||
{ id: '1', name: 'Coco Cloud', status: 'online', assistantCount: 3 },
|
||||
{ id: '2', name: 'Searchkit', status: 'online', assistantCount: 3 },
|
||||
{ id: '3', name: 'INFINI Labs', status: 'online', assistantCount: 2 },
|
||||
{ id: '4', name: 'Test server', status: 'offline', assistantCount: 1 },
|
||||
]);
|
||||
const [selectedServer, setSelectedServer] = useState(servers[0]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const setEndpoint = useAppStore((state) => state.setEndpoint);
|
||||
const isPinned = useAppStore((state) => state.isPinned);
|
||||
const setIsPinned = useAppStore((state) => state.setIsPinned);
|
||||
|
||||
const { setConnected, setMessages } = useChatStore();
|
||||
|
||||
const [serverList, setServerList] = useState<IServer[]>([]);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const activeServer = useAppStore((state) => state.activeServer);
|
||||
const setActiveServer = useAppStore((state) => state.setActiveServer);
|
||||
|
||||
const fetchServers = async (resetSelection: boolean) => {
|
||||
invoke("list_coco_servers")
|
||||
.then((res: any) => {
|
||||
setServerList(res);
|
||||
if (resetSelection && res.length > 0) {
|
||||
setActiveServer(res[0]);
|
||||
setEndpoint(res[0].endpoint);
|
||||
switchServer(res[0]);
|
||||
} else {
|
||||
console.warn("Service list is empty or last item has no id");
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchServers(true);
|
||||
}, []);
|
||||
|
||||
const disconnect = async () => {
|
||||
console.log("disconnecting");
|
||||
try {
|
||||
await invoke("disconnect");
|
||||
setConnected(false);
|
||||
setActiveServer(null);
|
||||
console.log("disconnected");
|
||||
} catch (error) {
|
||||
console.error("Failed to disconnect:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const connect = async (server: IServer) => {
|
||||
try {
|
||||
await invoke("connect_to_server", { id: server.id });
|
||||
setActiveServer(server);
|
||||
setEndpoint(server.endpoint);
|
||||
setConnected(true);
|
||||
setMessages(""); // Clear previous messages
|
||||
} catch (error) {
|
||||
console.error("Failed to connect:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const switchServer = async (server: IServer) => {
|
||||
try {
|
||||
await disconnect();
|
||||
await connect(server);
|
||||
} catch (error) {
|
||||
console.error("switchServer:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const togglePin = async () => {
|
||||
try {
|
||||
const newPinned = !isPinned;
|
||||
await getCurrentWindow().setAlwaysOnTop(newPinned);
|
||||
setIsPinned(newPinned);
|
||||
} catch (err) {
|
||||
console.error("Failed to toggle window pin state:", err);
|
||||
setIsPinned(isPinned);
|
||||
}
|
||||
};
|
||||
|
||||
const openSettings = async () => {
|
||||
emit("open_settings", "connect");
|
||||
};
|
||||
|
||||
const openHistList = async () => {};
|
||||
|
||||
return (
|
||||
<header className="flex items-center justify-between py-2 px-3" data-tauri-drag-region>
|
||||
<header
|
||||
className="flex items-center justify-between py-2 px-3"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onOpenChatAI}
|
||||
onClick={openHistList}
|
||||
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<PanelLeft className="h-4 w-4" />
|
||||
<PanelRightClose className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
{showAI ? <Listbox value={selectedServer} onChange={setSelectedServer}>
|
||||
<div className="relative">
|
||||
<Listbox.Button className="relative w-48 h-8 px-3 py-1 text-left bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-2">
|
||||
<img src="/path-to-server-icon.png" className="w-4 h-4 rounded-full" />
|
||||
<span className="block truncate">{selectedServer.name}</span>
|
||||
</div>
|
||||
</Listbox.Button>
|
||||
<Listbox.Options className="absolute w-full mt-1 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700">
|
||||
{servers.map((server) => (
|
||||
<Listbox.Option
|
||||
key={server.id}
|
||||
value={server}
|
||||
className={({ active }) =>
|
||||
`relative cursor-pointer select-none py-2 px-3 ${
|
||||
active ? 'bg-gray-100 dark:bg-gray-700' : ''
|
||||
}`
|
||||
}
|
||||
>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<img src="/path-to-server-icon.png" className="w-4 h-4 rounded-full" />
|
||||
<span>{server.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-2 h-2 rounded-full ${
|
||||
server.status === 'online' ? 'bg-green-500' : 'bg-gray-400'
|
||||
}`} />
|
||||
<span className="text-xs text-gray-500">
|
||||
AI Assistant: {server.assistantCount}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</div>
|
||||
</Listbox> : null}
|
||||
<Menu>
|
||||
<MenuButton className="flex items-center gap-1 rounded-full bg-white dark:bg-[#202126] p-1 text-sm/6 font-semibold text-gray-800 dark:text-white border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none">
|
||||
<img
|
||||
src={logoImg}
|
||||
className="w-4 h-4"
|
||||
alt={t("assistant.message.logo")}
|
||||
/>
|
||||
Coco AI
|
||||
<ChevronDownIcon className="size-4 text-gray-500 dark:text-gray-400" />
|
||||
</MenuButton>
|
||||
|
||||
<MenuItems
|
||||
transition
|
||||
anchor="bottom end"
|
||||
className="w-28 origin-top-right rounded-xl bg-white dark:bg-[#202126] p-1 text-sm/6 text-gray-800 dark:text-white shadow-lg border border-gray-200 dark:border-gray-700 focus:outline-none data-[closed]:scale-95 data-[closed]:opacity-0"
|
||||
>
|
||||
<MenuItem>
|
||||
<button className="group flex w-full items-center gap-2 rounded-lg py-1.5 px-3 hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<img
|
||||
src={logoImg}
|
||||
className="w-4 h-4"
|
||||
alt={t("assistant.message.logo")}
|
||||
/>
|
||||
Coco AI
|
||||
</button>
|
||||
</MenuItem>
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
|
||||
<button
|
||||
onClick={onCreateNewChat}
|
||||
@@ -84,44 +173,107 @@ export function ChatHeader({ onCreateNewChat, onOpenChatAI }: ChatHeaderProps) {
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setIsPinned(!isPinned)}
|
||||
className={`p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 ${
|
||||
isPinned ? 'text-blue-500' : ''
|
||||
onClick={togglePin}
|
||||
className={`rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 ${
|
||||
isPinned ? "text-blue-500" : ""
|
||||
}`}
|
||||
>
|
||||
<Pin className="h-4 w-4" />
|
||||
{isPinned ? (
|
||||
<Pin className="h-4 w-4" />
|
||||
) : (
|
||||
<PinOff className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onOpenChatAI}
|
||||
className="rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<PictureInPicture2 className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<Popover className="relative">
|
||||
<Popover.Button className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||
<PopoverButton className="flex items-center rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Popover.Button>
|
||||
</PopoverButton>
|
||||
|
||||
<Popover.Panel className="absolute right-0 mt-2 w-60 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-2">
|
||||
<h3 className="font-medium mb-2">Servers</h3>
|
||||
<div className="space-y-2">
|
||||
{servers.map(server => (
|
||||
<div key={server.id} className="flex items-center justify-between p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg">
|
||||
<PopoverPanel className="absolute right-0 z-10 mt-2 min-w-[240px] bg-white dark:bg-[#202126] rounded-lg shadow-lg border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-3">
|
||||
<div className="flex items-center justify-between mb-3 whitespace-nowrap">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
Servers
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={openSettings}
|
||||
className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<Settings className="h-4 w-4 text-[#0287FF]" />
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
setIsRefreshing(true);
|
||||
await fetchServers(false);
|
||||
setTimeout(() => setIsRefreshing(false), 1000);
|
||||
}}
|
||||
className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400"
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 text-[#0287FF] transition-transform duration-1000 ${
|
||||
isRefreshing ? "animate-spin" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{serverList.map((server) => (
|
||||
<button
|
||||
key={server.id}
|
||||
onClick={() => switchServer(server)}
|
||||
className={`w-full flex items-center justify-between p-2 rounded-lg transition-colors whitespace-nowrap ${
|
||||
activeServer?.id === server.id
|
||||
? "bg-gray-100 dark:bg-gray-800"
|
||||
: "hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<img src="/path-to-server-icon.png" className="w-6 h-6 rounded-full" />
|
||||
<div>
|
||||
<div className="font-medium">{server.name}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
AI Assistant: {server.assistantCount}
|
||||
<img
|
||||
src={server?.provider?.icon || logoImg}
|
||||
alt={server.name}
|
||||
className="w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800"
|
||||
/>
|
||||
<div className="text-left">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{server.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
AI Assistant: {server.assistantCount || 1}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`w-2 h-2 rounded-full ${
|
||||
server.status === 'online' ? 'bg-green-500' : 'bg-gray-400'
|
||||
}`} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`w-3 h-3 rounded-full ${
|
||||
server.available
|
||||
? "bg-[#00B926]"
|
||||
: "bg-gray-400 dark:bg-gray-600"
|
||||
}`}
|
||||
/>
|
||||
<div className="w-4 h-4">
|
||||
{activeServer?.id === server.id && (
|
||||
<Check className="w-full h-full text-gray-500 dark:text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
ChevronUp,
|
||||
ChevronDown,
|
||||
SquareArrowOutUpRight,
|
||||
File,
|
||||
Globe,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -80,7 +80,7 @@ export function SourceResult({ text }: SourceResultProps) {
|
||||
>
|
||||
<div className="flex-1 min-w-0 flex items-center gap-2">
|
||||
<div className="flex-1 min-w-0 flex items-center gap-1">
|
||||
<File className="w-3 h-3" />
|
||||
<Globe className="w-3 h-3" />
|
||||
<div className="text-xs text-[#333333] dark:text-[#D8D8D8] truncate font-normal group-hover:text-[#0072FF] dark:group-hover:text-[#0072FF]">
|
||||
{item.title || item.category}
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { ArrowDown01, Command, CornerDownLeft } from "lucide-react";
|
||||
import {
|
||||
ArrowDown01,
|
||||
Command,
|
||||
CornerDownLeft,
|
||||
Pin,
|
||||
PinOff,
|
||||
} from "lucide-react";
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
|
||||
import logoImg from "@/assets/icon.svg";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { isMac } from "@/utils/keyboardUtils";
|
||||
import TypeIcon from "@/components/Common/Icons/TypeIcon";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
|
||||
interface FooterProps {
|
||||
isChat: boolean;
|
||||
@@ -16,10 +24,24 @@ export default function Footer({}: FooterProps) {
|
||||
const { t } = useTranslation();
|
||||
const sourceData = useSearchStore((state) => state.sourceData);
|
||||
|
||||
const isPinned = useAppStore((state) => state.isPinned);
|
||||
const setIsPinned = useAppStore((state) => state.setIsPinned);
|
||||
|
||||
function openSetting() {
|
||||
emit("open_settings", "");
|
||||
}
|
||||
|
||||
const togglePin = async () => {
|
||||
try {
|
||||
const newPinned = !isPinned;
|
||||
await getCurrentWindow().setAlwaysOnTop(newPinned);
|
||||
setIsPinned(newPinned);
|
||||
} catch (err) {
|
||||
console.error("Failed to toggle window pin state:", err);
|
||||
setIsPinned(isPinned);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
@@ -34,18 +56,31 @@ export default function Footer({}: FooterProps) {
|
||||
src={logoImg}
|
||||
className="w-4 h-4 cursor-pointer"
|
||||
onClick={openSetting}
|
||||
alt={t('search.footer.logoAlt')}
|
||||
alt={t("search.footer.logoAlt")}
|
||||
/>
|
||||
)}
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{sourceData?.source?.name || t('search.footer.version', { version: 'v1.0.0' })}
|
||||
{sourceData?.source?.name ||
|
||||
t("search.footer.version", { version: process.env.VERSION || "v1.0.0" })}
|
||||
</span>
|
||||
<button
|
||||
onClick={togglePin}
|
||||
className={`rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 ${
|
||||
isPinned ? "text-blue-500" : ""
|
||||
}`}
|
||||
>
|
||||
{isPinned ? (
|
||||
<Pin className="h-3 w-3" />
|
||||
) : (
|
||||
<PinOff className="h-3 w-3" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="gap-1 flex items-center text-[#666] dark:text-[#666] text-xs">
|
||||
<span className="mr-1.5">{t('search.footer.select')}:</span>
|
||||
<span className="mr-1.5">{t("search.footer.select")}:</span>
|
||||
<kbd className="coco-modal-footer-commands-key pr-1">
|
||||
{isMac ? (
|
||||
<Command className="w-3 h-3" />
|
||||
@@ -61,7 +96,7 @@ export default function Footer({}: FooterProps) {
|
||||
</kbd>
|
||||
</div>
|
||||
<div className="flex items-center text-[#666] dark:text-[#666] text-xs">
|
||||
<span className="mr-1.5">{t('search.footer.open')}: </span>
|
||||
<span className="mr-1.5">{t("search.footer.open")}: </span>
|
||||
<kbd className="coco-modal-footer-commands-key pr-1">
|
||||
<CornerDownLeft className="w-3 h-3" />
|
||||
</kbd>
|
||||
|
||||
@@ -47,6 +47,8 @@ export default function ChatInput({
|
||||
(state: { showTooltip: boolean }) => state.showTooltip
|
||||
);
|
||||
|
||||
const isPinned = useAppStore((state) => state.isPinned);
|
||||
|
||||
const sourceData = useSearchStore(
|
||||
(state: { sourceData: any }) => state.sourceData
|
||||
);
|
||||
@@ -84,7 +86,7 @@ export default function ChatInput({
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
console.log("handleKeyDown", e.code, e.key);
|
||||
// console.log("handleKeyDown", e.code, e.key);
|
||||
|
||||
if (e.key === "Escape") {
|
||||
console.log("Escape:" + inputValue);
|
||||
@@ -93,13 +95,12 @@ export default function ChatInput({
|
||||
return;
|
||||
} else {
|
||||
console.log("empty value, but Escape key pressed.");
|
||||
invoke("hide_coco")
|
||||
.then(() => {
|
||||
console.log("Hide Coco");
|
||||
})
|
||||
.finally(() => {
|
||||
console.log("Hide Coco");
|
||||
});
|
||||
if (isPinned) {
|
||||
return;
|
||||
}
|
||||
invoke("hide_coco").then(() => {
|
||||
console.log("Hide Coco");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function AboutView() {
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-8 text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('settings.about.version', { version: '1.0.0' })}
|
||||
{t('settings.about.version', { version: process.env.VERSION || "v1.0.0" })}
|
||||
</div>
|
||||
<div className="mt-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('settings.about.copyright', { year: new Date().getFullYear() })}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useEffect, useCallback } from "react";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { listen} from "@tauri-apps/api/event";
|
||||
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
|
||||
import { isMetaOrCtrlKey } from "@/utils/keyboardUtils";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
|
||||
interface CreateWindowOptions {
|
||||
label?: string;
|
||||
title?: string;
|
||||
@@ -15,6 +17,8 @@ interface CreateWindowOptions {
|
||||
}
|
||||
|
||||
export default function useSettingsWindow() {
|
||||
const setTabIndex = useAppStore((state) => state.setTabIndex);
|
||||
|
||||
const openSettingsWindow = useCallback((tab?: string) => {
|
||||
const url = tab ? `/ui/settings?tab=${tab}` : `/ui/settings`;
|
||||
const options: CreateWindowOptions = {
|
||||
@@ -65,8 +69,9 @@ export default function useSettingsWindow() {
|
||||
useEffect(() => {
|
||||
const unlisten = listen("open_settings", (event) => {
|
||||
console.log("open_settings event received:", event);
|
||||
const tab = event.payload as string | undefined;
|
||||
const tab = event.payload as string | "";
|
||||
|
||||
setTabIndex(tab)
|
||||
openSettingsWindow(tab);
|
||||
});
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
@@ -16,20 +16,22 @@ export default function DesktopApp() {
|
||||
(state) => state.initializeListeners
|
||||
);
|
||||
|
||||
const isPinned = useAppStore((state) => state.isPinned);
|
||||
|
||||
useEffect(() => {
|
||||
initializeListeners();
|
||||
initializeListeners_auth();
|
||||
|
||||
// Listen for window focus and blur events
|
||||
const handleBlur = () => {
|
||||
const handleBlur = async () => {
|
||||
console.log("Window blurred");
|
||||
invoke("hide_coco")
|
||||
.then(() => {
|
||||
console.log("Hide Coco");
|
||||
})
|
||||
.finally(() => {
|
||||
console.log("Hide Coco");
|
||||
});
|
||||
if (isPinned) {
|
||||
return;
|
||||
}
|
||||
|
||||
invoke("hide_coco").then(() => {
|
||||
console.log("Hide Coco");
|
||||
});
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
@@ -45,7 +47,7 @@ export default function DesktopApp() {
|
||||
window.removeEventListener("blur", handleBlur);
|
||||
window.removeEventListener("focus", handleFocus);
|
||||
};
|
||||
}, []);
|
||||
}, [isPinned]);
|
||||
|
||||
const chatAIRef = useRef<ChatAIRef>(null);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react";
|
||||
import { Settings, Puzzle, Settings2, Info, Server } from "lucide-react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import SettingsPanel from "@/components/Settings/SettingsPanel";
|
||||
@@ -10,17 +9,13 @@ import AboutView from "@/components/Settings/AboutView";
|
||||
import Cloud from "@/components/Cloud/Cloud.tsx"
|
||||
import Footer from "@/components/Footer";
|
||||
import ApiDetails from "@/components/Common/ApiDetails";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
|
||||
|
||||
|
||||
|
||||
function SettingsPage() {
|
||||
const { t } = useTranslation();
|
||||
const [defaultIndex, setDefaultIndex] = useState<number>(0);
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const name = searchParams.get("tab");
|
||||
|
||||
useEffect(() => {
|
||||
setDefaultIndex(name === "about" ? 5 : 0);
|
||||
}, [name]);
|
||||
|
||||
const tabs = [
|
||||
{ name: t('settings.tabs.general'), icon: Settings },
|
||||
@@ -29,6 +24,9 @@ function SettingsPage() {
|
||||
{ name: t('settings.tabs.advanced'), icon: Settings2 },
|
||||
{ name: t('settings.tabs.about'), icon: Info },
|
||||
];
|
||||
|
||||
const tabIndex = useAppStore((state) => state.tabIndex);
|
||||
const [defaultIndex, setDefaultIndex] = useState<number>(tabIndex);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createBrowserRouter } from "react-router-dom";
|
||||
import {createBrowserRouter} from "react-router-dom";
|
||||
|
||||
import Layout from "./layout";
|
||||
import ErrorPage from "@/error-page";
|
||||
@@ -7,14 +7,14 @@ import SettingsPage from "@/pages/settings/index";
|
||||
import ChatAI from "@/pages/chat/index";
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <Layout />,
|
||||
errorElement: <ErrorPage />,
|
||||
children: [
|
||||
{ path: "/ui", element: <DesktopApp /> },
|
||||
{ path: "/ui/settings", element: <SettingsPage /> },
|
||||
{ path: "/ui/chat", element: <ChatAI /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
element: <Layout/>,
|
||||
errorElement: <ErrorPage/>,
|
||||
children: [
|
||||
{path: "/ui", element: <DesktopApp/>},
|
||||
{path: "/ui/settings", element: <SettingsPage/>},
|
||||
{ path: "/ui/chat", element: <ChatAI /> },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -6,6 +6,17 @@ import { AppEndpoint } from "@/utils/tauri"
|
||||
|
||||
const ENDPOINT_CHANGE_EVENT = 'endpoint-changed';
|
||||
|
||||
export interface IServer {
|
||||
id: string;
|
||||
name: string;
|
||||
available: boolean;
|
||||
endpoint: string;
|
||||
provider: {
|
||||
icon: string;
|
||||
};
|
||||
assistantCount?: number;
|
||||
}
|
||||
|
||||
export type IAppStore = {
|
||||
showTooltip: boolean;
|
||||
setShowTooltip: (showTooltip: boolean) => void;
|
||||
@@ -25,6 +36,12 @@ export type IAppStore = {
|
||||
setEndpoint: (endpoint: AppEndpoint) => void,
|
||||
language: string;
|
||||
setLanguage: (language: string) => void;
|
||||
tabIndex: number;
|
||||
setTabIndex: (tabName: string) => void;
|
||||
isPinned: boolean,
|
||||
setIsPinned: (isPinned: boolean) => void,
|
||||
activeServer: IServer | null,
|
||||
setActiveServer: (activeServer: IServer | null) => void,
|
||||
initializeListeners: () => void;
|
||||
};
|
||||
|
||||
@@ -65,6 +82,21 @@ export const useAppStore = create<IAppStore>()(
|
||||
},
|
||||
language: "en",
|
||||
setLanguage: (language: string) => set({ language }),
|
||||
tabIndex: 0,
|
||||
setTabIndex: (tabName: string) => {
|
||||
const tabIndexMap: { [key: string]: number } = {
|
||||
'general': 0,
|
||||
'extensions': 1,
|
||||
'connect': 2,
|
||||
'advanced': 3,
|
||||
'about': 4
|
||||
};
|
||||
set({ tabIndex: tabIndexMap[tabName || "general"] || 0 })
|
||||
},
|
||||
isPinned: false,
|
||||
setIsPinned: (isPinned: boolean) => set({ isPinned }),
|
||||
activeServer: null,
|
||||
setActiveServer: (activeServer: IServer | null) => set({ activeServer }),
|
||||
initializeListeners: () => {
|
||||
listen(ENDPOINT_CHANGE_EVENT, (event: any) => {
|
||||
const { endpoint, endpoint_http, endpoint_websocket } = event.payload;
|
||||
@@ -83,6 +115,7 @@ export const useAppStore = create<IAppStore>()(
|
||||
endpoint_http: state.endpoint_http,
|
||||
endpoint_websocket: state.endpoint_websocket,
|
||||
language: state.language,
|
||||
activeServer: state.activeServer,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -11,6 +11,8 @@ export type IChatStore = {
|
||||
setStopChat: (value: boolean) => void;
|
||||
connected: boolean;
|
||||
setConnected: (value: boolean) => void;
|
||||
messages: string;
|
||||
setMessages: (value: string | ((prev: string) => string)) => void;
|
||||
};
|
||||
|
||||
export const useChatStore = create<IChatStore>()(
|
||||
@@ -22,6 +24,11 @@ export const useChatStore = create<IChatStore>()(
|
||||
setStopChat: (value: boolean) => set(() => ({ stopChat: value })),
|
||||
connected: false,
|
||||
setConnected: (value: boolean) => set(() => ({ connected: value })),
|
||||
messages: "",
|
||||
setMessages: (value: string | ((prev: string) => string)) =>
|
||||
set((state) => ({
|
||||
messages: typeof value === "function" ? value(state.messages) : value,
|
||||
})),
|
||||
}),
|
||||
{
|
||||
name: "chat-state",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import path from 'path';
|
||||
import { config } from "dotenv";
|
||||
import packageJson from './package.json';
|
||||
|
||||
config();
|
||||
|
||||
@@ -10,6 +11,9 @@ const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
define: {
|
||||
'process.env.VERSION': JSON.stringify(packageJson.version),
|
||||
},
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
Reference in New Issue
Block a user