diff --git a/Cargo.lock b/Cargo.lock index f2bdbc2b..a3aa52d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 80754733..ae5cb838 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,46 +3,49 @@ name = "botserver" version = "6.0.7" edition = "2021" authors = [ - "Pragmatismo.com.br ", - "General Bots Community ", - "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 ", - "Sarah Lourenco", - "Thi Patriota", - "Webgus", - "Zuilho Se", + "Pragmatismo.com.br ", + "General Bots Community ", + "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 ", + "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 diff --git a/add-req.sh b/add-req.sh index 75c84b85..748dd67c 100755 --- a/add-req.sh +++ b/add-req.sh @@ -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() { diff --git a/src/llm_models/gpt_oss_20b.rs b/src/llm_models/gpt_oss_20b.rs index 70ddea50..7e447154 100644 --- a/src/llm_models/gpt_oss_20b.rs +++ b/src/llm_models/gpt_oss_20b.rs @@ -16,6 +16,6 @@ impl ModelHandler for GptOss20bHandler { } fn has_analysis_markers(&self, buffer: &str) -> bool { - buffer.contains("**") + buffer.contains("analysis<|message|>") } } diff --git a/src/main.rs b/src/main.rs index 628fbbf0..7dfc9418 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 = 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::>(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 diff --git a/src/ui_tree/editor.rs b/src/ui_tree/editor.rs new file mode 100644 index 00000000..07a32cc8 --- /dev/null +++ b/src/ui_tree/editor.rs @@ -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, bucket: &str, path: &str) -> Result { + 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) -> 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; + } +} diff --git a/src/ui_tree/file_tree.rs b/src/ui_tree/file_tree.rs new file mode 100644 index 00000000..25c94b88 --- /dev/null +++ b/src/ui_tree/file_tree.rs @@ -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, + items: Vec<(String, TreeNode)>, + selected: usize, + current_bucket: Option, + current_path: Vec, +} + +impl FileTree { + pub fn new(app_state: Arc) -> 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 = 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; + } + } +} diff --git a/src/ui_tree/log_panel.rs b/src/ui_tree/log_panel.rs new file mode 100644 index 00000000..755f8d59 --- /dev/null +++ b/src/ui_tree/log_panel.rs @@ -0,0 +1,73 @@ +use std::sync::{Arc, Mutex}; +use log::{Log, Metadata, LevelFilter, Record, SetLoggerError}; +use chrono::Local; + +pub struct LogPanel { + logs: Vec, + 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>, + 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>) -> Result<(), SetLoggerError> { + let logger = Box::new(UiLogger { + log_panel, + filter: LevelFilter::Info, + }); + log::set_boxed_logger(logger)?; + log::set_max_level(LevelFilter::Trace); + Ok(()) +} diff --git a/src/ui_tree/mod.rs b/src/ui_tree/mod.rs new file mode 100644 index 00000000..e8276df5 --- /dev/null +++ b/src/ui_tree/mod.rs @@ -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>, + file_tree: Option, + status_panel: Option, + log_panel: Arc>, + editor: Option, + 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) { + 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>) -> 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 = 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(()) + } +} diff --git a/src/ui_tree/status_panel.rs b/src/ui_tree/status_panel.rs new file mode 100644 index 00000000..9d28715e --- /dev/null +++ b/src/ui_tree/status_panel.rs @@ -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, + last_update: std::time::Instant, + cached_content: String, +} + +impl StatusPanel { + pub fn new(app_state: Arc) -> 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() + } +}