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:
Rodrigo Rodriguez (Pragmatismo) 2025-11-07 21:31:25 -03:00
parent 5379e21bfe
commit 7fa6ea9f6a
10 changed files with 1480 additions and 115 deletions

358
Cargo.lock generated
View file

@ -252,6 +252,15 @@ dependencies = [
"tokio",
]
[[package]]
name = "addr2line"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.1"
@ -423,7 +432,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a"
dependencies = [
"object",
"object 0.32.2",
]
[[package]]
@ -991,6 +1000,21 @@ dependencies = [
"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]]
name = "base16ct"
version = "0.1.1"
@ -1106,7 +1130,9 @@ dependencies = [
"base64 0.22.1",
"bytes",
"chrono",
"color-eyre",
"cron",
"crossterm 0.29.0",
"csv",
"diesel",
"dotenvy",
@ -1129,6 +1155,7 @@ dependencies = [
"pdf-extract",
"qdrant-client",
"rand 0.9.2",
"ratatui",
"redis",
"regex",
"reqwest",
@ -1250,6 +1277,21 @@ dependencies = [
"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]]
name = "cbc"
version = "0.1.2"
@ -1402,7 +1444,34 @@ checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
dependencies = [
"serde",
"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]]
@ -1434,6 +1503,20 @@ dependencies = [
"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]]
name = "console"
version = "0.16.1"
@ -1443,7 +1526,7 @@ dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width",
"unicode-width 0.2.0",
"windows-sys 0.61.2",
]
@ -1491,6 +1574,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "cookie"
version = "0.16.2"
@ -1591,6 +1683,49 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "crunchy"
version = "0.2.4"
@ -1871,7 +2006,7 @@ version = "0.99.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
dependencies = [
"convert_case",
"convert_case 0.4.0",
"proc-macro2",
"quote",
"rustc_version",
@ -1893,6 +2028,7 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
dependencies = [
"convert_case 0.7.1",
"proc-macro2",
"quote",
"syn",
@ -1960,6 +2096,15 @@ dependencies = [
"syn",
]
[[package]]
name = "document-features"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
dependencies = [
"litrs",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
@ -2131,6 +2276,16 @@ dependencies = [
"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]]
name = "fastrand"
version = "2.3.0"
@ -2363,6 +2518,12 @@ dependencies = [
"polyval",
]
[[package]]
name = "gimli"
version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
[[package]]
name = "glob"
version = "0.3.3"
@ -2881,6 +3042,12 @@ dependencies = [
"quote",
]
[[package]]
name = "indenter"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5"
[[package]]
name = "indexmap"
version = "1.9.3"
@ -2909,11 +3076,20 @@ checksum = "ade6dfcba0dfb62ad59e59e7241ec8912af34fd29e0e743e3db992bd278e8b65"
dependencies = [
"console",
"portable-atomic",
"unicode-width",
"unicode-width 0.2.0",
"unit-prefix",
"web-time",
]
[[package]]
name = "indoc"
version = "2.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
dependencies = [
"rustversion",
]
[[package]]
name = "inout"
version = "0.1.4"
@ -2924,6 +3100,19 @@ dependencies = [
"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]]
name = "ipnet"
version = "2.11.0"
@ -3156,6 +3345,12 @@ dependencies = [
"cc",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
@ -3168,6 +3363,12 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
[[package]]
name = "litrs"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
[[package]]
name = "livekit"
version = "0.7.24"
@ -3590,6 +3791,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "object"
version = "0.37.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.21.3"
@ -3685,6 +3895,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
[[package]]
name = "owo-colors"
version = "4.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
[[package]]
name = "p256"
version = "0.11.1"
@ -3747,6 +3963,12 @@ dependencies = [
"subtle",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pbjson"
version = "0.6.0"
@ -4250,6 +4472,27 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "redis"
version = "0.27.6"
@ -4419,6 +4662,12 @@ dependencies = [
"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]]
name = "rustc-hash"
version = "2.1.1"
@ -4434,6 +4683,19 @@ dependencies = [
"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]]
name = "rustix"
version = "1.1.2"
@ -4443,7 +4705,7 @@ dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"linux-raw-sys 0.11.0",
"windows-sys 0.61.2",
]
@ -4764,6 +5026,27 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "signal-hook-registry"
version = "1.4.6"
@ -4890,6 +5173,28 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "subtle"
version = "2.6.1"
@ -4971,7 +5276,7 @@ dependencies = [
"fastrand",
"getrandom 0.3.4",
"once_cell",
"rustix",
"rustix 1.1.2",
"windows-sys 0.61.2",
]
@ -5352,6 +5657,16 @@ dependencies = [
"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]]
name = "tracing-log"
version = "0.2.0"
@ -5455,10 +5770,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
[[package]]
name = "unicode-width"
version = "0.2.2"
name = "unicode-segmentation"
version = "1.12.0"
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]]
name = "unicode-xid"

View file

@ -3,46 +3,49 @@ name = "botserver"
version = "6.0.7"
edition = "2021"
authors = [
"Pragmatismo.com.br <contact@pragmatismo.com.br>",
"General Bots Community <https://github.com/GeneralBots>",
"Alan Perdomo",
"Ana Paula Gil",
"Arenas.io",
"Atylla L",
"Christopher de Castilho",
"Dario Junior",
"David Lerner",
"Experimentation Garage",
"Flavio Andrade",
"Heraldo Almeida",
"Joao Parana",
"Jonathas C",
"J Ramos",
"Lucas Picanco",
"Marcos Velasco",
"Matheus 39x",
"Oerlabs Henrique",
"Othon Lima",
"PH Nascimento",
"Phpussente",
"Robson Dantas",
"Rodrigo Rodriguez <me@rodrigorodriguez.com>",
"Sarah Lourenco",
"Thi Patriota",
"Webgus",
"Zuilho Se",
"Pragmatismo.com.br <contact@pragmatismo.com.br>",
"General Bots Community <https://github.com/GeneralBots>",
"Alan Perdomo",
"Ana Paula Gil",
"Arenas.io",
"Atylla L",
"Christopher de Castilho",
"Dario Junior",
"David Lerner",
"Experimentation Garage",
"Flavio Andrade",
"Heraldo Almeida",
"Joao Parana",
"Jonathas C",
"J Ramos",
"Lucas Picanco",
"Marcos Velasco",
"Matheus 39x",
"Oerlabs Henrique",
"Othon Lima",
"PH Nascimento",
"Phpussente",
"Robson Dantas",
"Rodrigo Rodriguez <me@rodrigorodriguez.com>",
"Sarah Lourenco",
"Thi Patriota",
"Webgus",
"Zuilho Se",
]
description = "General Bots Server - Open-source bot platform by Pragmatismo.com.br"
license = "AGPL-3.0"
repository = "https://github.com/GeneralBots/BotServer"
[features]
default = [ "vectordb"]
default = [ "vectordb", "desktop"]
vectordb = ["qdrant-client"]
email = ["imap"]
desktop = []
[dependencies]
color-eyre = "0.6.5"
crossterm = "0.29.0"
ratatui = "0.29.0"
scopeguard = "1.2.0"
once_cell = "1.18.0"
actix-cors = "0.7"
@ -101,10 +104,9 @@ urlencoding = "2.1"
uuid = { version = "1.11", features = ["serde", "v4"] }
zip = "2.2"
[profile.release]
lto = true # Enables Link-Time Optimization
opt-level = "z" # Optimizes for size instead of speed
strip = true # Strips debug symbols
panic = "abort" # Reduces size by removing panic unwinding
codegen-units = 1 # More aggressive optimization
lto = true
opt-level = "z"
strip = true
panic = "abort"
codegen-units = 1

View file

@ -21,31 +21,31 @@ for file in "${prompts[@]}"; do
done
dirs=(
"auth"
"automation"
"basic"
"bootstrap"
#"auth"
#"automation"
#"basic"
#"bootstrap"
"bot"
"channels"
"config"
"context"
#"channels"
#"config"
#"context"
"drive_monitor"
"email"
#"email"
"file"
# "kb"
"llm"
"llm_models"
"org"
"package"
"package_manager"
"riot_compiler"
"session"
#"llm_models"
#"org"
#"package_manager"
#"riot_compiler"
#"session"
"shared"
"tests"
"tools"
"ui"
"web_server"
"web_automation"
#"tests"
#"tools"
#"ui"
"ui_tree"
#"web_server"
#"web_automation"
)
filter_rust_file() {

View file

@ -16,6 +16,6 @@ impl ModelHandler for GptOss20bHandler {
}
fn has_analysis_markers(&self, buffer: &str) -> bool {
buffer.contains("**")
buffer.contains("analysis<|message|>")
}
}

View file

@ -27,10 +27,8 @@ mod package_manager;
mod session;
mod shared;
pub mod tests;
#[cfg(feature = "desktop")]
mod ui;
mod ui_tree;
mod web_server;
use crate::auth::auth_handler;
use crate::automation::AutomationService;
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::web_server::{bot_index, index, static_files};
#[cfg(not(feature = "desktop"))]
#[tokio::main]
async fn main() -> std::io::Result<()> {
use botserver::config::ConfigManager;
use crate::llm::local::ensure_llama_servers_running;
use botserver::config::ConfigManager;
let args: Vec<String> = std::env::args().collect();
let no_ui = args.contains(&"--noui".to_string());
if args.len() > 1 {
let command = &args[1];
match command.as_str() {
@ -71,6 +67,7 @@ async fn main() -> std::io::Result<()> {
));
}
},
"--noui" => {}
_ => {
eprintln!("Unknown command: {}", command);
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();
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
.write_style(env_logger::WriteStyle::Always)
.init();
let ui_handle = if !no_ui {
let (ui_tx, mut ui_rx) = tokio::sync::mpsc::channel::<Arc<AppState>>(1);
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()) {
InstallMode::Container
} else {
InstallMode::Local
};
let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") {
args.get(idx + 1).cloned()
} else {
None
};
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()?
.join("botserver-stack")
.join(".env");
let cfg = if env_path.exists() {
info!("Environment already initialized, skipping bootstrap");
match diesel::Connection::establish(&std::env::var("DATABASE_URL").unwrap()) {
Ok(mut conn) => {
AppConfig::from_database(&mut conn).expect("Failed to load config from DB")
@ -117,16 +129,11 @@ async fn main() -> std::io::Result<()> {
}
} else {
match bootstrap.bootstrap().await {
Ok(config) => {
info!("Bootstrap completed successfully");
config
}
Ok(config) => config,
Err(e) => {
log::error!("Bootstrap failed: {}", e);
match diesel::Connection::establish(
&std::env::var("DATABASE_URL").unwrap_or_else(|_| {
"postgres://gbuser:@localhost:5432/botserver".to_string()
}),
&std::env::var("DATABASE_URL").unwrap()
) {
Ok(mut conn) => {
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() {
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)) {
log::warn!("Failed to upload templates to MinIO: {}", e);
}
// Refresh configuration from environment to ensure latest DATABASE_URL and credentials
dotenv().ok();
let refreshed_cfg = AppConfig::from_env().expect("Failed to load config from env");
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")
.or_else(|_| std::env::var("REDIS_URL"))
.unwrap_or_else(|_| "redis://localhost:6379".to_string());
let redis_client = match redis::Client::open(cache_url.as_str()) {
Ok(client) => Some(Arc::new(client)),
Err(e) => {
log::warn!("Failed to connect to Redis: Redis URL did not parse- {}", e);
log::warn!("Failed to connect to Redis: {}", e);
None
}
};
let web_adapter = Arc::new(WebChannelAdapter::new());
let voice_adapter = Arc::new(VoiceAdapter::new());
let drive = init_drive(&config.drive)
.await
.expect("Failed to initialize Drive");
let session_manager = Arc::new(tokio::sync::Mutex::new(session::SessionManager::new(
diesel::Connection::establish(&cfg.database_url()).unwrap(),
redis_client.clone(),
)));
let auth_service = Arc::new(tokio::sync::Mutex::new(auth::AuthService::new()));
let conn = diesel::Connection::establish(&cfg.database_url()).unwrap();
let config_manager = ConfigManager::new(Arc::new(Mutex::new(conn)));
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
.get_config(&default_bot_id, "llm-url", Some("http://localhost:8081"))
.unwrap_or_else(|_| "http://localhost:8081".to_string());
let llm_provider = Arc::new(crate::llm::OpenAIClient::new(
"empty".to_string(),
Some(llm_url.clone()),
));
let app_state = Arc::new(AppState {
drive: Some(drive),
config: Some(cfg.clone()),
conn: db_pool.clone(),
bucket_name: "default.gbai".to_string(), // Default bucket name
bucket_name: "default.gbai".to_string(),
cache: redis_client.clone(),
session_manager: session_manager.clone(),
llm_provider: llm_provider.clone(),
@ -220,29 +214,25 @@ async fn main() -> std::io::Result<()> {
web_adapter: web_adapter.clone(),
voice_adapter: voice_adapter.clone(),
});
if let Some((_, ui_tx)) = &ui_handle {
ui_tx.send(app_state.clone()).await.ok();
}
info!(
"Starting HTTP server on {}:{}",
config.server.host, config.server.port
);
let worker_count = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4);
// Initialize bot orchestrator and mount all bots
let bot_orchestrator = BotOrchestrator::new(app_state.clone());
// Mount all active bots from database
if let Err(e) = bot_orchestrator.mount_all_bots().await {
log::error!("Failed to mount bots: {}", e);
// Use BotOrchestrator::send_warning to notify system admins
let msg = format!("Bot mount failure: {}", e);
let _ = bot_orchestrator
.send_warning("System", "AdminBot", msg.as_str())
.await;
} else {
let _sessions = get_sessions;
log::info!("Session handler registered successfully");
}
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 {
error!("Failed to stat LLM servers: {}", e);
error!("Failed to start LLM servers: {}", e);
}
HttpServer::new(move || {
@ -270,6 +260,7 @@ async fn main() -> std::io::Result<()> {
.max_age(3600);
let app_state_clone = app_state.clone();
let mut app = App::new()
.wrap(cors)
.wrap(Logger::default())
@ -301,6 +292,7 @@ async fn main() -> std::io::Result<()> {
.service(save_draft)
.service(save_click);
}
app = app.service(static_files);
app = app.service(bot_index);
app

153
src/ui_tree/editor.rs Normal file
View 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
View 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
View 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
View 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
View 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()
}
}