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

@ -37,12 +37,15 @@ 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();
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()
}
}