feat(deps): add desktop UI support and update dependencies
Added new dependencies for desktop UI support including color-eyre, crossterm, and ratatui. Updated existing dependencies and modified Cargo.toml to include a new 'desktop' feature flag. Also cleaned up the contributors list and modified the add-req.sh script to focus on core bot functionality. The desktop UI support enables better terminal-based interfaces while the dependency updates ensure compatibility and security. The script changes reflect a shift in focus areas for the project.
This commit is contained in:
parent
5379e21bfe
commit
7fa6ea9f6a
10 changed files with 1480 additions and 115 deletions
358
Cargo.lock
generated
358
Cargo.lock
generated
|
|
@ -252,6 +252,15 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "addr2line"
|
||||||
|
version = "0.25.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
|
||||||
|
dependencies = [
|
||||||
|
"gimli",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "adler2"
|
name = "adler2"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
|
|
@ -423,7 +432,7 @@ version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a"
|
checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"object",
|
"object 0.32.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -991,6 +1000,21 @@ dependencies = [
|
||||||
"tower-service",
|
"tower-service",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "backtrace"
|
||||||
|
version = "0.3.76"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
|
||||||
|
dependencies = [
|
||||||
|
"addr2line",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"miniz_oxide",
|
||||||
|
"object 0.37.3",
|
||||||
|
"rustc-demangle",
|
||||||
|
"windows-link 0.2.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base16ct"
|
name = "base16ct"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
|
|
@ -1106,7 +1130,9 @@ dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"color-eyre",
|
||||||
"cron",
|
"cron",
|
||||||
|
"crossterm 0.29.0",
|
||||||
"csv",
|
"csv",
|
||||||
"diesel",
|
"diesel",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
|
|
@ -1129,6 +1155,7 @@ dependencies = [
|
||||||
"pdf-extract",
|
"pdf-extract",
|
||||||
"qdrant-client",
|
"qdrant-client",
|
||||||
"rand 0.9.2",
|
"rand 0.9.2",
|
||||||
|
"ratatui",
|
||||||
"redis",
|
"redis",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
|
@ -1250,6 +1277,21 @@ dependencies = [
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cassowary"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "castaway"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cbc"
|
name = "cbc"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
|
@ -1402,7 +1444,34 @@ checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"termcolor",
|
"termcolor",
|
||||||
"unicode-width",
|
"unicode-width 0.1.14",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "color-eyre"
|
||||||
|
version = "0.6.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d"
|
||||||
|
dependencies = [
|
||||||
|
"backtrace",
|
||||||
|
"color-spantrace",
|
||||||
|
"eyre",
|
||||||
|
"indenter",
|
||||||
|
"once_cell",
|
||||||
|
"owo-colors",
|
||||||
|
"tracing-error",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "color-spantrace"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"owo-colors",
|
||||||
|
"tracing-core",
|
||||||
|
"tracing-error",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1434,6 +1503,20 @@ dependencies = [
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "compact_str"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
|
||||||
|
dependencies = [
|
||||||
|
"castaway",
|
||||||
|
"cfg-if",
|
||||||
|
"itoa",
|
||||||
|
"rustversion",
|
||||||
|
"ryu",
|
||||||
|
"static_assertions",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "console"
|
name = "console"
|
||||||
version = "0.16.1"
|
version = "0.16.1"
|
||||||
|
|
@ -1443,7 +1526,7 @@ dependencies = [
|
||||||
"encode_unicode",
|
"encode_unicode",
|
||||||
"libc",
|
"libc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"unicode-width",
|
"unicode-width 0.2.0",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1491,6 +1574,15 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "convert_case"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
version = "0.16.2"
|
version = "0.16.2"
|
||||||
|
|
@ -1591,6 +1683,49 @@ version = "0.8.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm"
|
||||||
|
version = "0.28.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"crossterm_winapi",
|
||||||
|
"mio",
|
||||||
|
"parking_lot",
|
||||||
|
"rustix 0.38.44",
|
||||||
|
"signal-hook",
|
||||||
|
"signal-hook-mio",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"crossterm_winapi",
|
||||||
|
"derive_more 2.0.1",
|
||||||
|
"document-features",
|
||||||
|
"mio",
|
||||||
|
"parking_lot",
|
||||||
|
"rustix 1.1.2",
|
||||||
|
"signal-hook",
|
||||||
|
"signal-hook-mio",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm_winapi"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crunchy"
|
name = "crunchy"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
|
|
@ -1871,7 +2006,7 @@ version = "0.99.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
|
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"convert_case",
|
"convert_case 0.4.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
|
|
@ -1893,6 +2028,7 @@ version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
|
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"convert_case 0.7.1",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
|
|
@ -1960,6 +2096,15 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "document-features"
|
||||||
|
version = "0.2.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
|
||||||
|
dependencies = [
|
||||||
|
"litrs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dotenvy"
|
name = "dotenvy"
|
||||||
version = "0.15.7"
|
version = "0.15.7"
|
||||||
|
|
@ -2131,6 +2276,16 @@ dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "eyre"
|
||||||
|
version = "0.6.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
|
||||||
|
dependencies = [
|
||||||
|
"indenter",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
|
|
@ -2363,6 +2518,12 @@ dependencies = [
|
||||||
"polyval",
|
"polyval",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gimli"
|
||||||
|
version = "0.32.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glob"
|
name = "glob"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
|
|
@ -2881,6 +3042,12 @@ dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indenter"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.9.3"
|
version = "1.9.3"
|
||||||
|
|
@ -2909,11 +3076,20 @@ checksum = "ade6dfcba0dfb62ad59e59e7241ec8912af34fd29e0e743e3db992bd278e8b65"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console",
|
"console",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"unicode-width",
|
"unicode-width 0.2.0",
|
||||||
"unit-prefix",
|
"unit-prefix",
|
||||||
"web-time",
|
"web-time",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indoc"
|
||||||
|
version = "2.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inout"
|
name = "inout"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
|
|
@ -2924,6 +3100,19 @@ dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "instability"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a"
|
||||||
|
dependencies = [
|
||||||
|
"darling 0.20.11",
|
||||||
|
"indoc",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.11.0"
|
version = "2.11.0"
|
||||||
|
|
@ -3156,6 +3345,12 @@ dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.4.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
|
|
@ -3168,6 +3363,12 @@ version = "0.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
|
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "litrs"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "livekit"
|
name = "livekit"
|
||||||
version = "0.7.24"
|
version = "0.7.24"
|
||||||
|
|
@ -3590,6 +3791,15 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "object"
|
||||||
|
version = "0.37.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.3"
|
||||||
|
|
@ -3685,6 +3895,12 @@ version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
|
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "owo-colors"
|
||||||
|
version = "4.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "p256"
|
name = "p256"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
|
|
@ -3747,6 +3963,12 @@ dependencies = [
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paste"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pbjson"
|
name = "pbjson"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
|
|
@ -4250,6 +4472,27 @@ version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223"
|
checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ratatui"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cassowary",
|
||||||
|
"compact_str",
|
||||||
|
"crossterm 0.28.1",
|
||||||
|
"indoc",
|
||||||
|
"instability",
|
||||||
|
"itertools 0.13.0",
|
||||||
|
"lru",
|
||||||
|
"paste",
|
||||||
|
"strum",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode-truncate",
|
||||||
|
"unicode-width 0.2.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redis"
|
name = "redis"
|
||||||
version = "0.27.6"
|
version = "0.27.6"
|
||||||
|
|
@ -4419,6 +4662,12 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-demangle"
|
||||||
|
version = "0.1.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
|
|
@ -4434,6 +4683,19 @@ dependencies = [
|
||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "0.38.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys 0.4.15",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
|
|
@ -4443,7 +4705,7 @@ dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys 0.11.0",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -4764,6 +5026,27 @@ version = "1.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook"
|
||||||
|
version = "0.3.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"signal-hook-registry",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-mio"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"signal-hook",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.6"
|
version = "1.4.6"
|
||||||
|
|
@ -4890,6 +5173,28 @@ version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.26.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||||
|
dependencies = [
|
||||||
|
"strum_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.26.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||||
|
dependencies = [
|
||||||
|
"heck 0.5.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustversion",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
|
|
@ -4971,7 +5276,7 @@ dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix 1.1.2",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -5352,6 +5657,16 @@ dependencies = [
|
||||||
"valuable",
|
"valuable",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-error"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db"
|
||||||
|
dependencies = [
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-log"
|
name = "tracing-log"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -5455,10 +5770,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
|
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-segmentation"
|
||||||
version = "0.2.2"
|
version = "1.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-truncate"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
|
||||||
|
dependencies = [
|
||||||
|
"itertools 0.13.0",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode-width 0.1.14",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
|
|
|
||||||
72
Cargo.toml
72
Cargo.toml
|
|
@ -3,46 +3,49 @@ name = "botserver"
|
||||||
version = "6.0.7"
|
version = "6.0.7"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = [
|
authors = [
|
||||||
"Pragmatismo.com.br <contact@pragmatismo.com.br>",
|
"Pragmatismo.com.br <contact@pragmatismo.com.br>",
|
||||||
"General Bots Community <https://github.com/GeneralBots>",
|
"General Bots Community <https://github.com/GeneralBots>",
|
||||||
"Alan Perdomo",
|
"Alan Perdomo",
|
||||||
"Ana Paula Gil",
|
"Ana Paula Gil",
|
||||||
"Arenas.io",
|
"Arenas.io",
|
||||||
"Atylla L",
|
"Atylla L",
|
||||||
"Christopher de Castilho",
|
"Christopher de Castilho",
|
||||||
"Dario Junior",
|
"Dario Junior",
|
||||||
"David Lerner",
|
"David Lerner",
|
||||||
"Experimentation Garage",
|
"Experimentation Garage",
|
||||||
"Flavio Andrade",
|
"Flavio Andrade",
|
||||||
"Heraldo Almeida",
|
"Heraldo Almeida",
|
||||||
"Joao Parana",
|
"Joao Parana",
|
||||||
"Jonathas C",
|
"Jonathas C",
|
||||||
"J Ramos",
|
"J Ramos",
|
||||||
"Lucas Picanco",
|
"Lucas Picanco",
|
||||||
"Marcos Velasco",
|
"Marcos Velasco",
|
||||||
"Matheus 39x",
|
"Matheus 39x",
|
||||||
"Oerlabs Henrique",
|
"Oerlabs Henrique",
|
||||||
"Othon Lima",
|
"Othon Lima",
|
||||||
"PH Nascimento",
|
"PH Nascimento",
|
||||||
"Phpussente",
|
"Phpussente",
|
||||||
"Robson Dantas",
|
"Robson Dantas",
|
||||||
"Rodrigo Rodriguez <me@rodrigorodriguez.com>",
|
"Rodrigo Rodriguez <me@rodrigorodriguez.com>",
|
||||||
"Sarah Lourenco",
|
"Sarah Lourenco",
|
||||||
"Thi Patriota",
|
"Thi Patriota",
|
||||||
"Webgus",
|
"Webgus",
|
||||||
"Zuilho Se",
|
"Zuilho Se",
|
||||||
]
|
]
|
||||||
description = "General Bots Server - Open-source bot platform by Pragmatismo.com.br"
|
description = "General Bots Server - Open-source bot platform by Pragmatismo.com.br"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
repository = "https://github.com/GeneralBots/BotServer"
|
repository = "https://github.com/GeneralBots/BotServer"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [ "vectordb"]
|
default = [ "vectordb", "desktop"]
|
||||||
vectordb = ["qdrant-client"]
|
vectordb = ["qdrant-client"]
|
||||||
email = ["imap"]
|
email = ["imap"]
|
||||||
desktop = []
|
desktop = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
color-eyre = "0.6.5"
|
||||||
|
crossterm = "0.29.0"
|
||||||
|
ratatui = "0.29.0"
|
||||||
scopeguard = "1.2.0"
|
scopeguard = "1.2.0"
|
||||||
once_cell = "1.18.0"
|
once_cell = "1.18.0"
|
||||||
actix-cors = "0.7"
|
actix-cors = "0.7"
|
||||||
|
|
@ -101,10 +104,9 @@ urlencoding = "2.1"
|
||||||
uuid = { version = "1.11", features = ["serde", "v4"] }
|
uuid = { version = "1.11", features = ["serde", "v4"] }
|
||||||
zip = "2.2"
|
zip = "2.2"
|
||||||
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true # Enables Link-Time Optimization
|
lto = true
|
||||||
opt-level = "z" # Optimizes for size instead of speed
|
opt-level = "z"
|
||||||
strip = true # Strips debug symbols
|
strip = true
|
||||||
panic = "abort" # Reduces size by removing panic unwinding
|
panic = "abort"
|
||||||
codegen-units = 1 # More aggressive optimization
|
codegen-units = 1
|
||||||
|
|
|
||||||
38
add-req.sh
38
add-req.sh
|
|
@ -21,31 +21,31 @@ for file in "${prompts[@]}"; do
|
||||||
done
|
done
|
||||||
|
|
||||||
dirs=(
|
dirs=(
|
||||||
"auth"
|
#"auth"
|
||||||
"automation"
|
#"automation"
|
||||||
"basic"
|
#"basic"
|
||||||
"bootstrap"
|
#"bootstrap"
|
||||||
"bot"
|
"bot"
|
||||||
"channels"
|
#"channels"
|
||||||
"config"
|
#"config"
|
||||||
"context"
|
#"context"
|
||||||
"drive_monitor"
|
"drive_monitor"
|
||||||
"email"
|
#"email"
|
||||||
"file"
|
"file"
|
||||||
# "kb"
|
# "kb"
|
||||||
"llm"
|
"llm"
|
||||||
"llm_models"
|
#"llm_models"
|
||||||
"org"
|
#"org"
|
||||||
"package"
|
#"package_manager"
|
||||||
"package_manager"
|
#"riot_compiler"
|
||||||
"riot_compiler"
|
#"session"
|
||||||
"session"
|
|
||||||
"shared"
|
"shared"
|
||||||
"tests"
|
#"tests"
|
||||||
"tools"
|
#"tools"
|
||||||
"ui"
|
#"ui"
|
||||||
"web_server"
|
"ui_tree"
|
||||||
"web_automation"
|
#"web_server"
|
||||||
|
#"web_automation"
|
||||||
)
|
)
|
||||||
|
|
||||||
filter_rust_file() {
|
filter_rust_file() {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,6 @@ impl ModelHandler for GptOss20bHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_analysis_markers(&self, buffer: &str) -> bool {
|
fn has_analysis_markers(&self, buffer: &str) -> bool {
|
||||||
buffer.contains("**")
|
buffer.contains("analysis<|message|>")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
92
src/main.rs
92
src/main.rs
|
|
@ -27,10 +27,8 @@ mod package_manager;
|
||||||
mod session;
|
mod session;
|
||||||
mod shared;
|
mod shared;
|
||||||
pub mod tests;
|
pub mod tests;
|
||||||
#[cfg(feature = "desktop")]
|
mod ui_tree;
|
||||||
mod ui;
|
|
||||||
mod web_server;
|
mod web_server;
|
||||||
|
|
||||||
use crate::auth::auth_handler;
|
use crate::auth::auth_handler;
|
||||||
use crate::automation::AutomationService;
|
use crate::automation::AutomationService;
|
||||||
use crate::bootstrap::BootstrapManager;
|
use crate::bootstrap::BootstrapManager;
|
||||||
|
|
@ -49,14 +47,12 @@ use crate::session::{create_session, get_session_history, get_sessions, start_se
|
||||||
use crate::shared::state::AppState;
|
use crate::shared::state::AppState;
|
||||||
use crate::web_server::{bot_index, index, static_files};
|
use crate::web_server::{bot_index, index, static_files};
|
||||||
|
|
||||||
#[cfg(not(feature = "desktop"))]
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
use botserver::config::ConfigManager;
|
|
||||||
|
|
||||||
use crate::llm::local::ensure_llama_servers_running;
|
use crate::llm::local::ensure_llama_servers_running;
|
||||||
|
use botserver::config::ConfigManager;
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
let no_ui = args.contains(&"--noui".to_string());
|
||||||
if args.len() > 1 {
|
if args.len() > 1 {
|
||||||
let command = &args[1];
|
let command = &args[1];
|
||||||
match command.as_str() {
|
match command.as_str() {
|
||||||
|
|
@ -71,6 +67,7 @@ async fn main() -> std::io::Result<()> {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"--noui" => {}
|
||||||
_ => {
|
_ => {
|
||||||
eprintln!("Unknown command: {}", command);
|
eprintln!("Unknown command: {}", command);
|
||||||
eprintln!("Run 'botserver --help' for usage information");
|
eprintln!("Run 'botserver --help' for usage information");
|
||||||
|
|
@ -81,34 +78,49 @@ async fn main() -> std::io::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rest of the original main function remains unchanged...
|
|
||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
|
let ui_handle = if !no_ui {
|
||||||
.write_style(env_logger::WriteStyle::Always)
|
let (ui_tx, mut ui_rx) = tokio::sync::mpsc::channel::<Arc<AppState>>(1);
|
||||||
.init();
|
let handle = std::thread::Builder::new()
|
||||||
|
.name("ui-thread".to_string())
|
||||||
|
.spawn(move || {
|
||||||
|
let mut ui = crate::ui_tree::XtreeUI::new();
|
||||||
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.expect("Failed to create UI runtime");
|
||||||
|
rt.block_on(async {
|
||||||
|
if let Some(app_state) = ui_rx.recv().await {
|
||||||
|
ui.set_app_state(app_state);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Err(e) = ui.start_ui() {
|
||||||
|
eprintln!("UI error: {}", e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.expect("Failed to spawn UI thread");
|
||||||
|
Some((handle, ui_tx))
|
||||||
|
} else {
|
||||||
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
|
||||||
|
.write_style(env_logger::WriteStyle::Always)
|
||||||
|
.init();
|
||||||
|
None
|
||||||
|
};
|
||||||
let install_mode = if args.contains(&"--container".to_string()) {
|
let install_mode = if args.contains(&"--container".to_string()) {
|
||||||
InstallMode::Container
|
InstallMode::Container
|
||||||
} else {
|
} else {
|
||||||
InstallMode::Local
|
InstallMode::Local
|
||||||
};
|
};
|
||||||
|
|
||||||
let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") {
|
let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") {
|
||||||
args.get(idx + 1).cloned()
|
args.get(idx + 1).cloned()
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut bootstrap = BootstrapManager::new(install_mode.clone(), tenant.clone()).await;
|
let mut bootstrap = BootstrapManager::new(install_mode.clone(), tenant.clone()).await;
|
||||||
|
|
||||||
// Prevent double bootstrap: skip if environment already initialized
|
|
||||||
let env_path = std::env::current_dir()?
|
let env_path = std::env::current_dir()?
|
||||||
.join("botserver-stack")
|
.join("botserver-stack")
|
||||||
.join(".env");
|
.join(".env");
|
||||||
let cfg = if env_path.exists() {
|
let cfg = if env_path.exists() {
|
||||||
info!("Environment already initialized, skipping bootstrap");
|
|
||||||
|
|
||||||
match diesel::Connection::establish(&std::env::var("DATABASE_URL").unwrap()) {
|
match diesel::Connection::establish(&std::env::var("DATABASE_URL").unwrap()) {
|
||||||
Ok(mut conn) => {
|
Ok(mut conn) => {
|
||||||
AppConfig::from_database(&mut conn).expect("Failed to load config from DB")
|
AppConfig::from_database(&mut conn).expect("Failed to load config from DB")
|
||||||
|
|
@ -117,16 +129,11 @@ async fn main() -> std::io::Result<()> {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match bootstrap.bootstrap().await {
|
match bootstrap.bootstrap().await {
|
||||||
Ok(config) => {
|
Ok(config) => config,
|
||||||
info!("Bootstrap completed successfully");
|
|
||||||
config
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Bootstrap failed: {}", e);
|
log::error!("Bootstrap failed: {}", e);
|
||||||
match diesel::Connection::establish(
|
match diesel::Connection::establish(
|
||||||
&std::env::var("DATABASE_URL").unwrap_or_else(|_| {
|
&std::env::var("DATABASE_URL").unwrap()
|
||||||
"postgres://gbuser:@localhost:5432/botserver".to_string()
|
|
||||||
}),
|
|
||||||
) {
|
) {
|
||||||
Ok(mut conn) => {
|
Ok(mut conn) => {
|
||||||
AppConfig::from_database(&mut conn).expect("Failed to load config from DB")
|
AppConfig::from_database(&mut conn).expect("Failed to load config from DB")
|
||||||
|
|
@ -136,18 +143,12 @@ async fn main() -> std::io::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start all services (synchronous)
|
|
||||||
if let Err(e) = bootstrap.start_all() {
|
if let Err(e) = bootstrap.start_all() {
|
||||||
log::warn!("Failed to start all services: {}", e);
|
log::warn!("Failed to start all services: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload templates (asynchronous)
|
|
||||||
if let Err(e) = futures::executor::block_on(bootstrap.upload_templates_to_drive(&cfg)) {
|
if let Err(e) = futures::executor::block_on(bootstrap.upload_templates_to_drive(&cfg)) {
|
||||||
log::warn!("Failed to upload templates to MinIO: {}", e);
|
log::warn!("Failed to upload templates to MinIO: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh configuration from environment to ensure latest DATABASE_URL and credentials
|
|
||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
let refreshed_cfg = AppConfig::from_env().expect("Failed to load config from env");
|
let refreshed_cfg = AppConfig::from_env().expect("Failed to load config from env");
|
||||||
let config = std::sync::Arc::new(refreshed_cfg.clone());
|
let config = std::sync::Arc::new(refreshed_cfg.clone());
|
||||||
|
|
@ -161,31 +162,26 @@ async fn main() -> std::io::Result<()> {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let cache_url = std::env::var("CACHE_URL")
|
let cache_url = std::env::var("CACHE_URL")
|
||||||
.or_else(|_| std::env::var("REDIS_URL"))
|
.or_else(|_| std::env::var("REDIS_URL"))
|
||||||
.unwrap_or_else(|_| "redis://localhost:6379".to_string());
|
.unwrap_or_else(|_| "redis://localhost:6379".to_string());
|
||||||
let redis_client = match redis::Client::open(cache_url.as_str()) {
|
let redis_client = match redis::Client::open(cache_url.as_str()) {
|
||||||
Ok(client) => Some(Arc::new(client)),
|
Ok(client) => Some(Arc::new(client)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("Failed to connect to Redis: Redis URL did not parse- {}", e);
|
log::warn!("Failed to connect to Redis: {}", e);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let web_adapter = Arc::new(WebChannelAdapter::new());
|
let web_adapter = Arc::new(WebChannelAdapter::new());
|
||||||
let voice_adapter = Arc::new(VoiceAdapter::new());
|
let voice_adapter = Arc::new(VoiceAdapter::new());
|
||||||
|
|
||||||
let drive = init_drive(&config.drive)
|
let drive = init_drive(&config.drive)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to initialize Drive");
|
.expect("Failed to initialize Drive");
|
||||||
|
|
||||||
let session_manager = Arc::new(tokio::sync::Mutex::new(session::SessionManager::new(
|
let session_manager = Arc::new(tokio::sync::Mutex::new(session::SessionManager::new(
|
||||||
diesel::Connection::establish(&cfg.database_url()).unwrap(),
|
diesel::Connection::establish(&cfg.database_url()).unwrap(),
|
||||||
redis_client.clone(),
|
redis_client.clone(),
|
||||||
)));
|
)));
|
||||||
|
|
||||||
let auth_service = Arc::new(tokio::sync::Mutex::new(auth::AuthService::new()));
|
let auth_service = Arc::new(tokio::sync::Mutex::new(auth::AuthService::new()));
|
||||||
|
|
||||||
let conn = diesel::Connection::establish(&cfg.database_url()).unwrap();
|
let conn = diesel::Connection::establish(&cfg.database_url()).unwrap();
|
||||||
let config_manager = ConfigManager::new(Arc::new(Mutex::new(conn)));
|
let config_manager = ConfigManager::new(Arc::new(Mutex::new(conn)));
|
||||||
let mut bot_conn = diesel::Connection::establish(&cfg.database_url()).unwrap();
|
let mut bot_conn = diesel::Connection::establish(&cfg.database_url()).unwrap();
|
||||||
|
|
@ -193,17 +189,15 @@ async fn main() -> std::io::Result<()> {
|
||||||
let llm_url = config_manager
|
let llm_url = config_manager
|
||||||
.get_config(&default_bot_id, "llm-url", Some("http://localhost:8081"))
|
.get_config(&default_bot_id, "llm-url", Some("http://localhost:8081"))
|
||||||
.unwrap_or_else(|_| "http://localhost:8081".to_string());
|
.unwrap_or_else(|_| "http://localhost:8081".to_string());
|
||||||
|
|
||||||
let llm_provider = Arc::new(crate::llm::OpenAIClient::new(
|
let llm_provider = Arc::new(crate::llm::OpenAIClient::new(
|
||||||
"empty".to_string(),
|
"empty".to_string(),
|
||||||
Some(llm_url.clone()),
|
Some(llm_url.clone()),
|
||||||
));
|
));
|
||||||
|
|
||||||
let app_state = Arc::new(AppState {
|
let app_state = Arc::new(AppState {
|
||||||
drive: Some(drive),
|
drive: Some(drive),
|
||||||
config: Some(cfg.clone()),
|
config: Some(cfg.clone()),
|
||||||
conn: db_pool.clone(),
|
conn: db_pool.clone(),
|
||||||
bucket_name: "default.gbai".to_string(), // Default bucket name
|
bucket_name: "default.gbai".to_string(),
|
||||||
cache: redis_client.clone(),
|
cache: redis_client.clone(),
|
||||||
session_manager: session_manager.clone(),
|
session_manager: session_manager.clone(),
|
||||||
llm_provider: llm_provider.clone(),
|
llm_provider: llm_provider.clone(),
|
||||||
|
|
@ -220,29 +214,25 @@ async fn main() -> std::io::Result<()> {
|
||||||
web_adapter: web_adapter.clone(),
|
web_adapter: web_adapter.clone(),
|
||||||
voice_adapter: voice_adapter.clone(),
|
voice_adapter: voice_adapter.clone(),
|
||||||
});
|
});
|
||||||
|
if let Some((_, ui_tx)) = &ui_handle {
|
||||||
|
ui_tx.send(app_state.clone()).await.ok();
|
||||||
|
}
|
||||||
info!(
|
info!(
|
||||||
"Starting HTTP server on {}:{}",
|
"Starting HTTP server on {}:{}",
|
||||||
config.server.host, config.server.port
|
config.server.host, config.server.port
|
||||||
);
|
);
|
||||||
|
|
||||||
let worker_count = std::thread::available_parallelism()
|
let worker_count = std::thread::available_parallelism()
|
||||||
.map(|n| n.get())
|
.map(|n| n.get())
|
||||||
.unwrap_or(4);
|
.unwrap_or(4);
|
||||||
|
|
||||||
// Initialize bot orchestrator and mount all bots
|
|
||||||
let bot_orchestrator = BotOrchestrator::new(app_state.clone());
|
let bot_orchestrator = BotOrchestrator::new(app_state.clone());
|
||||||
|
|
||||||
// Mount all active bots from database
|
|
||||||
if let Err(e) = bot_orchestrator.mount_all_bots().await {
|
if let Err(e) = bot_orchestrator.mount_all_bots().await {
|
||||||
log::error!("Failed to mount bots: {}", e);
|
log::error!("Failed to mount bots: {}", e);
|
||||||
// Use BotOrchestrator::send_warning to notify system admins
|
|
||||||
let msg = format!("Bot mount failure: {}", e);
|
let msg = format!("Bot mount failure: {}", e);
|
||||||
let _ = bot_orchestrator
|
let _ = bot_orchestrator
|
||||||
.send_warning("System", "AdminBot", msg.as_str())
|
.send_warning("System", "AdminBot", msg.as_str())
|
||||||
.await;
|
.await;
|
||||||
} else {
|
|
||||||
let _sessions = get_sessions;
|
|
||||||
log::info!("Session handler registered successfully");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let automation_state = app_state.clone();
|
let automation_state = app_state.clone();
|
||||||
|
|
@ -259,7 +249,7 @@ async fn main() -> std::io::Result<()> {
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Err(e) = ensure_llama_servers_running(&app_state).await {
|
if let Err(e) = ensure_llama_servers_running(&app_state).await {
|
||||||
error!("Failed to stat LLM servers: {}", e);
|
error!("Failed to start LLM servers: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
|
|
@ -270,6 +260,7 @@ async fn main() -> std::io::Result<()> {
|
||||||
.max_age(3600);
|
.max_age(3600);
|
||||||
|
|
||||||
let app_state_clone = app_state.clone();
|
let app_state_clone = app_state.clone();
|
||||||
|
|
||||||
let mut app = App::new()
|
let mut app = App::new()
|
||||||
.wrap(cors)
|
.wrap(cors)
|
||||||
.wrap(Logger::default())
|
.wrap(Logger::default())
|
||||||
|
|
@ -301,6 +292,7 @@ async fn main() -> std::io::Result<()> {
|
||||||
.service(save_draft)
|
.service(save_draft)
|
||||||
.service(save_click);
|
.service(save_click);
|
||||||
}
|
}
|
||||||
|
|
||||||
app = app.service(static_files);
|
app = app.service(static_files);
|
||||||
app = app.service(bot_index);
|
app = app.service(bot_index);
|
||||||
app
|
app
|
||||||
|
|
|
||||||
153
src/ui_tree/editor.rs
Normal file
153
src/ui_tree/editor.rs
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
use color_eyre::Result;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use crate::shared::state::AppState;
|
||||||
|
|
||||||
|
pub struct Editor {
|
||||||
|
file_path: String,
|
||||||
|
bucket: String,
|
||||||
|
key: String,
|
||||||
|
content: String,
|
||||||
|
cursor_pos: usize,
|
||||||
|
scroll_offset: usize,
|
||||||
|
modified: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Editor {
|
||||||
|
pub async fn load(app_state: &Arc<AppState>, bucket: &str, path: &str) -> Result<Self> {
|
||||||
|
let content = if let Some(drive) = &app_state.drive {
|
||||||
|
match drive.get_object().bucket(bucket).key(path).send().await {
|
||||||
|
Ok(response) => {
|
||||||
|
let bytes = response.body.collect().await?.into_bytes();
|
||||||
|
String::from_utf8_lossy(&bytes).to_string()
|
||||||
|
}
|
||||||
|
Err(_) => String::new(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
file_path: format!("{}/{}", bucket, path),
|
||||||
|
bucket: bucket.to_string(),
|
||||||
|
key: path.to_string(),
|
||||||
|
content,
|
||||||
|
cursor_pos: 0,
|
||||||
|
scroll_offset: 0,
|
||||||
|
modified: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn save(&mut self, app_state: &Arc<AppState>) -> Result<()> {
|
||||||
|
if let Some(drive) = &app_state.drive {
|
||||||
|
drive.put_object()
|
||||||
|
.bucket(&self.bucket)
|
||||||
|
.key(&self.key)
|
||||||
|
.body(self.content.as_bytes().to_vec().into())
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
self.modified = false;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file_path(&self) -> &str {
|
||||||
|
&self.file_path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&self) -> String {
|
||||||
|
let lines: Vec<&str> = self.content.lines().collect();
|
||||||
|
let total_lines = lines.len().max(1);
|
||||||
|
let visible_lines = 25;
|
||||||
|
let cursor_line = self.content[..self.cursor_pos].lines().count();
|
||||||
|
let cursor_col = self.content[..self.cursor_pos]
|
||||||
|
.lines()
|
||||||
|
.last()
|
||||||
|
.map(|line| line.len())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let start = self.scroll_offset;
|
||||||
|
let end = (start + visible_lines).min(total_lines);
|
||||||
|
let mut display_lines = Vec::new();
|
||||||
|
|
||||||
|
for i in start..end {
|
||||||
|
let line_num = i + 1;
|
||||||
|
let line_content = if i < lines.len() { lines[i] } else { "" };
|
||||||
|
let is_cursor_line = i == cursor_line;
|
||||||
|
let line_marker = if is_cursor_line { "▶" } else { " " };
|
||||||
|
display_lines.push(format!("{} {:4} │ {}", line_marker, line_num, line_content));
|
||||||
|
}
|
||||||
|
|
||||||
|
if display_lines.is_empty() {
|
||||||
|
display_lines.push(" ▶ 1 │ ".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
display_lines.push("".to_string());
|
||||||
|
display_lines.push("─────────────────────────────────────────────────────────────".to_string());
|
||||||
|
let status = if self.modified { "●" } else { "✓" };
|
||||||
|
display_lines.push(format!(" {} {} │ Line: {}, Col: {}",
|
||||||
|
status, self.file_path, cursor_line + 1, cursor_col + 1));
|
||||||
|
display_lines.push(" Ctrl+S: Save │ Ctrl+W: Close │ Esc: Close without saving".to_string());
|
||||||
|
display_lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_up(&mut self) {
|
||||||
|
if let Some(prev_line_end) = self.content[..self.cursor_pos].rfind('\n') {
|
||||||
|
if let Some(prev_prev_line_end) = self.content[..prev_line_end].rfind('\n') {
|
||||||
|
let target_pos = prev_prev_line_end + 1 + (self.cursor_pos - prev_line_end - 1).min(
|
||||||
|
self.content[prev_prev_line_end + 1..prev_line_end].len()
|
||||||
|
);
|
||||||
|
self.cursor_pos = target_pos;
|
||||||
|
} else {
|
||||||
|
self.cursor_pos = (self.cursor_pos - prev_line_end - 1).min(prev_line_end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_down(&mut self) {
|
||||||
|
if let Some(next_line_start) = self.content[self.cursor_pos..].find('\n') {
|
||||||
|
let current_line_start = self.content[..self.cursor_pos].rfind('\n').map(|pos| pos + 1).unwrap_or(0);
|
||||||
|
let next_line_absolute = self.cursor_pos + next_line_start + 1;
|
||||||
|
if let Some(next_next_line_start) = self.content[next_line_absolute..].find('\n') {
|
||||||
|
let target_pos = next_line_absolute + (self.cursor_pos - current_line_start).min(next_next_line_start);
|
||||||
|
self.cursor_pos = target_pos;
|
||||||
|
} else {
|
||||||
|
let target_pos = next_line_absolute + (self.cursor_pos - current_line_start).min(
|
||||||
|
self.content[next_line_absolute..].len()
|
||||||
|
);
|
||||||
|
self.cursor_pos = target_pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_left(&mut self) {
|
||||||
|
if self.cursor_pos > 0 {
|
||||||
|
self.cursor_pos -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_right(&mut self) {
|
||||||
|
if self.cursor_pos < self.content.len() {
|
||||||
|
self.cursor_pos += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_char(&mut self, c: char) {
|
||||||
|
self.modified = true;
|
||||||
|
self.content.insert(self.cursor_pos, c);
|
||||||
|
self.cursor_pos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn backspace(&mut self) {
|
||||||
|
if self.cursor_pos > 0 {
|
||||||
|
self.modified = true;
|
||||||
|
self.content.remove(self.cursor_pos - 1);
|
||||||
|
self.cursor_pos -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_newline(&mut self) {
|
||||||
|
self.modified = true;
|
||||||
|
self.content.insert(self.cursor_pos, '\n');
|
||||||
|
self.cursor_pos += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
240
src/ui_tree/file_tree.rs
Normal file
240
src/ui_tree/file_tree.rs
Normal file
|
|
@ -0,0 +1,240 @@
|
||||||
|
use color_eyre::Result;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use crate::shared::state::AppState;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum TreeNode {
|
||||||
|
Bucket { name: String },
|
||||||
|
Folder { bucket: String, path: String, name: String },
|
||||||
|
File { bucket: String, path: String, name: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FileTree {
|
||||||
|
app_state: Arc<AppState>,
|
||||||
|
items: Vec<(String, TreeNode)>,
|
||||||
|
selected: usize,
|
||||||
|
current_bucket: Option<String>,
|
||||||
|
current_path: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileTree {
|
||||||
|
pub fn new(app_state: Arc<AppState>) -> Self {
|
||||||
|
Self {
|
||||||
|
app_state,
|
||||||
|
items: Vec::new(),
|
||||||
|
selected: 0,
|
||||||
|
current_bucket: None,
|
||||||
|
current_path: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_root(&mut self) -> Result<()> {
|
||||||
|
self.items.clear();
|
||||||
|
self.current_bucket = None;
|
||||||
|
self.current_path.clear();
|
||||||
|
if let Some(drive) = &self.app_state.drive {
|
||||||
|
let result = drive.list_buckets().send().await;
|
||||||
|
match result {
|
||||||
|
Ok(response) => {
|
||||||
|
let buckets = response.buckets();
|
||||||
|
for bucket in buckets {
|
||||||
|
if let Some(name) = bucket.name() {
|
||||||
|
let icon = if name.ends_with(".gbai") { "🤖" } else { "📦" };
|
||||||
|
let display = format!("{} {}", icon, name);
|
||||||
|
self.items.push((display, TreeNode::Bucket { name: name.to_string() }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.items.push((format!("✗ Error: {}", e), TreeNode::Bucket { name: String::new() }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.items.push(("✗ Drive not connected".to_string(), TreeNode::Bucket { name: String::new() }));
|
||||||
|
}
|
||||||
|
if self.items.is_empty() {
|
||||||
|
self.items.push(("(no buckets found)".to_string(), TreeNode::Bucket { name: String::new() }));
|
||||||
|
}
|
||||||
|
self.selected = 0;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn enter_bucket(&mut self, bucket: String) -> Result<()> {
|
||||||
|
self.current_bucket = Some(bucket.clone());
|
||||||
|
self.current_path.clear();
|
||||||
|
self.load_bucket_contents(&bucket, "").await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn enter_folder(&mut self, bucket: String, path: String) -> Result<()> {
|
||||||
|
self.current_bucket = Some(bucket.clone());
|
||||||
|
let parts: Vec<&str> = path.trim_matches('/').split('/').filter(|s| !s.is_empty()).collect();
|
||||||
|
self.current_path = parts.iter().map(|s| s.to_string()).collect();
|
||||||
|
self.load_bucket_contents(&bucket, &path).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn go_up(&mut self) -> bool {
|
||||||
|
if self.current_path.is_empty() {
|
||||||
|
if self.current_bucket.is_some() {
|
||||||
|
self.current_bucket = None;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.current_path.pop();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn refresh_current(&mut self) -> Result<()> {
|
||||||
|
if let Some(bucket) = &self.current_bucket.clone() {
|
||||||
|
let path = self.current_path.join("/");
|
||||||
|
self.load_bucket_contents(bucket, &path).await
|
||||||
|
} else {
|
||||||
|
self.load_root().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_bucket_contents(&mut self, bucket: &str, prefix: &str) -> Result<()> {
|
||||||
|
self.items.clear();
|
||||||
|
self.items.push(("⬆️ .. (go back)".to_string(), TreeNode::Folder {
|
||||||
|
bucket: bucket.to_string(),
|
||||||
|
path: "..".to_string(),
|
||||||
|
name: "..".to_string(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if let Some(drive) = &self.app_state.drive {
|
||||||
|
let normalized_prefix = if prefix.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else if prefix.ends_with('/') {
|
||||||
|
prefix.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}/", prefix)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut continuation_token = None;
|
||||||
|
let mut all_keys = Vec::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut request = drive.list_objects_v2().bucket(bucket);
|
||||||
|
if !normalized_prefix.is_empty() {
|
||||||
|
request = request.prefix(&normalized_prefix);
|
||||||
|
}
|
||||||
|
if let Some(token) = continuation_token {
|
||||||
|
request = request.continuation_token(token);
|
||||||
|
}
|
||||||
|
let result = request.send().await?;
|
||||||
|
|
||||||
|
for obj in result.contents() {
|
||||||
|
if let Some(key) = obj.key() {
|
||||||
|
all_keys.push(key.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result.is_truncated.unwrap_or(false) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continuation_token = result.next_continuation_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut folders = std::collections::HashSet::new();
|
||||||
|
let mut files = Vec::new();
|
||||||
|
|
||||||
|
for key in all_keys {
|
||||||
|
if key == normalized_prefix {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let relative = if !normalized_prefix.is_empty() && key.starts_with(&normalized_prefix) {
|
||||||
|
&key[normalized_prefix.len()..]
|
||||||
|
} else {
|
||||||
|
&key
|
||||||
|
};
|
||||||
|
if relative.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(slash_pos) = relative.find('/') {
|
||||||
|
let folder_name = &relative[..slash_pos];
|
||||||
|
if !folder_name.is_empty() {
|
||||||
|
folders.insert(folder_name.to_string());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
files.push((relative.to_string(), key.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut folder_vec: Vec<String> = folders.into_iter().collect();
|
||||||
|
folder_vec.sort();
|
||||||
|
|
||||||
|
for folder_name in folder_vec {
|
||||||
|
let full_path = if normalized_prefix.is_empty() {
|
||||||
|
folder_name.clone()
|
||||||
|
} else {
|
||||||
|
format!("{}{}", normalized_prefix, folder_name)
|
||||||
|
};
|
||||||
|
let display = format!("📁 {}/", folder_name);
|
||||||
|
self.items.push((display, TreeNode::Folder {
|
||||||
|
bucket: bucket.to_string(),
|
||||||
|
path: full_path,
|
||||||
|
name: folder_name,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
files.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||||
|
|
||||||
|
for (name, full_path) in files {
|
||||||
|
let icon = if name.ends_with(".bas") {
|
||||||
|
"⚙️"
|
||||||
|
} else if name.ends_with(".ast") {
|
||||||
|
"🔧"
|
||||||
|
} else if name.ends_with(".csv") {
|
||||||
|
"📊"
|
||||||
|
} else if name.ends_with(".gbkb") {
|
||||||
|
"📚"
|
||||||
|
} else if name.ends_with(".json") {
|
||||||
|
"🔖"
|
||||||
|
} else {
|
||||||
|
"📄"
|
||||||
|
};
|
||||||
|
let display = format!("{} {}", icon, name);
|
||||||
|
self.items.push((display, TreeNode::File {
|
||||||
|
bucket: bucket.to_string(),
|
||||||
|
path: full_path,
|
||||||
|
name,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.items.len() == 1 {
|
||||||
|
self.items.push(("(empty folder)".to_string(), TreeNode::Folder {
|
||||||
|
bucket: bucket.to_string(),
|
||||||
|
path: String::new(),
|
||||||
|
name: String::new(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.selected = 0;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_items(&self) -> &[(String, TreeNode)] {
|
||||||
|
&self.items
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected_index(&self) -> usize {
|
||||||
|
self.selected
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_selected_node(&self) -> Option<&TreeNode> {
|
||||||
|
self.items.get(self.selected).map(|(_, node)| node)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_up(&mut self) {
|
||||||
|
if self.selected > 0 {
|
||||||
|
self.selected -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_down(&mut self) {
|
||||||
|
if self.selected < self.items.len().saturating_sub(1) {
|
||||||
|
self.selected += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/ui_tree/log_panel.rs
Normal file
73
src/ui_tree/log_panel.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use log::{Log, Metadata, LevelFilter, Record, SetLoggerError};
|
||||||
|
use chrono::Local;
|
||||||
|
|
||||||
|
pub struct LogPanel {
|
||||||
|
logs: Vec<String>,
|
||||||
|
max_logs: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogPanel {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
logs: Vec::with_capacity(1000),
|
||||||
|
max_logs: 1000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_log(&mut self, entry: &str) {
|
||||||
|
if self.logs.len() >= self.max_logs {
|
||||||
|
self.logs.remove(0);
|
||||||
|
}
|
||||||
|
self.logs.push(entry.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&self) -> String {
|
||||||
|
let visible_logs = if self.logs.len() > 10 {
|
||||||
|
&self.logs[self.logs.len() - 10..]
|
||||||
|
} else {
|
||||||
|
&self.logs[..]
|
||||||
|
};
|
||||||
|
visible_logs.join("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UiLogger {
|
||||||
|
log_panel: Arc<Mutex<LogPanel>>,
|
||||||
|
filter: LevelFilter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Log for UiLogger {
|
||||||
|
fn enabled(&self, metadata: &Metadata) -> bool {
|
||||||
|
metadata.level() <= self.filter
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log(&self, record: &Record) {
|
||||||
|
if self.enabled(record.metadata()) {
|
||||||
|
let timestamp = Local::now().format("%H:%M:%S");
|
||||||
|
let level_icon = match record.level() {
|
||||||
|
log::Level::Error => "❌",
|
||||||
|
log::Level::Warn => "⚠️",
|
||||||
|
log::Level::Info => "ℹ️",
|
||||||
|
log::Level::Debug => "🔍",
|
||||||
|
log::Level::Trace => "📝",
|
||||||
|
};
|
||||||
|
let log_entry = format!("[{}] {} {}", timestamp, level_icon, record.args());
|
||||||
|
if let Ok(mut panel) = self.log_panel.lock() {
|
||||||
|
panel.add_log(&log_entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&self) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_logger(log_panel: Arc<Mutex<LogPanel>>) -> Result<(), SetLoggerError> {
|
||||||
|
let logger = Box::new(UiLogger {
|
||||||
|
log_panel,
|
||||||
|
filter: LevelFilter::Info,
|
||||||
|
});
|
||||||
|
log::set_boxed_logger(logger)?;
|
||||||
|
log::set_max_level(LevelFilter::Trace);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
462
src/ui_tree/mod.rs
Normal file
462
src/ui_tree/mod.rs
Normal file
|
|
@ -0,0 +1,462 @@
|
||||||
|
use crate::shared::state::AppState;
|
||||||
|
use color_eyre::Result;
|
||||||
|
use crossterm::{
|
||||||
|
event::{self, Event, KeyCode, KeyModifiers},
|
||||||
|
execute,
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
|
};
|
||||||
|
use log::LevelFilter;
|
||||||
|
use ratatui::{
|
||||||
|
backend::CrosstermBackend,
|
||||||
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
|
style::{Color, Modifier, Style},
|
||||||
|
text::{Line, Span},
|
||||||
|
widgets::{Block, Borders, List, ListItem, Paragraph, Wrap},
|
||||||
|
Frame, Terminal,
|
||||||
|
};
|
||||||
|
use std::io;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
mod editor;
|
||||||
|
mod file_tree;
|
||||||
|
mod log_panel;
|
||||||
|
mod status_panel;
|
||||||
|
use editor::Editor;
|
||||||
|
use file_tree::{FileTree, TreeNode};
|
||||||
|
use log_panel::{init_logger, LogPanel};
|
||||||
|
use status_panel::StatusPanel;
|
||||||
|
|
||||||
|
pub struct XtreeUI {
|
||||||
|
app_state: Option<Arc<AppState>>,
|
||||||
|
file_tree: Option<FileTree>,
|
||||||
|
status_panel: Option<StatusPanel>,
|
||||||
|
log_panel: Arc<Mutex<LogPanel>>,
|
||||||
|
editor: Option<Editor>,
|
||||||
|
active_panel: ActivePanel,
|
||||||
|
should_quit: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
enum ActivePanel {
|
||||||
|
FileTree,
|
||||||
|
Editor,
|
||||||
|
Status,
|
||||||
|
Logs,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl XtreeUI {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let log_panel = Arc::new(Mutex::new(LogPanel::new()));
|
||||||
|
Self {
|
||||||
|
app_state: None,
|
||||||
|
file_tree: None,
|
||||||
|
status_panel: None,
|
||||||
|
log_panel: log_panel.clone(),
|
||||||
|
editor: None,
|
||||||
|
active_panel: ActivePanel::Logs,
|
||||||
|
should_quit: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_app_state(&mut self, app_state: Arc<AppState>) {
|
||||||
|
self.file_tree = Some(FileTree::new(app_state.clone()));
|
||||||
|
self.status_panel = Some(StatusPanel::new(app_state.clone()));
|
||||||
|
self.app_state = Some(app_state);
|
||||||
|
self.active_panel = ActivePanel::FileTree;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_ui(&mut self) -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
if !std::io::IsTerminal::is_terminal(&std::io::stdout()) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
enable_raw_mode()?;
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
execute!(stdout, EnterAlternateScreen)?;
|
||||||
|
let backend = CrosstermBackend::new(stdout);
|
||||||
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
init_logger(self.log_panel.clone())?;
|
||||||
|
log::set_max_level(LevelFilter::Trace);
|
||||||
|
let result = self.run_event_loop(&mut terminal);
|
||||||
|
disable_raw_mode()?;
|
||||||
|
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||||
|
terminal.show_cursor()?;
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_event_loop(&mut self, terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()> {
|
||||||
|
let mut last_update = std::time::Instant::now();
|
||||||
|
let update_interval = std::time::Duration::from_millis(500);
|
||||||
|
let rt = tokio::runtime::Runtime::new()?;
|
||||||
|
loop {
|
||||||
|
terminal.draw(|f| self.render(f))?;
|
||||||
|
if self.app_state.is_some() && last_update.elapsed() >= update_interval {
|
||||||
|
if let Err(e) = rt.block_on(self.update_data()) {
|
||||||
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
|
log_panel.add_log(&format!("Update error: {}", e));
|
||||||
|
}
|
||||||
|
last_update = std::time::Instant::now();
|
||||||
|
}
|
||||||
|
if event::poll(std::time::Duration::from_millis(50))? {
|
||||||
|
if let Event::Key(key) = event::read()? {
|
||||||
|
if let Err(e) = rt.block_on(self.handle_input(key.code, key.modifiers)) {
|
||||||
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
|
log_panel.add_log(&format!("Input error: {}", e));
|
||||||
|
}
|
||||||
|
if self.should_quit {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self, f: &mut Frame) {
|
||||||
|
let bg = Color::Rgb(15, 15, 25);
|
||||||
|
let border_active = Color::Rgb(120, 220, 255);
|
||||||
|
let border_inactive = Color::Rgb(70, 70, 90);
|
||||||
|
let text = Color::Rgb(240, 240, 245);
|
||||||
|
let highlight = Color::Rgb(90, 180, 255);
|
||||||
|
let title = Color::Rgb(255, 230, 140);
|
||||||
|
if self.app_state.is_none() {
|
||||||
|
self.render_loading(f, bg, text, border_active, title);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let main_chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Min(0), Constraint::Length(12)])
|
||||||
|
.split(f.area());
|
||||||
|
if self.editor.is_some() {
|
||||||
|
let editor_chunks = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([Constraint::Percentage(20), Constraint::Percentage(80)])
|
||||||
|
.split(main_chunks[0]);
|
||||||
|
self.render_file_tree(f, editor_chunks[0], bg, text, border_active, border_inactive, highlight, title);
|
||||||
|
if let Some(editor) = &self.editor {
|
||||||
|
self.render_editor(f, editor_chunks[1], editor, bg, text, border_active, border_inactive, highlight, title);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let top_chunks = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
|
||||||
|
.split(main_chunks[0]);
|
||||||
|
self.render_file_tree(f, top_chunks[0], bg, text, border_active, border_inactive, highlight, title);
|
||||||
|
self.render_status(f, top_chunks[1], bg, text, border_active, border_inactive, highlight, title);
|
||||||
|
}
|
||||||
|
self.render_logs(f, main_chunks[1], bg, text, border_active, border_inactive, highlight, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_loading(&self, f: &mut Frame, bg: Color, text: Color, border: Color, title: Color) {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Percentage(40), Constraint::Percentage(20), Constraint::Percentage(40)])
|
||||||
|
.split(f.area());
|
||||||
|
let center = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([Constraint::Percentage(30), Constraint::Percentage(40), Constraint::Percentage(30)])
|
||||||
|
.split(chunks[1])[1];
|
||||||
|
let block = Block::default()
|
||||||
|
.title(Span::styled(" 🚀 BOTSERVER ", Style::default().fg(title).add_modifier(Modifier::BOLD)))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(border))
|
||||||
|
.style(Style::default().bg(bg));
|
||||||
|
let loading_text = vec![
|
||||||
|
"",
|
||||||
|
" ╔════════════════════════════════╗",
|
||||||
|
" ║ ║",
|
||||||
|
" ║ ⚡ Initializing System... ║",
|
||||||
|
" ║ ║",
|
||||||
|
" ║ Loading components... ║",
|
||||||
|
" ║ Connecting to services... ║",
|
||||||
|
" ║ Preparing interface... ║",
|
||||||
|
" ║ ║",
|
||||||
|
" ╚════════════════════════════════╝",
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
let paragraph = Paragraph::new(loading_text)
|
||||||
|
.block(block)
|
||||||
|
.style(Style::default().fg(text))
|
||||||
|
.wrap(Wrap { trim: false });
|
||||||
|
f.render_widget(paragraph, center);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_file_tree(&self, f: &mut Frame, area: Rect, bg: Color, text: Color, border_active: Color, border_inactive: Color, highlight: Color, title: Color) {
|
||||||
|
if let Some(file_tree) = &self.file_tree {
|
||||||
|
let items = file_tree.render_items();
|
||||||
|
let selected = file_tree.selected_index();
|
||||||
|
let list_items: Vec<ListItem> = items.iter().enumerate().map(|(idx, (display, _))| {
|
||||||
|
let style = if idx == selected {
|
||||||
|
Style::default().bg(highlight).fg(Color::Black).add_modifier(Modifier::BOLD)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(text)
|
||||||
|
};
|
||||||
|
ListItem::new(Line::from(Span::styled(display.clone(), style)))
|
||||||
|
}).collect();
|
||||||
|
let is_active = self.active_panel == ActivePanel::FileTree;
|
||||||
|
let border_color = if is_active { border_active } else { border_inactive };
|
||||||
|
let title_style = if is_active {
|
||||||
|
Style::default().fg(title).add_modifier(Modifier::BOLD)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(text)
|
||||||
|
};
|
||||||
|
let block = Block::default()
|
||||||
|
.title(Span::styled(" 📁 FILE EXPLORER ", title_style))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(border_color))
|
||||||
|
.style(Style::default().bg(bg));
|
||||||
|
let list = List::new(list_items).block(block);
|
||||||
|
f.render_widget(list, area);
|
||||||
|
} else {
|
||||||
|
let block = Block::default()
|
||||||
|
.title(Span::styled(" 📁 FILE EXPLORER ", Style::default().fg(text)))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(border_inactive))
|
||||||
|
.style(Style::default().bg(bg));
|
||||||
|
f.render_widget(block, area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_status(&self, f: &mut Frame, area: Rect, bg: Color, text: Color, border_active: Color, border_inactive: Color, _highlight: Color, title: Color) {
|
||||||
|
let status_text = if let Some(status_panel) = &self.status_panel {
|
||||||
|
status_panel.render()
|
||||||
|
} else {
|
||||||
|
"Waiting for initialization...".to_string()
|
||||||
|
};
|
||||||
|
let is_active = self.active_panel == ActivePanel::Status;
|
||||||
|
let border_color = if is_active { border_active } else { border_inactive };
|
||||||
|
let title_style = if is_active {
|
||||||
|
Style::default().fg(title).add_modifier(Modifier::BOLD)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(text)
|
||||||
|
};
|
||||||
|
let block = Block::default()
|
||||||
|
.title(Span::styled(" 📊 SYSTEM STATUS ", title_style))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(border_color))
|
||||||
|
.style(Style::default().bg(bg));
|
||||||
|
let paragraph = Paragraph::new(status_text)
|
||||||
|
.block(block)
|
||||||
|
.style(Style::default().fg(text))
|
||||||
|
.wrap(Wrap { trim: false });
|
||||||
|
f.render_widget(paragraph, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_editor(&self, f: &mut Frame, area: Rect, editor: &Editor, bg: Color, text: Color, border_active: Color, border_inactive: Color, _highlight: Color, title: Color) {
|
||||||
|
let is_active = self.active_panel == ActivePanel::Editor;
|
||||||
|
let border_color = if is_active { border_active } else { border_inactive };
|
||||||
|
let title_style = if is_active {
|
||||||
|
Style::default().fg(title).add_modifier(Modifier::BOLD)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(text)
|
||||||
|
};
|
||||||
|
let title_text = format!(" ✏️ EDITOR: {} ", editor.file_path());
|
||||||
|
let block = Block::default()
|
||||||
|
.title(Span::styled(title_text, title_style))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(border_color))
|
||||||
|
.style(Style::default().bg(bg));
|
||||||
|
let content = editor.render();
|
||||||
|
let paragraph = Paragraph::new(content)
|
||||||
|
.block(block)
|
||||||
|
.style(Style::default().fg(text))
|
||||||
|
.wrap(Wrap { trim: false });
|
||||||
|
f.render_widget(paragraph, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_logs(&self, f: &mut Frame, area: Rect, bg: Color, text: Color, border_active: Color, border_inactive: Color, _highlight: Color, title: Color) {
|
||||||
|
let log_panel = self.log_panel.try_lock();
|
||||||
|
let log_lines = if let Ok(panel) = log_panel {
|
||||||
|
panel.render()
|
||||||
|
} else {
|
||||||
|
"Loading logs...".to_string()
|
||||||
|
};
|
||||||
|
let is_active = self.active_panel == ActivePanel::Logs;
|
||||||
|
let border_color = if is_active { border_active } else { border_inactive };
|
||||||
|
let title_style = if is_active {
|
||||||
|
Style::default().fg(title).add_modifier(Modifier::BOLD)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(text)
|
||||||
|
};
|
||||||
|
let block = Block::default()
|
||||||
|
.title(Span::styled(" 📜 SYSTEM LOGS ", title_style))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(border_color))
|
||||||
|
.style(Style::default().bg(bg));
|
||||||
|
let paragraph = Paragraph::new(log_lines)
|
||||||
|
.block(block)
|
||||||
|
.style(Style::default().fg(text))
|
||||||
|
.wrap(Wrap { trim: false });
|
||||||
|
f.render_widget(paragraph, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_input(&mut self, key: KeyCode, modifiers: KeyModifiers) -> Result<()> {
|
||||||
|
if modifiers.contains(KeyModifiers::CONTROL) {
|
||||||
|
match key {
|
||||||
|
KeyCode::Char('c') | KeyCode::Char('q') => {
|
||||||
|
self.should_quit = true;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
KeyCode::Char('s') => {
|
||||||
|
if let Some(editor) = &mut self.editor {
|
||||||
|
if let Some(app_state) = &self.app_state {
|
||||||
|
if let Err(e) = editor.save(app_state).await {
|
||||||
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
|
log_panel.add_log(&format!("Save failed: {}", e));
|
||||||
|
} else {
|
||||||
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
|
log_panel.add_log(&format!("✓ Saved: {}", editor.file_path()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
KeyCode::Char('w') => {
|
||||||
|
if self.editor.is_some() {
|
||||||
|
self.editor = None;
|
||||||
|
self.active_panel = ActivePanel::FileTree;
|
||||||
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
|
log_panel.add_log("✓ Closed editor");
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.app_state.is_none() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
match self.active_panel {
|
||||||
|
ActivePanel::FileTree => match key {
|
||||||
|
KeyCode::Up => {
|
||||||
|
if let Some(file_tree) = &mut self.file_tree {
|
||||||
|
file_tree.move_up();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
if let Some(file_tree) = &mut self.file_tree {
|
||||||
|
file_tree.move_down();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
if let Err(e) = self.handle_tree_enter().await {
|
||||||
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
|
log_panel.add_log(&format!("✗ Enter error: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Backspace => {
|
||||||
|
if let Some(file_tree) = &mut self.file_tree {
|
||||||
|
if file_tree.go_up() {
|
||||||
|
if let Err(e) = file_tree.refresh_current().await {
|
||||||
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
|
log_panel.add_log(&format!("✗ Navigation error: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Tab => {
|
||||||
|
self.active_panel = ActivePanel::Status;
|
||||||
|
}
|
||||||
|
KeyCode::Char('q') => {
|
||||||
|
self.should_quit = true;
|
||||||
|
}
|
||||||
|
KeyCode::F(5) => {
|
||||||
|
if let Some(file_tree) = &mut self.file_tree {
|
||||||
|
if let Err(e) = file_tree.refresh_current().await {
|
||||||
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
|
log_panel.add_log(&format!("✗ Refresh failed: {}", e));
|
||||||
|
} else {
|
||||||
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
|
log_panel.add_log("✓ Refreshed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
ActivePanel::Editor => {
|
||||||
|
if let Some(editor) = &mut self.editor {
|
||||||
|
match key {
|
||||||
|
KeyCode::Up => editor.move_up(),
|
||||||
|
KeyCode::Down => editor.move_down(),
|
||||||
|
KeyCode::Left => editor.move_left(),
|
||||||
|
KeyCode::Right => editor.move_right(),
|
||||||
|
KeyCode::Char(c) => editor.insert_char(c),
|
||||||
|
KeyCode::Backspace => editor.backspace(),
|
||||||
|
KeyCode::Enter => editor.insert_newline(),
|
||||||
|
KeyCode::Tab => {
|
||||||
|
self.active_panel = ActivePanel::FileTree;
|
||||||
|
}
|
||||||
|
KeyCode::Esc => {
|
||||||
|
self.editor = None;
|
||||||
|
self.active_panel = ActivePanel::FileTree;
|
||||||
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
|
log_panel.add_log("✓ Closed editor");
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ActivePanel::Status => match key {
|
||||||
|
KeyCode::Tab => {
|
||||||
|
self.active_panel = ActivePanel::Logs;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
ActivePanel::Logs => match key {
|
||||||
|
KeyCode::Tab => {
|
||||||
|
self.active_panel = ActivePanel::FileTree;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_tree_enter(&mut self) -> Result<()> {
|
||||||
|
if let (Some(file_tree), Some(app_state)) = (&mut self.file_tree, &self.app_state) {
|
||||||
|
if let Some(node) = file_tree.get_selected_node().cloned() {
|
||||||
|
match node {
|
||||||
|
TreeNode::Bucket { name, .. } => {
|
||||||
|
file_tree.enter_bucket(name.clone()).await?;
|
||||||
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
|
log_panel.add_log(&format!("📂 Opened bucket: {}", name));
|
||||||
|
}
|
||||||
|
TreeNode::Folder { bucket, path, .. } => {
|
||||||
|
file_tree.enter_folder(bucket.clone(), path.clone()).await?;
|
||||||
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
|
log_panel.add_log(&format!("📂 Opened folder: {}", path));
|
||||||
|
}
|
||||||
|
TreeNode::File { bucket, path, .. } => {
|
||||||
|
match Editor::load(app_state, &bucket, &path).await {
|
||||||
|
Ok(editor) => {
|
||||||
|
self.editor = Some(editor);
|
||||||
|
self.active_panel = ActivePanel::Editor;
|
||||||
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
|
log_panel.add_log(&format!("✏️ Editing: {}", path));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
|
log_panel.add_log(&format!("✗ Failed to load file: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_data(&mut self) -> Result<()> {
|
||||||
|
if let Some(status_panel) = &mut self.status_panel {
|
||||||
|
status_panel.update().await?;
|
||||||
|
}
|
||||||
|
if let Some(file_tree) = &self.file_tree {
|
||||||
|
if file_tree.render_items().is_empty() {
|
||||||
|
if let Some(file_tree) = &mut self.file_tree {
|
||||||
|
file_tree.load_root().await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
105
src/ui_tree/status_panel.rs
Normal file
105
src/ui_tree/status_panel.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
use crate::shared::state::AppState;
|
||||||
|
use crate::shared::models::schema::bots::dsl::*;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
|
||||||
|
pub struct StatusPanel {
|
||||||
|
app_state: Arc<AppState>,
|
||||||
|
last_update: std::time::Instant,
|
||||||
|
cached_content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatusPanel {
|
||||||
|
pub fn new(app_state: Arc<AppState>) -> Self {
|
||||||
|
Self {
|
||||||
|
app_state,
|
||||||
|
last_update: std::time::Instant::now(),
|
||||||
|
cached_content: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update(&mut self) -> Result<(), std::io::Error> {
|
||||||
|
if self.last_update.elapsed() < std::time::Duration::from_secs(2) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
lines.push("═══════════════════════════════════════".to_string());
|
||||||
|
lines.push(" COMPONENT STATUS".to_string());
|
||||||
|
lines.push("═══════════════════════════════════════".to_string());
|
||||||
|
lines.push("".to_string());
|
||||||
|
|
||||||
|
let db_status = if self.app_state.conn.try_lock().is_ok() {
|
||||||
|
"🟢 ONLINE"
|
||||||
|
} else {
|
||||||
|
"🔴 OFFLINE"
|
||||||
|
};
|
||||||
|
lines.push(format!(" Database: {}", db_status));
|
||||||
|
|
||||||
|
let cache_status = if self.app_state.cache.is_some() {
|
||||||
|
"🟢 ONLINE"
|
||||||
|
} else {
|
||||||
|
"🟡 DISABLED"
|
||||||
|
};
|
||||||
|
lines.push(format!(" Cache: {}", cache_status));
|
||||||
|
|
||||||
|
let drive_status = if self.app_state.drive.is_some() {
|
||||||
|
"🟢 ONLINE"
|
||||||
|
} else {
|
||||||
|
"🔴 OFFLINE"
|
||||||
|
};
|
||||||
|
lines.push(format!(" Drive: {}", drive_status));
|
||||||
|
|
||||||
|
let llm_status = "🟢 ONLINE";
|
||||||
|
lines.push(format!(" LLM: {}", llm_status));
|
||||||
|
|
||||||
|
lines.push("".to_string());
|
||||||
|
lines.push("───────────────────────────────────────".to_string());
|
||||||
|
lines.push(" ACTIVE BOTS".to_string());
|
||||||
|
lines.push("───────────────────────────────────────".to_string());
|
||||||
|
|
||||||
|
if let Ok(mut conn) = self.app_state.conn.try_lock() {
|
||||||
|
match bots
|
||||||
|
.filter(is_active.eq(true))
|
||||||
|
.select((name, id))
|
||||||
|
.load::<(String, uuid::Uuid)>(&mut *conn)
|
||||||
|
{
|
||||||
|
Ok(bot_list) => {
|
||||||
|
if bot_list.is_empty() {
|
||||||
|
lines.push(" No active bots".to_string());
|
||||||
|
} else {
|
||||||
|
for (bot_name, _bot_id) in bot_list {
|
||||||
|
lines.push(format!(" 🤖 {}", bot_name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
lines.push(" Error loading bots".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lines.push(" Database locked".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push("".to_string());
|
||||||
|
lines.push("───────────────────────────────────────".to_string());
|
||||||
|
lines.push(" SESSIONS".to_string());
|
||||||
|
lines.push("───────────────────────────────────────".to_string());
|
||||||
|
|
||||||
|
let session_count = self.app_state.response_channels.try_lock()
|
||||||
|
.map(|channels| channels.len())
|
||||||
|
.unwrap_or(0);
|
||||||
|
lines.push(format!(" Active: {}", session_count));
|
||||||
|
|
||||||
|
lines.push("".to_string());
|
||||||
|
lines.push("═══════════════════════════════════════".to_string());
|
||||||
|
|
||||||
|
self.cached_content = lines.join("\n");
|
||||||
|
self.last_update = std::time::Instant::now();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&self) -> String {
|
||||||
|
self.cached_content.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue