feat(bootstrap): enable bootstrap and package_manager modules
Uncommented bootstrap and package_manager directories in add-req.sh to include them in build process. Refactored bootstrap module for cleaner initialization and improved component handling logic.
This commit is contained in:
parent
332dbe7420
commit
25daaa8a9e
14 changed files with 1627 additions and 1353 deletions
|
|
@ -24,7 +24,7 @@ dirs=(
|
||||||
#"auth"
|
#"auth"
|
||||||
#"automation"
|
#"automation"
|
||||||
#"basic"
|
#"basic"
|
||||||
#"bootstrap"
|
"bootstrap"
|
||||||
"bot"
|
"bot"
|
||||||
#"channels"
|
#"channels"
|
||||||
#"config"
|
#"config"
|
||||||
|
|
@ -36,7 +36,7 @@ dirs=(
|
||||||
"llm"
|
"llm"
|
||||||
#"llm_models"
|
#"llm_models"
|
||||||
#"org"
|
#"org"
|
||||||
#"package_manager"
|
"package_manager"
|
||||||
#"riot_compiler"
|
#"riot_compiler"
|
||||||
#"session"
|
#"session"
|
||||||
"shared"
|
"shared"
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
|
||||||
pub struct ComponentInfo {
|
pub struct ComponentInfo {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
}
|
}
|
||||||
|
|
@ -30,18 +29,13 @@ impl BootstrapManager {
|
||||||
match Command::new("pg_isready").arg("-q").status() {
|
match Command::new("pg_isready").arg("-q").status() {
|
||||||
Ok(status) => status.success(),
|
Ok(status) => status.success(),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// fallback check using pgrep
|
|
||||||
Command::new("pgrep").arg("postgres").output().map(|o| !o.stdout.is_empty()).unwrap_or(false)
|
Command::new("pgrep").arg("postgres").output().map(|o| !o.stdout.is_empty()).unwrap_or(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn new(install_mode: InstallMode, tenant: Option<String>) -> Self {
|
pub async fn new(install_mode: InstallMode, tenant: Option<String>) -> Self {
|
||||||
info!(
|
trace!("Initializing BootstrapManager with mode {:?} and tenant {:?}", install_mode, tenant);
|
||||||
"Initializing BootstrapManager with mode {:?} and tenant {:?}",
|
|
||||||
install_mode, tenant
|
|
||||||
);
|
|
||||||
|
|
||||||
if !Self::is_postgres_running() {
|
if !Self::is_postgres_running() {
|
||||||
let pm = PackageManager::new(install_mode.clone(), tenant.clone())
|
let pm = PackageManager::new(install_mode.clone(), tenant.clone())
|
||||||
.expect("Failed to initialize PackageManager");
|
.expect("Failed to initialize PackageManager");
|
||||||
|
|
@ -49,12 +43,11 @@ impl BootstrapManager {
|
||||||
error!("Failed to start Tables server component automatically: {}", e);
|
error!("Failed to start Tables server component automatically: {}", e);
|
||||||
panic!("Database not available and auto-start failed.");
|
panic!("Database not available and auto-start failed.");
|
||||||
} else {
|
} else {
|
||||||
info!("Tables component started successfully.");
|
info!("Started Tables server component automatically");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = AppConfig::from_env().expect("Failed to load config from env");
|
let config = AppConfig::from_env().expect("Failed to load config from env");
|
||||||
let s3_client = futures::executor::block_on(Self::create_s3_operator(&config));
|
let s3_client = Self::create_s3_operator(&config).await;
|
||||||
Self {
|
Self {
|
||||||
install_mode,
|
install_mode,
|
||||||
tenant,
|
tenant,
|
||||||
|
|
@ -65,126 +58,50 @@ impl BootstrapManager {
|
||||||
pub fn start_all(&mut self) -> Result<()> {
|
pub fn start_all(&mut self) -> Result<()> {
|
||||||
let pm = PackageManager::new(self.install_mode.clone(), self.tenant.clone())?;
|
let pm = PackageManager::new(self.install_mode.clone(), self.tenant.clone())?;
|
||||||
let components = vec![
|
let components = vec![
|
||||||
ComponentInfo {
|
ComponentInfo { name: "tables" },
|
||||||
name: "tables",
|
ComponentInfo { name: "cache" },
|
||||||
|
ComponentInfo { name: "drive" },
|
||||||
},
|
ComponentInfo { name: "llm" },
|
||||||
ComponentInfo {
|
ComponentInfo { name: "email" },
|
||||||
name: "cache",
|
ComponentInfo { name: "proxy" },
|
||||||
|
ComponentInfo { name: "directory" },
|
||||||
},
|
ComponentInfo { name: "alm" },
|
||||||
ComponentInfo {
|
ComponentInfo { name: "alm_ci" },
|
||||||
name: "drive",
|
ComponentInfo { name: "dns" },
|
||||||
|
ComponentInfo { name: "webmail" },
|
||||||
},
|
ComponentInfo { name: "meeting" },
|
||||||
ComponentInfo {
|
ComponentInfo { name: "table_editor" },
|
||||||
name: "llm",
|
ComponentInfo { name: "doc_editor" },
|
||||||
|
ComponentInfo { name: "desktop" },
|
||||||
},
|
ComponentInfo { name: "devtools" },
|
||||||
ComponentInfo {
|
ComponentInfo { name: "bot" },
|
||||||
name: "email",
|
ComponentInfo { name: "system" },
|
||||||
|
ComponentInfo { name: "vector_db" },
|
||||||
},
|
ComponentInfo { name: "host" },
|
||||||
ComponentInfo {
|
|
||||||
name: "proxy",
|
|
||||||
|
|
||||||
},
|
|
||||||
ComponentInfo {
|
|
||||||
name: "directory",
|
|
||||||
|
|
||||||
},
|
|
||||||
ComponentInfo {
|
|
||||||
name: "alm",
|
|
||||||
|
|
||||||
},
|
|
||||||
ComponentInfo {
|
|
||||||
name: "alm_ci",
|
|
||||||
|
|
||||||
},
|
|
||||||
ComponentInfo {
|
|
||||||
name: "dns",
|
|
||||||
|
|
||||||
},
|
|
||||||
ComponentInfo {
|
|
||||||
name: "webmail",
|
|
||||||
|
|
||||||
},
|
|
||||||
ComponentInfo {
|
|
||||||
name: "meeting",
|
|
||||||
|
|
||||||
},
|
|
||||||
ComponentInfo {
|
|
||||||
name: "table_editor",
|
|
||||||
|
|
||||||
},
|
|
||||||
ComponentInfo {
|
|
||||||
name: "doc_editor",
|
|
||||||
|
|
||||||
},
|
|
||||||
ComponentInfo {
|
|
||||||
name: "desktop",
|
|
||||||
|
|
||||||
},
|
|
||||||
ComponentInfo {
|
|
||||||
name: "devtools",
|
|
||||||
|
|
||||||
},
|
|
||||||
ComponentInfo {
|
|
||||||
name: "bot",
|
|
||||||
|
|
||||||
},
|
|
||||||
ComponentInfo {
|
|
||||||
name: "system",
|
|
||||||
|
|
||||||
},
|
|
||||||
ComponentInfo {
|
|
||||||
name: "vector_db",
|
|
||||||
|
|
||||||
},
|
|
||||||
ComponentInfo {
|
|
||||||
name: "host",
|
|
||||||
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
info!("Starting all installed components...");
|
|
||||||
for component in components {
|
for component in components {
|
||||||
if pm.is_installed(component.name) {
|
if pm.is_installed(component.name) {
|
||||||
debug!("Starting component: {}", component.name);
|
|
||||||
pm.start(component.name)?;
|
pm.start(component.name)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn bootstrap(&mut self) -> Result<AppConfig> {
|
pub async fn bootstrap(&mut self) -> Result<AppConfig> {
|
||||||
// First check for legacy mode
|
|
||||||
if let Ok(tables_server) = std::env::var("TABLES_SERVER") {
|
if let Ok(tables_server) = std::env::var("TABLES_SERVER") {
|
||||||
if !tables_server.is_empty() {
|
if !tables_server.is_empty() {
|
||||||
info!(
|
info!("Legacy mode detected (TABLES_SERVER present), skipping bootstrap installation");
|
||||||
"Legacy mode detected (TABLES_SERVER present), skipping bootstrap installation"
|
|
||||||
);
|
|
||||||
let _database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| {
|
let _database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| {
|
||||||
let username =
|
let username = std::env::var("TABLES_USERNAME").unwrap_or_else(|_| "gbuser".to_string());
|
||||||
std::env::var("TABLES_USERNAME").unwrap_or_else(|_| "gbuser".to_string());
|
let password = std::env::var("TABLES_PASSWORD").unwrap_or_else(|_| "postgres".to_string());
|
||||||
let password =
|
let server = std::env::var("TABLES_SERVER").unwrap_or_else(|_| "localhost".to_string());
|
||||||
std::env::var("TABLES_PASSWORD").unwrap_or_else(|_| "postgres".to_string());
|
|
||||||
let server =
|
|
||||||
std::env::var("TABLES_SERVER").unwrap_or_else(|_| "localhost".to_string());
|
|
||||||
let port = std::env::var("TABLES_PORT").unwrap_or_else(|_| "5432".to_string());
|
let port = std::env::var("TABLES_PORT").unwrap_or_else(|_| "5432".to_string());
|
||||||
let database =
|
let database = std::env::var("TABLES_DATABASE").unwrap_or_else(|_| "gbserver".to_string());
|
||||||
std::env::var("TABLES_DATABASE").unwrap_or_else(|_| "gbserver".to_string());
|
format!("postgres://{}:{}@{}:{}/{}", username, password, server, port, database)
|
||||||
format!(
|
|
||||||
"postgres://{}:{}@{}:{}/{}",
|
|
||||||
username, password, server, port, database
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// In legacy mode, still try to load config.csv if available
|
|
||||||
if let Ok(config) = self.load_config_from_csv().await {
|
if let Ok(config) = self.load_config_from_csv().await {
|
||||||
return Ok(config);
|
return Ok(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
match establish_pg_connection() {
|
match establish_pg_connection() {
|
||||||
Ok(mut conn) => {
|
Ok(mut conn) => {
|
||||||
if let Err(e) = self.apply_migrations(&mut conn) {
|
if let Err(e) = self.apply_migrations(&mut conn) {
|
||||||
|
|
@ -199,11 +116,9 @@ impl BootstrapManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let pm = PackageManager::new(self.install_mode.clone(), self.tenant.clone())?;
|
let pm = PackageManager::new(self.install_mode.clone(), self.tenant.clone())?;
|
||||||
let required_components = vec!["tables", "drive", "cache", "llm"];
|
let required_components = vec!["tables", "drive", "cache", "llm"];
|
||||||
let mut config = AppConfig::from_env().expect("Failed to load config from env");
|
let mut config = AppConfig::from_env().expect("Failed to load config from env");
|
||||||
|
|
||||||
for component in required_components {
|
for component in required_components {
|
||||||
if !pm.is_installed(component) {
|
if !pm.is_installed(component) {
|
||||||
let termination_cmd = pm
|
let termination_cmd = pm
|
||||||
|
|
@ -211,7 +126,6 @@ impl BootstrapManager {
|
||||||
.get(component)
|
.get(component)
|
||||||
.and_then(|cfg| cfg.binary_name.clone())
|
.and_then(|cfg| cfg.binary_name.clone())
|
||||||
.unwrap_or_else(|| component.to_string());
|
.unwrap_or_else(|| component.to_string());
|
||||||
|
|
||||||
if !termination_cmd.is_empty() {
|
if !termination_cmd.is_empty() {
|
||||||
let check = Command::new("pgrep")
|
let check = Command::new("pgrep")
|
||||||
.arg("-f")
|
.arg("-f")
|
||||||
|
|
@ -231,16 +145,12 @@ impl BootstrapManager {
|
||||||
.status();
|
.status();
|
||||||
println!("Terminated existing '{}' process.", component);
|
println!("Terminated existing '{}' process.", component);
|
||||||
} else {
|
} else {
|
||||||
println!(
|
println!("Skipping start of '{}' as it is already running.", component);
|
||||||
"Skipping start of '{}' as it is already running.",
|
|
||||||
component
|
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if component == "tables" {
|
if component == "tables" {
|
||||||
let db_password = self.generate_secure_password(16);
|
let db_password = self.generate_secure_password(16);
|
||||||
let farm_password = self.generate_secure_password(32);
|
let farm_password = self.generate_secure_password(32);
|
||||||
|
|
@ -252,13 +162,10 @@ impl BootstrapManager {
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to write .env file: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Failed to write .env file: {}", e))?;
|
||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
}
|
}
|
||||||
|
pm.install(component).await?;
|
||||||
futures::executor::block_on(pm.install(component))?;
|
|
||||||
|
|
||||||
if component == "tables" {
|
if component == "tables" {
|
||||||
let mut conn = establish_pg_connection()
|
let mut conn = establish_pg_connection()
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to connect to database: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Failed to connect to database: {}", e))?;
|
||||||
|
|
||||||
let migration_dir = include_dir::include_dir!("./migrations");
|
let migration_dir = include_dir::include_dir!("./migrations");
|
||||||
let mut migration_files: Vec<_> = migration_dir
|
let mut migration_files: Vec<_> = migration_dir
|
||||||
.files()
|
.files()
|
||||||
|
|
@ -271,60 +178,40 @@ impl BootstrapManager {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
migration_files.sort_by_key(|f| f.path());
|
migration_files.sort_by_key(|f| f.path());
|
||||||
|
|
||||||
for migration_file in migration_files {
|
for migration_file in migration_files {
|
||||||
let migration = migration_file
|
let migration = migration_file
|
||||||
.contents_utf8()
|
.contents_utf8()
|
||||||
.ok_or_else(|| anyhow::anyhow!("Migration file is not valid UTF-8"))?;
|
.ok_or_else(|| anyhow::anyhow!("Migration file is not valid UTF-8"))?;
|
||||||
|
|
||||||
if let Err(e) = conn.batch_execute(migration) {
|
if let Err(e) = conn.batch_execute(migration) {
|
||||||
log::error!(
|
log::error!("Failed to execute migration {}: {}", migration_file.path().display(), e);
|
||||||
"Failed to execute migration {}: {}",
|
|
||||||
migration_file.path().display(),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
info!(
|
trace!("Successfully executed migration: {}", migration_file.path().display());
|
||||||
"Successfully executed migration: {}",
|
|
||||||
migration_file.path().display()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
config = AppConfig::from_database(&mut conn).expect("Failed to load config from DB");
|
config = AppConfig::from_database(&mut conn).expect("Failed to load config from DB");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.s3_client = Self::create_s3_operator(&config).await;
|
||||||
self.s3_client = futures::executor::block_on(Self::create_s3_operator(&config));
|
|
||||||
|
|
||||||
// Load config from CSV if available
|
|
||||||
let final_config = if let Ok(csv_config) = self.load_config_from_csv().await {
|
let final_config = if let Ok(csv_config) = self.load_config_from_csv().await {
|
||||||
csv_config
|
csv_config
|
||||||
} else {
|
} else {
|
||||||
config
|
config
|
||||||
};
|
};
|
||||||
|
|
||||||
// Write drive config to .env file if not already present (first bootstrap)
|
|
||||||
if std::env::var("DRIVE_SERVER").is_err() {
|
if std::env::var("DRIVE_SERVER").is_err() {
|
||||||
write_drive_config_to_env(&final_config.drive)
|
write_drive_config_to_env(&final_config.drive)
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to write drive config to .env: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Failed to write drive config to .env: {}", e))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(final_config)
|
Ok(final_config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async fn create_s3_operator(config: &AppConfig) -> Client {
|
async fn create_s3_operator(config: &AppConfig) -> Client {
|
||||||
let endpoint = if !config.drive.server.ends_with('/') {
|
let endpoint = if !config.drive.server.ends_with('/') {
|
||||||
format!("{}/", config.drive.server)
|
format!("{}/", config.drive.server)
|
||||||
} else {
|
} else {
|
||||||
config.drive.server.clone()
|
config.drive.server.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let base_config = aws_config::defaults(BehaviorVersion::latest())
|
let base_config = aws_config::defaults(BehaviorVersion::latest())
|
||||||
.endpoint_url(endpoint)
|
.endpoint_url(endpoint)
|
||||||
.region("auto")
|
.region("auto")
|
||||||
|
|
@ -339,17 +226,12 @@ impl BootstrapManager {
|
||||||
)
|
)
|
||||||
.load()
|
.load()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let s3_config = aws_sdk_s3::config::Builder::from(&base_config)
|
let s3_config = aws_sdk_s3::config::Builder::from(&base_config)
|
||||||
.force_path_style(true)
|
.force_path_style(true)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
aws_sdk_s3::Client::from_conf(s3_config)
|
aws_sdk_s3::Client::from_conf(s3_config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fn generate_secure_password(&self, length: usize) -> String {
|
fn generate_secure_password(&self, length: usize) -> String {
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand::rng();
|
||||||
std::iter::repeat_with(|| rng.sample(Alphanumeric) as char)
|
std::iter::repeat_with(|| rng.sample(Alphanumeric) as char)
|
||||||
|
|
@ -357,7 +239,6 @@ impl BootstrapManager {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub async fn upload_templates_to_drive(&self, _config: &AppConfig) -> Result<()> {
|
pub async fn upload_templates_to_drive(&self, _config: &AppConfig) -> Result<()> {
|
||||||
let mut conn = establish_pg_connection()?;
|
let mut conn = establish_pg_connection()?;
|
||||||
self.create_bots_from_templates(&mut conn)?;
|
self.create_bots_from_templates(&mut conn)?;
|
||||||
|
|
@ -366,8 +247,8 @@ impl BootstrapManager {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let client = &self.s3_client;
|
let client = &self.s3_client;
|
||||||
for entry in std::fs::read_dir(templates_dir)? {
|
let mut read_dir = tokio::fs::read_dir(templates_dir).await?;
|
||||||
let entry = entry?;
|
while let Some(entry) = read_dir.next_entry().await? {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.is_dir()
|
if path.is_dir()
|
||||||
&& path
|
&& path
|
||||||
|
|
@ -378,21 +259,15 @@ impl BootstrapManager {
|
||||||
{
|
{
|
||||||
let bot_name = path.file_name().unwrap().to_string_lossy().to_string();
|
let bot_name = path.file_name().unwrap().to_string_lossy().to_string();
|
||||||
let bucket = bot_name.trim_start_matches('/').to_string();
|
let bucket = bot_name.trim_start_matches('/').to_string();
|
||||||
info!("Checking template {} for Drive bucket {}", bot_name, bucket);
|
|
||||||
|
|
||||||
// Check if bucket exists
|
|
||||||
if client.head_bucket().bucket(&bucket).send().await.is_err() {
|
if client.head_bucket().bucket(&bucket).send().await.is_err() {
|
||||||
info!("Bucket {} not found, creating it and uploading template", bucket);
|
|
||||||
match client.create_bucket()
|
match client.create_bucket()
|
||||||
.bucket(&bucket)
|
.bucket(&bucket)
|
||||||
.send()
|
.send()
|
||||||
.await {
|
.await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
debug!("Bucket {} created successfully", bucket);
|
trace!("Created bucket: {}", bucket);
|
||||||
// Only upload template if bucket was just created
|
|
||||||
self.upload_directory_recursive(client, &path, &bucket, "/")
|
self.upload_directory_recursive(client, &path, &bucket, "/")
|
||||||
.await?;
|
.await?;
|
||||||
info!("Uploaded template {} to Drive bucket {}", bot_name, bucket);
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to create bucket {}: {:?}", bucket, e);
|
error!("Failed to create bucket {}: {:?}", bucket, e);
|
||||||
|
|
@ -403,7 +278,7 @@ impl BootstrapManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
info!("Bucket {} already exists, skipping template upload", bucket);
|
debug!("Bucket {} already exists", bucket);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -413,25 +288,21 @@ impl BootstrapManager {
|
||||||
fn create_bots_from_templates(&self, conn: &mut diesel::PgConnection) -> Result<()> {
|
fn create_bots_from_templates(&self, conn: &mut diesel::PgConnection) -> Result<()> {
|
||||||
use crate::shared::models::schema::bots;
|
use crate::shared::models::schema::bots;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
let templates_dir = Path::new("templates");
|
let templates_dir = Path::new("templates");
|
||||||
if !templates_dir.exists() {
|
if !templates_dir.exists() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
for entry in std::fs::read_dir(templates_dir)? {
|
for entry in std::fs::read_dir(templates_dir)? {
|
||||||
let entry = entry?;
|
let entry = entry?;
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.is_dir() && path.extension().map(|e| e == "gbai").unwrap_or(false) {
|
if path.is_dir() && path.extension().map(|e| e == "gbai").unwrap_or(false) {
|
||||||
let bot_folder = path.file_name().unwrap().to_string_lossy().to_string();
|
let bot_folder = path.file_name().unwrap().to_string_lossy().to_string();
|
||||||
let bot_name = bot_folder.trim_end_matches(".gbai");
|
let bot_name = bot_folder.trim_end_matches(".gbai");
|
||||||
|
|
||||||
let existing: Option<String> = bots::table
|
let existing: Option<String> = bots::table
|
||||||
.filter(bots::name.eq(&bot_name))
|
.filter(bots::name.eq(&bot_name))
|
||||||
.select(bots::name)
|
.select(bots::name)
|
||||||
.first(conn)
|
.first(conn)
|
||||||
.optional()?;
|
.optional()?;
|
||||||
|
|
||||||
if existing.is_none() {
|
if existing.is_none() {
|
||||||
diesel::sql_query(
|
diesel::sql_query(
|
||||||
"INSERT INTO bots (id, name, description, llm_provider, llm_config, context_provider, context_config, is_active) \
|
"INSERT INTO bots (id, name, description, llm_provider, llm_config, context_provider, context_config, is_active) \
|
||||||
|
|
@ -440,12 +311,12 @@ impl BootstrapManager {
|
||||||
.bind::<diesel::sql_types::Text, _>(&bot_name)
|
.bind::<diesel::sql_types::Text, _>(&bot_name)
|
||||||
.bind::<diesel::sql_types::Text, _>(format!("Bot for {} template", bot_name))
|
.bind::<diesel::sql_types::Text, _>(format!("Bot for {} template", bot_name))
|
||||||
.execute(conn)?;
|
.execute(conn)?;
|
||||||
|
info!("Created bot: {}", bot_name);
|
||||||
} else {
|
} else {
|
||||||
log::trace!("Bot {} already exists", bot_name);
|
debug!("Bot {} already exists", bot_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -457,28 +328,23 @@ impl BootstrapManager {
|
||||||
prefix: &'a str,
|
prefix: &'a str,
|
||||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + 'a>> {
|
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + 'a>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let normalized_path = if !local_path.to_string_lossy().ends_with('/') {
|
let _normalized_path = if !local_path.to_string_lossy().ends_with('/') {
|
||||||
format!("{}/", local_path.to_string_lossy())
|
format!("{}/", local_path.to_string_lossy())
|
||||||
} else {
|
} else {
|
||||||
local_path.to_string_lossy().to_string()
|
local_path.to_string_lossy().to_string()
|
||||||
};
|
};
|
||||||
trace!("Starting upload from local path: {}", normalized_path);
|
let mut read_dir = tokio::fs::read_dir(local_path).await?;
|
||||||
for entry in std::fs::read_dir(local_path)? {
|
while let Some(entry) = read_dir.next_entry().await? {
|
||||||
let entry = entry?;
|
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
let file_name = path.file_name().unwrap().to_string_lossy().to_string();
|
let file_name = path.file_name().unwrap().to_string_lossy().to_string();
|
||||||
|
|
||||||
// Construct key path, ensuring no duplicate slashes
|
|
||||||
let mut key = prefix.trim_matches('/').to_string();
|
let mut key = prefix.trim_matches('/').to_string();
|
||||||
if !key.is_empty() {
|
if !key.is_empty() {
|
||||||
key.push('/');
|
key.push('/');
|
||||||
}
|
}
|
||||||
key.push_str(&file_name);
|
key.push_str(&file_name);
|
||||||
|
|
||||||
if path.is_file() {
|
if path.is_file() {
|
||||||
info!("Uploading file: {} to bucket {} with key: {}",
|
trace!("Uploading file {} to bucket {} with key {}", path.display(), bucket, key);
|
||||||
path.display(), bucket, key);
|
let content = tokio::fs::read(&path).await?;
|
||||||
let content = std::fs::read(&path)?;
|
|
||||||
client.put_object()
|
client.put_object()
|
||||||
.bucket(bucket)
|
.bucket(bucket)
|
||||||
.key(&key)
|
.key(&key)
|
||||||
|
|
@ -496,11 +362,9 @@ impl BootstrapManager {
|
||||||
async fn load_config_from_csv(&self) -> Result<AppConfig> {
|
async fn load_config_from_csv(&self) -> Result<AppConfig> {
|
||||||
use crate::config::ConfigManager;
|
use crate::config::ConfigManager;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
let client = &self.s3_client;
|
let client = &self.s3_client;
|
||||||
let bucket = "default.gbai";
|
let bucket = "default.gbai";
|
||||||
let config_key = "default.gbot/config.csv";
|
let config_key = "default.gbot/config.csv";
|
||||||
|
|
||||||
match client.get_object()
|
match client.get_object()
|
||||||
.bucket(bucket)
|
.bucket(bucket)
|
||||||
.key(config_key)
|
.key(config_key)
|
||||||
|
|
@ -508,32 +372,22 @@ impl BootstrapManager {
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
|
trace!("Found config.csv in default.gbai");
|
||||||
let bytes = response.body.collect().await?.into_bytes();
|
let bytes = response.body.collect().await?.into_bytes();
|
||||||
let csv_content = String::from_utf8(bytes.to_vec())?;
|
let csv_content = String::from_utf8(bytes.to_vec())?;
|
||||||
|
|
||||||
// Create new connection for config loading
|
|
||||||
let config_conn = establish_pg_connection()?;
|
let config_conn = establish_pg_connection()?;
|
||||||
let config_manager = ConfigManager::new(Arc::new(Mutex::new(config_conn)));
|
let config_manager = ConfigManager::new(Arc::new(Mutex::new(config_conn)));
|
||||||
|
|
||||||
// Use default bot ID or create one if needed
|
|
||||||
let default_bot_id = Uuid::parse_str("00000000-0000-0000-0000-000000000000")?;
|
let default_bot_id = Uuid::parse_str("00000000-0000-0000-0000-000000000000")?;
|
||||||
|
|
||||||
// Write CSV to temp file for ConfigManager
|
|
||||||
let temp_path = std::env::temp_dir().join("config.csv");
|
let temp_path = std::env::temp_dir().join("config.csv");
|
||||||
std::fs::write(&temp_path, csv_content)?;
|
tokio::fs::write(&temp_path, csv_content).await?;
|
||||||
|
|
||||||
// First sync the CSV to database
|
|
||||||
config_manager.sync_gbot_config(&default_bot_id, temp_path.to_str().unwrap())
|
config_manager.sync_gbot_config(&default_bot_id, temp_path.to_str().unwrap())
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to sync gbot config: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Failed to sync gbot config: {}", e))?;
|
||||||
|
|
||||||
// Create fresh connection for final config load
|
|
||||||
let mut final_conn = establish_pg_connection()?;
|
let mut final_conn = establish_pg_connection()?;
|
||||||
let config = AppConfig::from_database(&mut final_conn)?;
|
let config = AppConfig::from_database(&mut final_conn)?;
|
||||||
info!("Successfully loaded config from CSV with LLM settings");
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
debug!("No config.csv found: {}", e);
|
debug!("No config.csv found in default.gbai: {:?}", e);
|
||||||
Err(e.into())
|
Err(e.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -544,7 +398,6 @@ Ok(config)
|
||||||
if !migrations_dir.exists() {
|
if !migrations_dir.exists() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut sql_files: Vec<_> = std::fs::read_dir(migrations_dir)?
|
let mut sql_files: Vec<_> = std::fs::read_dir(migrations_dir)?
|
||||||
.filter_map(|entry| entry.ok())
|
.filter_map(|entry| entry.ok())
|
||||||
.filter(|entry| {
|
.filter(|entry| {
|
||||||
|
|
@ -556,9 +409,7 @@ Ok(config)
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
sql_files.sort_by_key(|entry| entry.path());
|
sql_files.sort_by_key(|entry| entry.path());
|
||||||
|
|
||||||
for entry in sql_files {
|
for entry in sql_files {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
let filename = path.file_name().unwrap().to_string_lossy();
|
let filename = path.file_name().unwrap().to_string_lossy();
|
||||||
|
|
@ -574,7 +425,6 @@ Ok(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -235,22 +235,20 @@ impl BotOrchestrator {
|
||||||
channel
|
channel
|
||||||
);
|
);
|
||||||
|
|
||||||
let event_response = BotResponse {
|
let event_response = BotResponse::from_string_ids(
|
||||||
bot_id: bot_id.to_string(),
|
bot_id,
|
||||||
user_id: user_id.to_string(),
|
session_id,
|
||||||
session_id: session_id.to_string(),
|
user_id,
|
||||||
channel: channel.to_string(),
|
serde_json::to_string(&serde_json::json!({
|
||||||
content: serde_json::to_string(&serde_json::json!({
|
|
||||||
"event": event_type,
|
"event": event_type,
|
||||||
"data": data
|
"data": data
|
||||||
}))?,
|
}))?,
|
||||||
|
channel.to_string(),
|
||||||
|
)?;
|
||||||
|
let event_response = BotResponse {
|
||||||
message_type: 2,
|
message_type: 2,
|
||||||
stream_token: None,
|
|
||||||
is_complete: true,
|
is_complete: true,
|
||||||
suggestions: Vec::new(),
|
..event_response
|
||||||
context_name: None,
|
|
||||||
context_length: 0,
|
|
||||||
context_max_length: 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(adapter) = self.state.channels.lock().unwrap().get(channel) {
|
if let Some(adapter) = self.state.channels.lock().unwrap().get(channel) {
|
||||||
|
|
@ -510,7 +508,7 @@ impl BotOrchestrator {
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
// Show initial progress
|
// Show initial progress
|
||||||
if let Ok(metrics) = get_system_metrics(initial_tokens, max_context_size) {
|
if let Ok(_metrics) = get_system_metrics(initial_tokens, max_context_size) {
|
||||||
}
|
}
|
||||||
let model = config_manager
|
let model = config_manager
|
||||||
.get_config(
|
.get_config(
|
||||||
|
|
@ -563,11 +561,11 @@ impl BotOrchestrator {
|
||||||
let current_tokens =
|
let current_tokens =
|
||||||
initial_tokens + crate::shared::utils::estimate_token_count(&full_response);
|
initial_tokens + crate::shared::utils::estimate_token_count(&full_response);
|
||||||
if let Ok(metrics) = get_system_metrics(current_tokens, max_context_size) {
|
if let Ok(metrics) = get_system_metrics(current_tokens, max_context_size) {
|
||||||
let gpu_bar =
|
let _gpu_bar =
|
||||||
"█".repeat((metrics.gpu_usage.unwrap_or(0.0) / 5.0).round() as usize);
|
"█".repeat((metrics.gpu_usage.unwrap_or(0.0) / 5.0).round() as usize);
|
||||||
let cpu_bar = "█".repeat((metrics.cpu_usage / 5.0).round() as usize);
|
let _cpu_bar = "█".repeat((metrics.cpu_usage / 5.0).round() as usize);
|
||||||
let token_ratio = current_tokens as f64 / max_context_size.max(1) as f64;
|
let token_ratio = current_tokens as f64 / max_context_size.max(1) as f64;
|
||||||
let token_bar = "█".repeat((token_ratio * 20.0).round() as usize);
|
let _token_bar = "█".repeat((token_ratio * 20.0).round() as usize);
|
||||||
let mut ui = BotUI::new().unwrap();
|
let mut ui = BotUI::new().unwrap();
|
||||||
ui.render_progress(current_tokens, max_context_size).unwrap();
|
ui.render_progress(current_tokens, max_context_size).unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -297,19 +297,30 @@ impl DriveMonitor {
|
||||||
let bot_name = self.bucket_name.strip_suffix(".gbai").unwrap_or(&self.bucket_name);
|
let bot_name = self.bucket_name.strip_suffix(".gbai").unwrap_or(&self.bucket_name);
|
||||||
let work_dir = format!("./work/{}.gbai/{}.gbdialog", bot_name, bot_name);
|
let work_dir = format!("./work/{}.gbai/{}.gbdialog", bot_name, bot_name);
|
||||||
|
|
||||||
std::fs::create_dir_all(&work_dir)?;
|
// Offload the blocking compilation work to a blocking thread pool
|
||||||
|
let state_clone = Arc::clone(&self.state);
|
||||||
|
let work_dir_clone = work_dir.clone();
|
||||||
|
let tool_name_clone = tool_name.clone();
|
||||||
|
let source_content_clone = source_content.clone();
|
||||||
|
let bot_id = self.bot_id;
|
||||||
|
|
||||||
let local_source_path = format!("{}/{}.bas", work_dir, tool_name);
|
tokio::task::spawn_blocking(move || {
|
||||||
std::fs::write(&local_source_path, &source_content)?;
|
std::fs::create_dir_all(&work_dir_clone)?;
|
||||||
|
|
||||||
let mut compiler = BasicCompiler::new(Arc::clone(&self.state), self.bot_id);
|
let local_source_path = format!("{}/{}.bas", work_dir_clone, tool_name_clone);
|
||||||
let result = compiler.compile_file(&local_source_path, &work_dir)?;
|
std::fs::write(&local_source_path, &source_content_clone)?;
|
||||||
|
|
||||||
|
let mut compiler = BasicCompiler::new(state_clone, bot_id);
|
||||||
|
let result = compiler.compile_file(&local_source_path, &work_dir_clone)?;
|
||||||
|
|
||||||
if let Some(mcp_tool) = result.mcp_tool {
|
if let Some(mcp_tool) = result.mcp_tool {
|
||||||
info!("MCP tool definition generated with {} parameters",
|
info!("MCP tool definition generated with {} parameters",
|
||||||
mcp_tool.input_schema.properties.len());
|
mcp_tool.input_schema.properties.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok::<(), Box<dyn Error + Send + Sync>>(())
|
||||||
|
}).await??;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,22 +30,29 @@ pub async fn embeddings_local(
|
||||||
pub async fn ensure_llama_servers_running(
|
pub async fn ensure_llama_servers_running(
|
||||||
app_state: &Arc<AppState>
|
app_state: &Arc<AppState>
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let conn = app_state.conn.clone();
|
// Get all config values before starting async operations
|
||||||
let config_manager = ConfigManager::new(conn.clone());
|
let config_values = {
|
||||||
|
let conn_arc = app_state.conn.clone();
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let mut conn = conn_arc.lock().unwrap();
|
||||||
|
let config_manager = ConfigManager::new(Arc::clone(&conn_arc));
|
||||||
|
|
||||||
let default_bot_id = {
|
let default_bot_id = bots.filter(name.eq("default"))
|
||||||
let mut conn = conn.lock().unwrap();
|
|
||||||
bots.filter(name.eq("default"))
|
|
||||||
.select(id)
|
.select(id)
|
||||||
.first::<uuid::Uuid>(&mut *conn)
|
.first::<uuid::Uuid>(&mut *conn)
|
||||||
.unwrap_or_else(|_| uuid::Uuid::nil())
|
.unwrap_or_else(|_| uuid::Uuid::nil());
|
||||||
};
|
|
||||||
|
|
||||||
let llm_url = config_manager.get_config(&default_bot_id, "llm-url", None)?;
|
(
|
||||||
let llm_model = config_manager.get_config(&default_bot_id, "llm-model", None)?;
|
default_bot_id,
|
||||||
let embedding_url = config_manager.get_config(&default_bot_id, "embedding-url", None)?;
|
config_manager.get_config(&default_bot_id, "llm-url", None).unwrap_or_default(),
|
||||||
let embedding_model = config_manager.get_config(&default_bot_id, "embedding-model", None)?;
|
config_manager.get_config(&default_bot_id, "llm-model", None).unwrap_or_default(),
|
||||||
let llm_server_path = config_manager.get_config(&default_bot_id, "llm-server-path", None)?;
|
config_manager.get_config(&default_bot_id, "embedding-url", None).unwrap_or_default(),
|
||||||
|
config_manager.get_config(&default_bot_id, "embedding-model", None).unwrap_or_default(),
|
||||||
|
config_manager.get_config(&default_bot_id, "llm-server-path", None).unwrap_or_default(),
|
||||||
|
)
|
||||||
|
}).await?
|
||||||
|
};
|
||||||
|
let (_default_bot_id, llm_url, llm_model, embedding_url, embedding_model, llm_server_path) = config_values;
|
||||||
|
|
||||||
info!("Starting LLM servers...");
|
info!("Starting LLM servers...");
|
||||||
info!("Configuration:");
|
info!("Configuration:");
|
||||||
|
|
|
||||||
119
src/main.rs
119
src/main.rs
|
|
@ -3,8 +3,7 @@ use actix_cors::Cors;
|
||||||
use actix_web::middleware::Logger;
|
use actix_web::middleware::Logger;
|
||||||
use actix_web::{web, App, HttpServer};
|
use actix_web::{web, App, HttpServer};
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use log::error;
|
use log::{error, info};
|
||||||
use log::info;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
mod auth;
|
mod auth;
|
||||||
|
|
@ -47,6 +46,18 @@ use crate::session::{create_session, get_session_history, get_sessions, start_se
|
||||||
use crate::shared::state::AppState;
|
use crate::shared::state::AppState;
|
||||||
use crate::web_server::{bot_index, index, static_files};
|
use crate::web_server::{bot_index, index, static_files};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum BootstrapProgress {
|
||||||
|
StartingBootstrap,
|
||||||
|
InstallingComponent(String),
|
||||||
|
StartingComponent(String),
|
||||||
|
UploadingTemplates,
|
||||||
|
ConnectingDatabase,
|
||||||
|
StartingLLM,
|
||||||
|
BootstrapComplete,
|
||||||
|
BootstrapError(String),
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
use crate::llm::local::ensure_llama_servers_running;
|
use crate::llm::local::ensure_llama_servers_running;
|
||||||
|
|
@ -79,27 +90,41 @@ async fn main() -> std::io::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
|
let (progress_tx, progress_rx) = tokio::sync::mpsc::unbounded_channel::<BootstrapProgress>();
|
||||||
|
let (state_tx, state_rx) = tokio::sync::mpsc::channel::<Arc<AppState>>(1);
|
||||||
let ui_handle = if !no_ui {
|
let ui_handle = if !no_ui {
|
||||||
let (ui_tx, mut ui_rx) = tokio::sync::mpsc::channel::<Arc<AppState>>(1);
|
let progress_rx = Arc::new(tokio::sync::Mutex::new(progress_rx));
|
||||||
|
let state_rx = Arc::new(tokio::sync::Mutex::new(state_rx));
|
||||||
let handle = std::thread::Builder::new()
|
let handle = std::thread::Builder::new()
|
||||||
.name("ui-thread".to_string())
|
.name("ui-thread".to_string())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
let mut ui = crate::ui_tree::XtreeUI::new();
|
let mut ui = crate::ui_tree::XtreeUI::new();
|
||||||
|
ui.set_progress_channel(progress_rx.clone());
|
||||||
let rt = tokio::runtime::Builder::new_current_thread()
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()
|
.build()
|
||||||
.expect("Failed to create UI runtime");
|
.expect("Failed to create UI runtime");
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
if let Some(app_state) = ui_rx.recv().await {
|
tokio::select! {
|
||||||
|
result = async {
|
||||||
|
let mut rx = state_rx.lock().await;
|
||||||
|
rx.recv().await
|
||||||
|
} => {
|
||||||
|
if let Some(app_state) = result {
|
||||||
ui.set_app_state(app_state);
|
ui.set_app_state(app_state);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
_ = tokio::time::sleep(tokio::time::Duration::from_secs(300)) => {
|
||||||
|
eprintln!("UI initialization timeout");
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if let Err(e) = ui.start_ui() {
|
if let Err(e) = ui.start_ui() {
|
||||||
eprintln!("UI error: {}", e);
|
eprintln!("UI error: {}", e);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.expect("Failed to spawn UI thread");
|
.expect("Failed to spawn UI thread");
|
||||||
Some((handle, ui_tx))
|
Some(handle)
|
||||||
} else {
|
} else {
|
||||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
|
||||||
.write_style(env_logger::WriteStyle::Always)
|
.write_style(env_logger::WriteStyle::Always)
|
||||||
|
|
@ -116,14 +141,22 @@ async fn main() -> std::io::Result<()> {
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
let progress_tx_clone = progress_tx.clone();
|
||||||
|
let cfg = {
|
||||||
|
progress_tx_clone.send(BootstrapProgress::StartingBootstrap).ok();
|
||||||
let mut bootstrap = BootstrapManager::new(install_mode.clone(), tenant.clone()).await;
|
let mut bootstrap = BootstrapManager::new(install_mode.clone(), tenant.clone()).await;
|
||||||
let env_path = std::env::current_dir()?
|
let env_path = match std::env::current_dir() {
|
||||||
.join("botserver-stack")
|
Ok(dir) => dir.join("botserver-stack").join(".env"),
|
||||||
.join(".env");
|
Err(_) => {
|
||||||
|
progress_tx_clone.send(BootstrapProgress::BootstrapError("Failed to get current directory".to_string())).ok();
|
||||||
|
return Err(std::io::Error::new(std::io::ErrorKind::Other, "Failed to get current directory"));
|
||||||
|
}
|
||||||
|
};
|
||||||
let cfg = if env_path.exists() {
|
let cfg = if env_path.exists() {
|
||||||
match diesel::Connection::establish(&std::env::var("DATABASE_URL").unwrap()) {
|
progress_tx_clone.send(BootstrapProgress::ConnectingDatabase).ok();
|
||||||
|
match diesel::Connection::establish(&std::env::var("DATABASE_URL").unwrap_or_else(|_| "postgres://gbuser:@localhost:5432/botserver".to_string())) {
|
||||||
Ok(mut conn) => {
|
Ok(mut conn) => {
|
||||||
AppConfig::from_database(&mut conn).expect("Failed to load config from DB")
|
AppConfig::from_database(&mut conn).unwrap_or_else(|_| AppConfig::from_env().expect("Failed to load config"))
|
||||||
}
|
}
|
||||||
Err(_) => AppConfig::from_env().expect("Failed to load config from env"),
|
Err(_) => AppConfig::from_env().expect("Failed to load config from env"),
|
||||||
}
|
}
|
||||||
|
|
@ -131,31 +164,36 @@ async fn main() -> std::io::Result<()> {
|
||||||
match bootstrap.bootstrap().await {
|
match bootstrap.bootstrap().await {
|
||||||
Ok(config) => config,
|
Ok(config) => config,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Bootstrap failed: {}", e);
|
progress_tx_clone.send(BootstrapProgress::BootstrapError(format!("Bootstrap failed: {}", e))).ok();
|
||||||
match diesel::Connection::establish(
|
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) => {
|
Ok(mut conn) => {
|
||||||
AppConfig::from_database(&mut conn).expect("Failed to load config from DB")
|
AppConfig::from_database(&mut conn).unwrap_or_else(|_| AppConfig::from_env().expect("Failed to load config"))
|
||||||
}
|
}
|
||||||
Err(_) => AppConfig::from_env().expect("Failed to load config from env"),
|
Err(_) => AppConfig::from_env().expect("Failed to load config from env"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
progress_tx_clone.send(BootstrapProgress::StartingComponent("all services".to_string())).ok();
|
||||||
if let Err(e) = bootstrap.start_all() {
|
if let Err(e) = bootstrap.start_all() {
|
||||||
log::warn!("Failed to start all services: {}", e);
|
progress_tx_clone.send(BootstrapProgress::BootstrapError(format!("Failed to start services: {}", e))).ok();
|
||||||
}
|
}
|
||||||
if let Err(e) = futures::executor::block_on(bootstrap.upload_templates_to_drive(&cfg)) {
|
progress_tx_clone.send(BootstrapProgress::UploadingTemplates).ok();
|
||||||
log::warn!("Failed to upload templates to MinIO: {}", e);
|
if let Err(e) = bootstrap.upload_templates_to_drive(&cfg).await {
|
||||||
|
progress_tx_clone.send(BootstrapProgress::BootstrapError(format!("Failed to upload templates: {}", e))).ok();
|
||||||
}
|
}
|
||||||
|
Ok::<AppConfig, std::io::Error>(cfg)
|
||||||
|
};
|
||||||
|
let cfg = cfg?;
|
||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
let refreshed_cfg = AppConfig::from_env().expect("Failed to load config from env");
|
let refreshed_cfg = AppConfig::from_env().expect("Failed to load config from env");
|
||||||
let config = std::sync::Arc::new(refreshed_cfg.clone());
|
let config = std::sync::Arc::new(refreshed_cfg.clone());
|
||||||
|
progress_tx.send(BootstrapProgress::ConnectingDatabase).ok();
|
||||||
let db_pool = match diesel::Connection::establish(&refreshed_cfg.database_url()) {
|
let db_pool = match diesel::Connection::establish(&refreshed_cfg.database_url()) {
|
||||||
Ok(conn) => Arc::new(Mutex::new(conn)),
|
Ok(conn) => Arc::new(Mutex::new(conn)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to connect to main database: {}", e);
|
error!("Failed to connect to main database: {}", e);
|
||||||
|
progress_tx.send(BootstrapProgress::BootstrapError(format!("Database connection failed: {}", e))).ok();
|
||||||
return Err(std::io::Error::new(
|
return Err(std::io::Error::new(
|
||||||
std::io::ErrorKind::ConnectionRefused,
|
std::io::ErrorKind::ConnectionRefused,
|
||||||
format!("Database connection failed: {}", e),
|
format!("Database connection failed: {}", e),
|
||||||
|
|
@ -214,27 +252,18 @@ async fn main() -> std::io::Result<()> {
|
||||||
web_adapter: web_adapter.clone(),
|
web_adapter: web_adapter.clone(),
|
||||||
voice_adapter: voice_adapter.clone(),
|
voice_adapter: voice_adapter.clone(),
|
||||||
});
|
});
|
||||||
if let Some((_, ui_tx)) = &ui_handle {
|
state_tx.send(app_state.clone()).await.ok();
|
||||||
ui_tx.send(app_state.clone()).await.ok();
|
progress_tx.send(BootstrapProgress::BootstrapComplete).ok();
|
||||||
}
|
info!("Starting HTTP server on {}:{}", config.server.host, config.server.port);
|
||||||
info!(
|
|
||||||
"Starting HTTP server on {}:{}",
|
|
||||||
config.server.host, config.server.port
|
|
||||||
);
|
|
||||||
|
|
||||||
let worker_count = std::thread::available_parallelism()
|
let worker_count = std::thread::available_parallelism()
|
||||||
.map(|n| n.get())
|
.map(|n| n.get())
|
||||||
.unwrap_or(4);
|
.unwrap_or(4);
|
||||||
|
|
||||||
let bot_orchestrator = BotOrchestrator::new(app_state.clone());
|
let bot_orchestrator = BotOrchestrator::new(app_state.clone());
|
||||||
|
tokio::spawn(async move {
|
||||||
if let Err(e) = bot_orchestrator.mount_all_bots().await {
|
if let Err(e) = bot_orchestrator.mount_all_bots().await {
|
||||||
log::error!("Failed to mount bots: {}", e);
|
error!("Failed to mount bots: {}", e);
|
||||||
let msg = format!("Bot mount failure: {}", e);
|
|
||||||
let _ = bot_orchestrator
|
|
||||||
.send_warning("System", "AdminBot", msg.as_str())
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
let automation_state = app_state.clone();
|
let automation_state = app_state.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let rt = tokio::runtime::Builder::new_current_thread()
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
|
@ -247,20 +276,19 @@ async fn main() -> std::io::Result<()> {
|
||||||
automation.spawn().await.ok();
|
automation.spawn().await.ok();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
let app_state_for_llm = app_state.clone();
|
||||||
if let Err(e) = ensure_llama_servers_running(&app_state).await {
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = ensure_llama_servers_running(&app_state_for_llm).await {
|
||||||
error!("Failed to start LLM servers: {}", e);
|
error!("Failed to start LLM servers: {}", e);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
HttpServer::new(move || {
|
let server_result = HttpServer::new(move || {
|
||||||
let cors = Cors::default()
|
let cors = Cors::default()
|
||||||
.allow_any_origin()
|
.allow_any_origin()
|
||||||
.allow_any_method()
|
.allow_any_method()
|
||||||
.allow_any_header()
|
.allow_any_header()
|
||||||
.max_age(3600);
|
.max_age(3600);
|
||||||
|
|
||||||
let app_state_clone = app_state.clone();
|
let app_state_clone = app_state.clone();
|
||||||
|
|
||||||
let mut app = App::new()
|
let mut app = App::new()
|
||||||
.wrap(cors)
|
.wrap(cors)
|
||||||
.wrap(Logger::default())
|
.wrap(Logger::default())
|
||||||
|
|
@ -280,8 +308,8 @@ async fn main() -> std::io::Result<()> {
|
||||||
.service(crate::bot::mount_bot_handler)
|
.service(crate::bot::mount_bot_handler)
|
||||||
.service(crate::bot::handle_user_input_handler)
|
.service(crate::bot::handle_user_input_handler)
|
||||||
.service(crate::bot::get_user_sessions_handler)
|
.service(crate::bot::get_user_sessions_handler)
|
||||||
.service(crate::bot::get_conversation_history_handler);
|
.service(crate::bot::get_conversation_history_handler)
|
||||||
|
.service(crate::bot::send_warning_handler);
|
||||||
#[cfg(feature = "email")]
|
#[cfg(feature = "email")]
|
||||||
{
|
{
|
||||||
app = app
|
app = app
|
||||||
|
|
@ -292,7 +320,6 @@ async fn main() -> std::io::Result<()> {
|
||||||
.service(save_draft)
|
.service(save_draft)
|
||||||
.service(save_click);
|
.service(save_click);
|
||||||
}
|
}
|
||||||
|
|
||||||
app = app.service(static_files);
|
app = app.service(static_files);
|
||||||
app = app.service(bot_index);
|
app = app.service(bot_index);
|
||||||
app
|
app
|
||||||
|
|
@ -300,5 +327,9 @@ async fn main() -> std::io::Result<()> {
|
||||||
.workers(worker_count)
|
.workers(worker_count)
|
||||||
.bind((config.server.host.clone(), config.server.port))?
|
.bind((config.server.host.clone(), config.server.port))?
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await;
|
||||||
|
if let Some(handle) = ui_handle {
|
||||||
|
handle.join().ok();
|
||||||
|
}
|
||||||
|
server_result
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use log::warn;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use sysinfo::{System};
|
use sysinfo::{System};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,31 @@ pub struct BotResponse {
|
||||||
pub context_max_length: usize,
|
pub context_max_length: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl BotResponse {
|
||||||
|
pub fn from_string_ids(
|
||||||
|
bot_id: &str,
|
||||||
|
session_id: &str,
|
||||||
|
user_id: &str,
|
||||||
|
content: String,
|
||||||
|
channel: String,
|
||||||
|
) -> Result<Self, anyhow::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
bot_id: bot_id.to_string(),
|
||||||
|
user_id: user_id.to_string(),
|
||||||
|
session_id: session_id.to_string(),
|
||||||
|
channel,
|
||||||
|
content,
|
||||||
|
message_type: 2,
|
||||||
|
stream_token: None,
|
||||||
|
is_complete: true,
|
||||||
|
suggestions: Vec::new(),
|
||||||
|
context_name: None,
|
||||||
|
context_length: 0,
|
||||||
|
context_max_length: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Identifiable, Insertable)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Identifiable, Insertable)]
|
||||||
#[diesel(table_name = bot_memories)]
|
#[diesel(table_name = bot_memories)]
|
||||||
pub struct BotMemory {
|
pub struct BotMemory {
|
||||||
|
|
|
||||||
139
src/ui_tree/chat_panel.rs
Normal file
139
src/ui_tree/chat_panel.rs
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
use color_eyre::Result;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use crate::shared::state::AppState;
|
||||||
|
use crate::shared::models::BotResponse;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub struct ChatPanel {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub app_state: Arc<AppState>,
|
||||||
|
pub messages: Vec<String>,
|
||||||
|
pub input_buffer: String,
|
||||||
|
pub session_id: Uuid,
|
||||||
|
pub user_id: Uuid,
|
||||||
|
pub response_rx: Option<mpsc::Receiver<BotResponse>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChatPanel {
|
||||||
|
pub fn new(app_state: Arc<AppState>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
app_state,
|
||||||
|
messages: vec!["Welcome to General Bots Console Chat!".to_string()],
|
||||||
|
input_buffer: String::new(),
|
||||||
|
session_id: Uuid::new_v4(),
|
||||||
|
user_id: Uuid::new_v4(),
|
||||||
|
response_rx: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_char(&mut self, c: char) {
|
||||||
|
self.input_buffer.push(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn backspace(&mut self) {
|
||||||
|
self.input_buffer.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_message(&mut self, bot_name: &str, app_state: &Arc<AppState>) -> Result<()> {
|
||||||
|
if self.input_buffer.trim().is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = self.input_buffer.clone();
|
||||||
|
self.messages.push(format!("You: {}", message));
|
||||||
|
self.input_buffer.clear();
|
||||||
|
|
||||||
|
let bot_id = self.get_bot_id(bot_name, app_state).await?;
|
||||||
|
|
||||||
|
let user_message = crate::shared::models::UserMessage {
|
||||||
|
bot_id: bot_id.to_string(),
|
||||||
|
user_id: self.user_id.to_string(),
|
||||||
|
session_id: self.session_id.to_string(),
|
||||||
|
channel: "console".to_string(),
|
||||||
|
content: message,
|
||||||
|
message_type: 1,
|
||||||
|
media_url: None,
|
||||||
|
timestamp: chrono::Utc::now(),
|
||||||
|
context_name: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::channel::<BotResponse>(100);
|
||||||
|
self.response_rx = Some(rx);
|
||||||
|
|
||||||
|
let orchestrator = crate::bot::BotOrchestrator::new(app_state.clone());
|
||||||
|
let _ = orchestrator.stream_response(user_message, tx).await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn poll_response(&mut self, _bot_name: &str) -> Result<()> {
|
||||||
|
if let Some(rx) = &mut self.response_rx {
|
||||||
|
while let Ok(response) = rx.try_recv() {
|
||||||
|
if !response.content.is_empty() && !response.is_complete {
|
||||||
|
if let Some(last_msg) = self.messages.last_mut() {
|
||||||
|
if last_msg.starts_with("Bot: ") {
|
||||||
|
last_msg.push_str(&response.content);
|
||||||
|
} else {
|
||||||
|
self.messages.push(format!("Bot: {}", response.content));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.messages.push(format!("Bot: {}", response.content));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.is_complete && response.content.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_bot_id(&self, bot_name: &str, app_state: &Arc<AppState>) -> Result<Uuid> {
|
||||||
|
use crate::shared::models::schema::bots::dsl::*;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
|
||||||
|
let mut conn = app_state.conn.lock().unwrap();
|
||||||
|
let bot_id = bots
|
||||||
|
.filter(name.eq(bot_name))
|
||||||
|
.select(id)
|
||||||
|
.first::<Uuid>(&mut *conn)?;
|
||||||
|
|
||||||
|
Ok(bot_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&self) -> String {
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
|
||||||
|
lines.push("╔═══════════════════════════════════════╗".to_string());
|
||||||
|
lines.push("║ CONVERSATION ║".to_string());
|
||||||
|
lines.push("╚═══════════════════════════════════════╝".to_string());
|
||||||
|
lines.push("".to_string());
|
||||||
|
|
||||||
|
let visible_start = if self.messages.len() > 15 {
|
||||||
|
self.messages.len() - 15
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
for msg in &self.messages[visible_start..] {
|
||||||
|
if msg.starts_with("You: ") {
|
||||||
|
lines.push(format!(" {}", msg));
|
||||||
|
} else if msg.starts_with("Bot: ") {
|
||||||
|
lines.push(format!(" {}", msg));
|
||||||
|
} else {
|
||||||
|
lines.push(format!(" {}", msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push("".to_string());
|
||||||
|
lines.push("─────────────────────────────────────────".to_string());
|
||||||
|
lines.push(format!(" > {}_", self.input_buffer));
|
||||||
|
lines.push("".to_string());
|
||||||
|
lines.push(" Enter: Send | Tab: Switch Panel".to_string());
|
||||||
|
|
||||||
|
lines.join("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -54,7 +54,7 @@ impl Editor {
|
||||||
&self.file_path
|
&self.file_path
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(&self) -> String {
|
pub fn render(&self, cursor_blink: bool) -> String {
|
||||||
let lines: Vec<&str> = self.content.lines().collect();
|
let lines: Vec<&str> = self.content.lines().collect();
|
||||||
let total_lines = lines.len().max(1);
|
let total_lines = lines.len().max(1);
|
||||||
let visible_lines = 25;
|
let visible_lines = 25;
|
||||||
|
|
@ -67,26 +67,35 @@ impl Editor {
|
||||||
|
|
||||||
let start = self.scroll_offset;
|
let start = self.scroll_offset;
|
||||||
let end = (start + visible_lines).min(total_lines);
|
let end = (start + visible_lines).min(total_lines);
|
||||||
let mut display_lines = Vec::new();
|
|
||||||
|
|
||||||
|
let mut display_lines = Vec::new();
|
||||||
for i in start..end {
|
for i in start..end {
|
||||||
let line_num = i + 1;
|
let line_num = i + 1;
|
||||||
let line_content = if i < lines.len() { lines[i] } else { "" };
|
let line_content = if i < lines.len() { lines[i] } else { "" };
|
||||||
let is_cursor_line = i == cursor_line;
|
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));
|
let cursor_indicator = if is_cursor_line && cursor_blink {
|
||||||
|
let spaces = " ".repeat(cursor_col);
|
||||||
|
format!("{}█", spaces)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
display_lines.push(format!(" {:4} │ {}{}", line_num, line_content, cursor_indicator));
|
||||||
}
|
}
|
||||||
|
|
||||||
if display_lines.is_empty() {
|
if display_lines.is_empty() {
|
||||||
display_lines.push(" ▶ 1 │ ".to_string());
|
let cursor_indicator = if cursor_blink { "█" } else { "" };
|
||||||
|
display_lines.push(format!(" 1 │ {}", cursor_indicator));
|
||||||
}
|
}
|
||||||
|
|
||||||
display_lines.push("".to_string());
|
display_lines.push("".to_string());
|
||||||
display_lines.push("─────────────────────────────────────────────────────────────".to_string());
|
display_lines.push("─────────────────────────────────────────────────────────────".to_string());
|
||||||
let status = if self.modified { "●" } else { "✓" };
|
let status = if self.modified { "MODIFIED" } else { "SAVED" };
|
||||||
display_lines.push(format!(" {} {} │ Line: {}, Col: {}",
|
display_lines.push(format!(" {} {} │ Line: {}, Col: {}",
|
||||||
status, self.file_path, cursor_line + 1, cursor_col + 1));
|
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.push(" Ctrl+S: Save │ Ctrl+W: Close │ Esc: Close without saving".to_string());
|
||||||
|
|
||||||
display_lines.join("\n")
|
display_lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ impl FileTree {
|
||||||
self.items.clear();
|
self.items.clear();
|
||||||
self.current_bucket = None;
|
self.current_bucket = None;
|
||||||
self.current_path.clear();
|
self.current_path.clear();
|
||||||
|
|
||||||
if let Some(drive) = &self.app_state.drive {
|
if let Some(drive) = &self.app_state.drive {
|
||||||
let result = drive.list_buckets().send().await;
|
let result = drive.list_buckets().send().await;
|
||||||
match result {
|
match result {
|
||||||
|
|
@ -52,6 +53,7 @@ impl FileTree {
|
||||||
} else {
|
} else {
|
||||||
self.items.push(("✗ Drive not connected".to_string(), TreeNode::Bucket { name: String::new() }));
|
self.items.push(("✗ Drive not connected".to_string(), TreeNode::Bucket { name: String::new() }));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.items.is_empty() {
|
if self.items.is_empty() {
|
||||||
self.items.push(("(no buckets found)".to_string(), TreeNode::Bucket { name: String::new() }));
|
self.items.push(("(no buckets found)".to_string(), TreeNode::Bucket { name: String::new() }));
|
||||||
}
|
}
|
||||||
|
|
@ -121,6 +123,7 @@ impl FileTree {
|
||||||
if let Some(token) = continuation_token {
|
if let Some(token) = continuation_token {
|
||||||
request = request.continuation_token(token);
|
request = request.continuation_token(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = request.send().await?;
|
let result = request.send().await?;
|
||||||
|
|
||||||
for obj in result.contents() {
|
for obj in result.contents() {
|
||||||
|
|
@ -142,14 +145,17 @@ impl FileTree {
|
||||||
if key == normalized_prefix {
|
if key == normalized_prefix {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let relative = if !normalized_prefix.is_empty() && key.starts_with(&normalized_prefix) {
|
let relative = if !normalized_prefix.is_empty() && key.starts_with(&normalized_prefix) {
|
||||||
&key[normalized_prefix.len()..]
|
&key[normalized_prefix.len()..]
|
||||||
} else {
|
} else {
|
||||||
&key
|
&key
|
||||||
};
|
};
|
||||||
|
|
||||||
if relative.is_empty() {
|
if relative.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(slash_pos) = relative.find('/') {
|
if let Some(slash_pos) = relative.find('/') {
|
||||||
let folder_name = &relative[..slash_pos];
|
let folder_name = &relative[..slash_pos];
|
||||||
if !folder_name.is_empty() {
|
if !folder_name.is_empty() {
|
||||||
|
|
@ -162,7 +168,6 @@ impl FileTree {
|
||||||
|
|
||||||
let mut folder_vec: Vec<String> = folders.into_iter().collect();
|
let mut folder_vec: Vec<String> = folders.into_iter().collect();
|
||||||
folder_vec.sort();
|
folder_vec.sort();
|
||||||
|
|
||||||
for folder_name in folder_vec {
|
for folder_name in folder_vec {
|
||||||
let full_path = if normalized_prefix.is_empty() {
|
let full_path = if normalized_prefix.is_empty() {
|
||||||
folder_name.clone()
|
folder_name.clone()
|
||||||
|
|
@ -178,7 +183,6 @@ impl FileTree {
|
||||||
}
|
}
|
||||||
|
|
||||||
files.sort_by(|(a, _), (b, _)| a.cmp(b));
|
files.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||||
|
|
||||||
for (name, full_path) in files {
|
for (name, full_path) in files {
|
||||||
let icon = if name.ends_with(".bas") {
|
let icon = if name.ends_with(".bas") {
|
||||||
"⚙️"
|
"⚙️"
|
||||||
|
|
@ -226,6 +230,27 @@ impl FileTree {
|
||||||
self.items.get(self.selected).map(|(_, node)| node)
|
self.items.get(self.selected).map(|(_, node)| node)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_selected_bot(&self) -> Option<String> {
|
||||||
|
if let Some(bucket) = &self.current_bucket {
|
||||||
|
if bucket.ends_with(".gbai") {
|
||||||
|
return Some(bucket.trim_end_matches(".gbai").to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((_, node)) = self.items.get(self.selected) {
|
||||||
|
match node {
|
||||||
|
TreeNode::Bucket { name } => {
|
||||||
|
if name.ends_with(".gbai") {
|
||||||
|
return Some(name.trim_end_matches(".gbai").to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
pub fn move_up(&mut self) {
|
pub fn move_up(&mut self) {
|
||||||
if self.selected > 0 {
|
if self.selected > 0 {
|
||||||
self.selected -= 1;
|
self.selected -= 1;
|
||||||
|
|
|
||||||
|
|
@ -46,11 +46,11 @@ impl Log for UiLogger {
|
||||||
if self.enabled(record.metadata()) {
|
if self.enabled(record.metadata()) {
|
||||||
let timestamp = Local::now().format("%H:%M:%S");
|
let timestamp = Local::now().format("%H:%M:%S");
|
||||||
let level_icon = match record.level() {
|
let level_icon = match record.level() {
|
||||||
log::Level::Error => "❌",
|
log::Level::Error => "ERR",
|
||||||
log::Level::Warn => "⚠️",
|
log::Level::Warn => "WRN",
|
||||||
log::Level::Info => "ℹ️",
|
log::Level::Info => "INF",
|
||||||
log::Level::Debug => "🔍",
|
log::Level::Debug => "DBG",
|
||||||
log::Level::Trace => "📝",
|
log::Level::Trace => "TRC",
|
||||||
};
|
};
|
||||||
let log_entry = format!("[{}] {} {}", timestamp, level_icon, record.args());
|
let log_entry = format!("[{}] {} {}", timestamp, level_icon, record.args());
|
||||||
if let Ok(mut panel) = self.log_panel.lock() {
|
if let Ok(mut panel) = self.log_panel.lock() {
|
||||||
|
|
|
||||||
|
|
@ -21,19 +21,24 @@ mod editor;
|
||||||
mod file_tree;
|
mod file_tree;
|
||||||
mod log_panel;
|
mod log_panel;
|
||||||
mod status_panel;
|
mod status_panel;
|
||||||
|
mod chat_panel;
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use file_tree::{FileTree, TreeNode};
|
use file_tree::{FileTree, TreeNode};
|
||||||
use log_panel::{init_logger, LogPanel};
|
use log_panel::{init_logger, LogPanel};
|
||||||
use status_panel::StatusPanel;
|
use status_panel::StatusPanel;
|
||||||
|
use chat_panel::ChatPanel;
|
||||||
|
|
||||||
pub struct XtreeUI {
|
pub struct XtreeUI {
|
||||||
app_state: Option<Arc<AppState>>,
|
app_state: Option<Arc<AppState>>,
|
||||||
file_tree: Option<FileTree>,
|
file_tree: Option<FileTree>,
|
||||||
status_panel: Option<StatusPanel>,
|
status_panel: Option<StatusPanel>,
|
||||||
log_panel: Arc<Mutex<LogPanel>>,
|
log_panel: Arc<Mutex<LogPanel>>,
|
||||||
|
chat_panel: Option<ChatPanel>,
|
||||||
editor: Option<Editor>,
|
editor: Option<Editor>,
|
||||||
active_panel: ActivePanel,
|
active_panel: ActivePanel,
|
||||||
should_quit: bool,
|
should_quit: bool,
|
||||||
|
progress_channel: Option<Arc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<crate::BootstrapProgress>>>>,
|
||||||
|
bootstrap_status: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
|
@ -42,6 +47,7 @@ enum ActivePanel {
|
||||||
Editor,
|
Editor,
|
||||||
Status,
|
Status,
|
||||||
Logs,
|
Logs,
|
||||||
|
Chat,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl XtreeUI {
|
impl XtreeUI {
|
||||||
|
|
@ -52,17 +58,26 @@ impl XtreeUI {
|
||||||
file_tree: None,
|
file_tree: None,
|
||||||
status_panel: None,
|
status_panel: None,
|
||||||
log_panel: log_panel.clone(),
|
log_panel: log_panel.clone(),
|
||||||
|
chat_panel: None,
|
||||||
editor: None,
|
editor: None,
|
||||||
active_panel: ActivePanel::Logs,
|
active_panel: ActivePanel::Logs,
|
||||||
should_quit: false,
|
should_quit: false,
|
||||||
|
progress_channel: None,
|
||||||
|
bootstrap_status: "Initializing...".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_progress_channel(&mut self, rx: Arc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<crate::BootstrapProgress>>>) {
|
||||||
|
self.progress_channel = Some(rx);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_app_state(&mut self, app_state: Arc<AppState>) {
|
pub fn set_app_state(&mut self, app_state: Arc<AppState>) {
|
||||||
self.file_tree = Some(FileTree::new(app_state.clone()));
|
self.file_tree = Some(FileTree::new(app_state.clone()));
|
||||||
self.status_panel = Some(StatusPanel::new(app_state.clone()));
|
self.status_panel = Some(StatusPanel::new(app_state.clone()));
|
||||||
|
self.chat_panel = Some(ChatPanel::new(app_state.clone()));
|
||||||
self.app_state = Some(app_state);
|
self.app_state = Some(app_state);
|
||||||
self.active_panel = ActivePanel::FileTree;
|
self.active_panel = ActivePanel::FileTree;
|
||||||
|
self.bootstrap_status = "Ready".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_ui(&mut self) -> Result<()> {
|
pub fn start_ui(&mut self) -> Result<()> {
|
||||||
|
|
@ -86,10 +101,32 @@ impl XtreeUI {
|
||||||
|
|
||||||
fn run_event_loop(&mut self, terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()> {
|
fn run_event_loop(&mut self, terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()> {
|
||||||
let mut last_update = std::time::Instant::now();
|
let mut last_update = std::time::Instant::now();
|
||||||
let update_interval = std::time::Duration::from_millis(500);
|
let update_interval = std::time::Duration::from_millis(1000);
|
||||||
let rt = tokio::runtime::Runtime::new()?;
|
let mut cursor_blink = false;
|
||||||
|
let mut last_blink = std::time::Instant::now();
|
||||||
|
let rt = tokio::runtime::Runtime::new()?; // create runtime once
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|f| self.render(f))?;
|
if let Some(ref progress_rx) = self.progress_channel {
|
||||||
|
if let Ok(mut rx) = progress_rx.try_lock() {
|
||||||
|
while let Ok(progress) = rx.try_recv() {
|
||||||
|
self.bootstrap_status = match progress {
|
||||||
|
crate::BootstrapProgress::StartingBootstrap => "Starting bootstrap...".to_string(),
|
||||||
|
crate::BootstrapProgress::InstallingComponent(name) => format!("Installing: {}", name),
|
||||||
|
crate::BootstrapProgress::StartingComponent(name) => format!("Starting: {}", name),
|
||||||
|
crate::BootstrapProgress::UploadingTemplates => "Uploading templates...".to_string(),
|
||||||
|
crate::BootstrapProgress::ConnectingDatabase => "Connecting to database...".to_string(),
|
||||||
|
crate::BootstrapProgress::StartingLLM => "Starting LLM servers...".to_string(),
|
||||||
|
crate::BootstrapProgress::BootstrapComplete => "Bootstrap complete".to_string(),
|
||||||
|
crate::BootstrapProgress::BootstrapError(msg) => format!("Error: {}", msg),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if last_blink.elapsed() >= std::time::Duration::from_millis(500) {
|
||||||
|
cursor_blink = !cursor_blink;
|
||||||
|
last_blink = std::time::Instant::now();
|
||||||
|
}
|
||||||
|
terminal.draw(|f| self.render(f, cursor_blink))?;
|
||||||
if self.app_state.is_some() && last_update.elapsed() >= update_interval {
|
if self.app_state.is_some() && last_update.elapsed() >= update_interval {
|
||||||
if let Err(e) = rt.block_on(self.update_data()) {
|
if let Err(e) = rt.block_on(self.update_data()) {
|
||||||
let mut log_panel = self.log_panel.lock().unwrap();
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
|
|
@ -112,42 +149,101 @@ impl XtreeUI {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(&self, f: &mut Frame) {
|
fn render(&mut self, f: &mut Frame, cursor_blink: bool) {
|
||||||
let bg = Color::Rgb(15, 15, 25);
|
let bg = Color::Rgb(0, 30, 100);
|
||||||
let border_active = Color::Rgb(120, 220, 255);
|
let border_active = Color::Rgb(85, 255, 255);
|
||||||
let border_inactive = Color::Rgb(70, 70, 90);
|
let border_inactive = Color::Rgb(170, 170, 170);
|
||||||
let text = Color::Rgb(240, 240, 245);
|
let text = Color::Rgb(255, 255, 255);
|
||||||
let highlight = Color::Rgb(90, 180, 255);
|
let highlight = Color::Rgb(0, 170, 170);
|
||||||
let title = Color::Rgb(255, 230, 140);
|
let title_bg = Color::Rgb(170, 170, 170);
|
||||||
|
let title_fg = Color::Rgb(0, 0, 0);
|
||||||
if self.app_state.is_none() {
|
if self.app_state.is_none() {
|
||||||
self.render_loading(f, bg, text, border_active, title);
|
self.render_loading(f, bg, text, border_active, title_bg, title_fg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let main_chunks = Layout::default()
|
let main_chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([Constraint::Min(0), Constraint::Length(12)])
|
.constraints([
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Min(0),
|
||||||
|
Constraint::Length(12)
|
||||||
|
])
|
||||||
.split(f.area());
|
.split(f.area());
|
||||||
|
self.render_header(f, main_chunks[0], bg, title_bg, title_fg);
|
||||||
if self.editor.is_some() {
|
if self.editor.is_some() {
|
||||||
let editor_chunks = Layout::default()
|
let content_chunks = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints([Constraint::Percentage(20), Constraint::Percentage(80)])
|
.constraints([Constraint::Percentage(25), Constraint::Percentage(40), Constraint::Percentage(35)])
|
||||||
.split(main_chunks[0]);
|
.split(main_chunks[1]);
|
||||||
self.render_file_tree(f, editor_chunks[0], bg, text, border_active, border_inactive, highlight, title);
|
self.render_file_tree(f, content_chunks[0], bg, text, border_active, border_inactive, highlight, title_bg, title_fg);
|
||||||
if let Some(editor) = &self.editor {
|
if let Some(editor) = &self.editor {
|
||||||
self.render_editor(f, editor_chunks[1], editor, bg, text, border_active, border_inactive, highlight, title);
|
self.render_editor(f, content_chunks[1], editor, bg, text, border_active, border_inactive, highlight, title_bg, title_fg, cursor_blink);
|
||||||
}
|
}
|
||||||
|
self.render_chat(f, content_chunks[2], bg, text, border_active, border_inactive, highlight, title_bg, title_fg);
|
||||||
} else {
|
} else {
|
||||||
let top_chunks = Layout::default()
|
let content_chunks = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
|
.constraints([Constraint::Percentage(25), Constraint::Percentage(40), Constraint::Percentage(35)])
|
||||||
.split(main_chunks[0]);
|
.split(main_chunks[1]);
|
||||||
self.render_file_tree(f, top_chunks[0], bg, text, border_active, border_inactive, highlight, title);
|
self.render_file_tree(f, content_chunks[0], bg, text, border_active, border_inactive, highlight, title_bg, title_fg);
|
||||||
self.render_status(f, top_chunks[1], bg, text, border_active, border_inactive, highlight, title);
|
let right_chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||||
|
.split(content_chunks[1]);
|
||||||
|
self.render_status(f, right_chunks[0], bg, text, border_active, border_inactive, highlight, title_bg, title_fg);
|
||||||
|
self.render_chat(f, content_chunks[2], bg, text, border_active, border_inactive, highlight, title_bg, title_fg);
|
||||||
}
|
}
|
||||||
self.render_logs(f, main_chunks[1], bg, text, border_active, border_inactive, highlight, title);
|
self.render_logs(f, main_chunks[2], bg, text, border_active, border_inactive, highlight, title_bg, title_fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_loading(&self, f: &mut Frame, bg: Color, text: Color, border: Color, title: Color) {
|
fn render_header(&self, f: &mut Frame, area: Rect, _bg: Color, title_bg: Color, title_fg: Color) {
|
||||||
|
let block = Block::default()
|
||||||
|
.style(Style::default().bg(title_bg));
|
||||||
|
f.render_widget(block, area);
|
||||||
|
let title = if self.app_state.is_some() {
|
||||||
|
let components = vec![
|
||||||
|
("Tables", "postgres", "5432"),
|
||||||
|
("Cache", "valkey-server", "6379"),
|
||||||
|
("Drive", "minio", "9000"),
|
||||||
|
("LLM", "llama-server", "8081")
|
||||||
|
];
|
||||||
|
let statuses: Vec<String> = components.iter().map(|(comp_name, process, _port)| {
|
||||||
|
let status = if status_panel::StatusPanel::check_component_running(process) {
|
||||||
|
format!("🟢 {}", comp_name)
|
||||||
|
} else {
|
||||||
|
format!("🔴 {}", comp_name)
|
||||||
|
};
|
||||||
|
status
|
||||||
|
}).collect();
|
||||||
|
format!(" GENERAL BOTS ┃ {} ", statuses.join(" ┃ "))
|
||||||
|
} else {
|
||||||
|
" GENERAL BOTS ".to_string()
|
||||||
|
};
|
||||||
|
let title_len = title.len() as u16;
|
||||||
|
let centered_x = (area.width.saturating_sub(title_len)) / 2;
|
||||||
|
let centered_y = area.y + 1;
|
||||||
|
let x = area.x + centered_x;
|
||||||
|
let max_width = area.width.saturating_sub(x - area.x);
|
||||||
|
let width = title_len.min(max_width);
|
||||||
|
let title_span = Span::styled(
|
||||||
|
title,
|
||||||
|
Style::default()
|
||||||
|
.fg(title_fg)
|
||||||
|
.bg(title_bg)
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
|
);
|
||||||
|
f.render_widget(
|
||||||
|
Paragraph::new(Line::from(title_span)),
|
||||||
|
Rect {
|
||||||
|
x,
|
||||||
|
y: centered_y,
|
||||||
|
width,
|
||||||
|
height: 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_loading(&self, f: &mut Frame, bg: Color, text: Color, border: Color, title_bg: Color, title_fg: Color) {
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([Constraint::Percentage(40), Constraint::Percentage(20), Constraint::Percentage(40)])
|
.constraints([Constraint::Percentage(40), Constraint::Percentage(20), Constraint::Percentage(40)])
|
||||||
|
|
@ -157,23 +253,14 @@ impl XtreeUI {
|
||||||
.constraints([Constraint::Percentage(30), Constraint::Percentage(40), Constraint::Percentage(30)])
|
.constraints([Constraint::Percentage(30), Constraint::Percentage(40), Constraint::Percentage(30)])
|
||||||
.split(chunks[1])[1];
|
.split(chunks[1])[1];
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.title(Span::styled(" 🚀 BOTSERVER ", Style::default().fg(title).add_modifier(Modifier::BOLD)))
|
.title(Span::styled(" General Bots ", Style::default().fg(title_fg).bg(title_bg).add_modifier(Modifier::BOLD)))
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::default().fg(border))
|
.border_style(Style::default().fg(border))
|
||||||
.style(Style::default().bg(bg));
|
.style(Style::default().bg(bg));
|
||||||
let loading_text = vec![
|
let loading_text = format!(
|
||||||
"",
|
"\n ╔════════════════════════════════╗\n ║ ║\n ║ Initializing System... ║\n ║ ║\n ║ {} ║\n ║ ║\n ╚════════════════════════════════╝\n",
|
||||||
" ╔════════════════════════════════╗",
|
format!("{:^30}", self.bootstrap_status)
|
||||||
" ║ ║",
|
);
|
||||||
" ║ ⚡ Initializing System... ║",
|
|
||||||
" ║ ║",
|
|
||||||
" ║ Loading components... ║",
|
|
||||||
" ║ Connecting to services... ║",
|
|
||||||
" ║ Preparing interface... ║",
|
|
||||||
" ║ ║",
|
|
||||||
" ╚════════════════════════════════╝",
|
|
||||||
"",
|
|
||||||
].join("\n");
|
|
||||||
let paragraph = Paragraph::new(loading_text)
|
let paragraph = Paragraph::new(loading_text)
|
||||||
.block(block)
|
.block(block)
|
||||||
.style(Style::default().fg(text))
|
.style(Style::default().fg(text))
|
||||||
|
|
@ -181,7 +268,7 @@ impl XtreeUI {
|
||||||
f.render_widget(paragraph, center);
|
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) {
|
fn render_file_tree(&self, f: &mut Frame, area: Rect, bg: Color, text: Color, border_active: Color, border_inactive: Color, highlight: Color, title_bg: Color, title_fg: Color) {
|
||||||
if let Some(file_tree) = &self.file_tree {
|
if let Some(file_tree) = &self.file_tree {
|
||||||
let items = file_tree.render_items();
|
let items = file_tree.render_items();
|
||||||
let selected = file_tree.selected_index();
|
let selected = file_tree.selected_index();
|
||||||
|
|
@ -196,42 +283,39 @@ impl XtreeUI {
|
||||||
let is_active = self.active_panel == ActivePanel::FileTree;
|
let is_active = self.active_panel == ActivePanel::FileTree;
|
||||||
let border_color = if is_active { border_active } else { border_inactive };
|
let border_color = if is_active { border_active } else { border_inactive };
|
||||||
let title_style = if is_active {
|
let title_style = if is_active {
|
||||||
Style::default().fg(title).add_modifier(Modifier::BOLD)
|
Style::default().fg(title_fg).bg(title_bg).add_modifier(Modifier::BOLD)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(text)
|
Style::default().fg(title_fg).bg(title_bg)
|
||||||
};
|
};
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.title(Span::styled(" 📁 FILE EXPLORER ", title_style))
|
.title(Span::styled(" FILE EXPLORER ", title_style))
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::default().fg(border_color))
|
.border_style(Style::default().fg(border_color))
|
||||||
.style(Style::default().bg(bg));
|
.style(Style::default().bg(bg));
|
||||||
let list = List::new(list_items).block(block);
|
let list = List::new(list_items).block(block);
|
||||||
f.render_widget(list, area);
|
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) {
|
fn render_status(&mut self, f: &mut Frame, area: Rect, bg: Color, text: Color, border_active: Color, border_inactive: Color, _highlight: Color, title_bg: Color, title_fg: Color) {
|
||||||
let status_text = if let Some(status_panel) = &self.status_panel {
|
let selected_bot_opt = self.file_tree.as_ref().and_then(|ft| ft.get_selected_bot());
|
||||||
status_panel.render()
|
let status_text = if let Some(status_panel) = &mut self.status_panel {
|
||||||
|
match selected_bot_opt {
|
||||||
|
Some(bot) => status_panel.render(Some(bot)),
|
||||||
|
None => status_panel.render(None),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
"Waiting for initialization...".to_string()
|
"Waiting for initialization...".to_string()
|
||||||
};
|
};
|
||||||
let is_active = self.active_panel == ActivePanel::Status;
|
let is_active = self.active_panel == ActivePanel::Status;
|
||||||
let border_color = if is_active { border_active } else { border_inactive };
|
let border_color = if is_active { border_active } else { border_inactive };
|
||||||
let title_style = if is_active {
|
let title_style = if is_active {
|
||||||
Style::default().fg(title).add_modifier(Modifier::BOLD)
|
Style::default().fg(title_fg).bg(title_bg).add_modifier(Modifier::BOLD)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(text)
|
Style::default().fg(title_fg).bg(title_bg)
|
||||||
};
|
};
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.title(Span::styled(" 📊 SYSTEM STATUS ", title_style))
|
.title(Span::styled(" SYSTEM STATUS ", title_style))
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::default().fg(border_color))
|
.border_style(Style::default().fg(border_color))
|
||||||
.style(Style::default().bg(bg));
|
.style(Style::default().bg(bg));
|
||||||
|
|
@ -242,21 +326,21 @@ impl XtreeUI {
|
||||||
f.render_widget(paragraph, area);
|
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) {
|
fn render_editor(&self, f: &mut Frame, area: Rect, editor: &Editor, bg: Color, text: Color, border_active: Color, border_inactive: Color, _highlight: Color, title_bg: Color, title_fg: Color, cursor_blink: bool) {
|
||||||
let is_active = self.active_panel == ActivePanel::Editor;
|
let is_active = self.active_panel == ActivePanel::Editor;
|
||||||
let border_color = if is_active { border_active } else { border_inactive };
|
let border_color = if is_active { border_active } else { border_inactive };
|
||||||
let title_style = if is_active {
|
let title_style = if is_active {
|
||||||
Style::default().fg(title).add_modifier(Modifier::BOLD)
|
Style::default().fg(title_fg).bg(title_bg).add_modifier(Modifier::BOLD)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(text)
|
Style::default().fg(title_fg).bg(title_bg)
|
||||||
};
|
};
|
||||||
let title_text = format!(" ✏️ EDITOR: {} ", editor.file_path());
|
let title_text = format!(" EDITOR: {} ", editor.file_path());
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.title(Span::styled(title_text, title_style))
|
.title(Span::styled(title_text, title_style))
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::default().fg(border_color))
|
.border_style(Style::default().fg(border_color))
|
||||||
.style(Style::default().bg(bg));
|
.style(Style::default().bg(bg));
|
||||||
let content = editor.render();
|
let content = editor.render(cursor_blink);
|
||||||
let paragraph = Paragraph::new(content)
|
let paragraph = Paragraph::new(content)
|
||||||
.block(block)
|
.block(block)
|
||||||
.style(Style::default().fg(text))
|
.style(Style::default().fg(text))
|
||||||
|
|
@ -264,7 +348,36 @@ impl XtreeUI {
|
||||||
f.render_widget(paragraph, area);
|
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) {
|
fn render_chat(&self, f: &mut Frame, area: Rect, bg: Color, text: Color, border_active: Color, border_inactive: Color, _highlight: Color, title_bg: Color, title_fg: Color) {
|
||||||
|
if let Some(chat_panel) = &self.chat_panel {
|
||||||
|
let is_active = self.active_panel == ActivePanel::Chat;
|
||||||
|
let border_color = if is_active { border_active } else { border_inactive };
|
||||||
|
let title_style = if is_active {
|
||||||
|
Style::default().fg(title_fg).bg(title_bg).add_modifier(Modifier::BOLD)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(title_fg).bg(title_bg)
|
||||||
|
};
|
||||||
|
let selected_bot = if let Some(file_tree) = &self.file_tree {
|
||||||
|
file_tree.get_selected_bot().unwrap_or("No bot selected".to_string())
|
||||||
|
} else {
|
||||||
|
"No bot selected".to_string()
|
||||||
|
};
|
||||||
|
let title_text = format!(" CHAT: {} ", selected_bot);
|
||||||
|
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 = chat_panel.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_bg: Color, title_fg: Color) {
|
||||||
let log_panel = self.log_panel.try_lock();
|
let log_panel = self.log_panel.try_lock();
|
||||||
let log_lines = if let Ok(panel) = log_panel {
|
let log_lines = if let Ok(panel) = log_panel {
|
||||||
panel.render()
|
panel.render()
|
||||||
|
|
@ -274,12 +387,12 @@ impl XtreeUI {
|
||||||
let is_active = self.active_panel == ActivePanel::Logs;
|
let is_active = self.active_panel == ActivePanel::Logs;
|
||||||
let border_color = if is_active { border_active } else { border_inactive };
|
let border_color = if is_active { border_active } else { border_inactive };
|
||||||
let title_style = if is_active {
|
let title_style = if is_active {
|
||||||
Style::default().fg(title).add_modifier(Modifier::BOLD)
|
Style::default().fg(title_fg).bg(title_bg).add_modifier(Modifier::BOLD)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(text)
|
Style::default().fg(title_fg).bg(title_bg)
|
||||||
};
|
};
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.title(Span::styled(" 📜 SYSTEM LOGS ", title_style))
|
.title(Span::styled(" SYSTEM LOGS ", title_style))
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::default().fg(border_color))
|
.border_style(Style::default().fg(border_color))
|
||||||
.style(Style::default().bg(bg));
|
.style(Style::default().bg(bg));
|
||||||
|
|
@ -305,7 +418,7 @@ impl XtreeUI {
|
||||||
log_panel.add_log(&format!("Save failed: {}", e));
|
log_panel.add_log(&format!("Save failed: {}", e));
|
||||||
} else {
|
} else {
|
||||||
let mut log_panel = self.log_panel.lock().unwrap();
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
log_panel.add_log(&format!("✓ Saved: {}", editor.file_path()));
|
log_panel.add_log(&format!("Saved: {}", editor.file_path()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -316,7 +429,7 @@ impl XtreeUI {
|
||||||
self.editor = None;
|
self.editor = None;
|
||||||
self.active_panel = ActivePanel::FileTree;
|
self.active_panel = ActivePanel::FileTree;
|
||||||
let mut log_panel = self.log_panel.lock().unwrap();
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
log_panel.add_log("✓ Closed editor");
|
log_panel.add_log("Closed editor");
|
||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
@ -341,7 +454,7 @@ impl XtreeUI {
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
if let Err(e) = self.handle_tree_enter().await {
|
if let Err(e) = self.handle_tree_enter().await {
|
||||||
let mut log_panel = self.log_panel.lock().unwrap();
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
log_panel.add_log(&format!("✗ Enter error: {}", e));
|
log_panel.add_log(&format!("Enter error: {}", e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
|
|
@ -349,13 +462,13 @@ impl XtreeUI {
|
||||||
if file_tree.go_up() {
|
if file_tree.go_up() {
|
||||||
if let Err(e) = file_tree.refresh_current().await {
|
if let Err(e) = file_tree.refresh_current().await {
|
||||||
let mut log_panel = self.log_panel.lock().unwrap();
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
log_panel.add_log(&format!("✗ Navigation error: {}", e));
|
log_panel.add_log(&format!("Navigation error: {}", e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Tab => {
|
KeyCode::Tab => {
|
||||||
self.active_panel = ActivePanel::Status;
|
self.active_panel = ActivePanel::Chat;
|
||||||
}
|
}
|
||||||
KeyCode::Char('q') => {
|
KeyCode::Char('q') => {
|
||||||
self.should_quit = true;
|
self.should_quit = true;
|
||||||
|
|
@ -364,10 +477,10 @@ impl XtreeUI {
|
||||||
if let Some(file_tree) = &mut self.file_tree {
|
if let Some(file_tree) = &mut self.file_tree {
|
||||||
if let Err(e) = file_tree.refresh_current().await {
|
if let Err(e) = file_tree.refresh_current().await {
|
||||||
let mut log_panel = self.log_panel.lock().unwrap();
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
log_panel.add_log(&format!("✗ Refresh failed: {}", e));
|
log_panel.add_log(&format!("Refresh failed: {}", e));
|
||||||
} else {
|
} else {
|
||||||
let mut log_panel = self.log_panel.lock().unwrap();
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
log_panel.add_log("✓ Refreshed");
|
log_panel.add_log("Refreshed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -384,18 +497,44 @@ impl XtreeUI {
|
||||||
KeyCode::Backspace => editor.backspace(),
|
KeyCode::Backspace => editor.backspace(),
|
||||||
KeyCode::Enter => editor.insert_newline(),
|
KeyCode::Enter => editor.insert_newline(),
|
||||||
KeyCode::Tab => {
|
KeyCode::Tab => {
|
||||||
self.active_panel = ActivePanel::FileTree;
|
self.active_panel = ActivePanel::Chat;
|
||||||
}
|
}
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
self.editor = None;
|
self.editor = None;
|
||||||
self.active_panel = ActivePanel::FileTree;
|
self.active_panel = ActivePanel::FileTree;
|
||||||
let mut log_panel = self.log_panel.lock().unwrap();
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
log_panel.add_log("✓ Closed editor");
|
log_panel.add_log("Closed editor");
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ActivePanel::Chat => match key {
|
||||||
|
KeyCode::Tab => {
|
||||||
|
self.active_panel = ActivePanel::FileTree;
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
if let (Some(chat_panel), Some(file_tree), Some(app_state)) = (&mut self.chat_panel, &self.file_tree, &self.app_state) {
|
||||||
|
if let Some(bot_name) = file_tree.get_selected_bot() {
|
||||||
|
if let Err(e) = chat_panel.send_message(&bot_name, app_state).await {
|
||||||
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
|
log_panel.add_log(&format!("Chat error: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char(c) => {
|
||||||
|
if let Some(chat_panel) = &mut self.chat_panel {
|
||||||
|
chat_panel.add_char(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Backspace => {
|
||||||
|
if let Some(chat_panel) = &mut self.chat_panel {
|
||||||
|
chat_panel.backspace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
ActivePanel::Status => match key {
|
ActivePanel::Status => match key {
|
||||||
KeyCode::Tab => {
|
KeyCode::Tab => {
|
||||||
self.active_panel = ActivePanel::Logs;
|
self.active_panel = ActivePanel::Logs;
|
||||||
|
|
@ -419,12 +558,12 @@ impl XtreeUI {
|
||||||
TreeNode::Bucket { name, .. } => {
|
TreeNode::Bucket { name, .. } => {
|
||||||
file_tree.enter_bucket(name.clone()).await?;
|
file_tree.enter_bucket(name.clone()).await?;
|
||||||
let mut log_panel = self.log_panel.lock().unwrap();
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
log_panel.add_log(&format!("📂 Opened bucket: {}", name));
|
log_panel.add_log(&format!("Opened bucket: {}", name));
|
||||||
}
|
}
|
||||||
TreeNode::Folder { bucket, path, .. } => {
|
TreeNode::Folder { bucket, path, .. } => {
|
||||||
file_tree.enter_folder(bucket.clone(), path.clone()).await?;
|
file_tree.enter_folder(bucket.clone(), path.clone()).await?;
|
||||||
let mut log_panel = self.log_panel.lock().unwrap();
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
log_panel.add_log(&format!("📂 Opened folder: {}", path));
|
log_panel.add_log(&format!("Opened folder: {}", path));
|
||||||
}
|
}
|
||||||
TreeNode::File { bucket, path, .. } => {
|
TreeNode::File { bucket, path, .. } => {
|
||||||
match Editor::load(app_state, &bucket, &path).await {
|
match Editor::load(app_state, &bucket, &path).await {
|
||||||
|
|
@ -432,11 +571,11 @@ impl XtreeUI {
|
||||||
self.editor = Some(editor);
|
self.editor = Some(editor);
|
||||||
self.active_panel = ActivePanel::Editor;
|
self.active_panel = ActivePanel::Editor;
|
||||||
let mut log_panel = self.log_panel.lock().unwrap();
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
log_panel.add_log(&format!("✏️ Editing: {}", path));
|
log_panel.add_log(&format!("Editing: {}", path));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let mut log_panel = self.log_panel.lock().unwrap();
|
let mut log_panel = self.log_panel.lock().unwrap();
|
||||||
log_panel.add_log(&format!("✗ Failed to load file: {}", e));
|
log_panel.add_log(&format!("Failed to load file: {}", e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -457,6 +596,11 @@ impl XtreeUI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let (Some(chat_panel), Some(file_tree)) = (&mut self.chat_panel, &self.file_tree) {
|
||||||
|
if let Some(bot_name) = file_tree.get_selected_bot() {
|
||||||
|
chat_panel.poll_response(&bot_name).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,15 @@ use std::sync::Arc;
|
||||||
use crate::shared::state::AppState;
|
use crate::shared::state::AppState;
|
||||||
use crate::shared::models::schema::bots::dsl::*;
|
use crate::shared::models::schema::bots::dsl::*;
|
||||||
use crate::nvidia;
|
use crate::nvidia;
|
||||||
|
use crate::config::ConfigManager;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
use sysinfo::System;
|
||||||
|
|
||||||
pub struct StatusPanel {
|
pub struct StatusPanel {
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
last_update: std::time::Instant,
|
last_update: std::time::Instant,
|
||||||
cached_content: String,
|
cached_content: String,
|
||||||
|
system: System,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StatusPanel {
|
impl StatusPanel {
|
||||||
|
|
@ -16,61 +19,40 @@ impl StatusPanel {
|
||||||
app_state,
|
app_state,
|
||||||
last_update: std::time::Instant::now(),
|
last_update: std::time::Instant::now(),
|
||||||
cached_content: String::new(),
|
cached_content: String::new(),
|
||||||
|
system: System::new_all(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update(&mut self) -> Result<(), std::io::Error> {
|
pub async fn update(&mut self) -> Result<(), std::io::Error> {
|
||||||
if self.last_update.elapsed() < std::time::Duration::from_secs(2) {
|
if self.last_update.elapsed() < std::time::Duration::from_secs(1) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.system.refresh_all();
|
||||||
|
|
||||||
|
self.cached_content = String::new();
|
||||||
|
self.last_update = std::time::Instant::now();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&mut self, selected_bot: Option<String>) -> String {
|
||||||
let mut lines = Vec::new();
|
let mut lines = Vec::new();
|
||||||
lines.push("═══════════════════════════════════════".to_string());
|
|
||||||
lines.push(" COMPONENT STATUS".to_string());
|
self.system.refresh_all();
|
||||||
lines.push("═══════════════════════════════════════".to_string());
|
|
||||||
|
lines.push("╔═══════════════════════════════════════╗".to_string());
|
||||||
|
lines.push("║ SYSTEM METRICS ║".to_string());
|
||||||
|
lines.push("╚═══════════════════════════════════════╝".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));
|
|
||||||
|
|
||||||
// Get system metrics
|
|
||||||
let system_metrics = match nvidia::get_system_metrics(0, 0) {
|
let system_metrics = match nvidia::get_system_metrics(0, 0) {
|
||||||
Ok(metrics) => metrics,
|
Ok(metrics) => metrics,
|
||||||
Err(_) => nvidia::SystemMetrics::default(),
|
Err(_) => nvidia::SystemMetrics::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add system metrics with progress bars
|
|
||||||
lines.push("".to_string());
|
|
||||||
lines.push("───────────────────────────────────────".to_string());
|
|
||||||
lines.push(" SYSTEM METRICS".to_string());
|
|
||||||
lines.push("───────────────────────────────────────".to_string());
|
|
||||||
|
|
||||||
// CPU usage with progress bar
|
|
||||||
let cpu_bar = Self::create_progress_bar(system_metrics.cpu_usage, 20);
|
let cpu_bar = Self::create_progress_bar(system_metrics.cpu_usage, 20);
|
||||||
lines.push(format!(" CPU: {:5.1}% {}", system_metrics.cpu_usage, cpu_bar));
|
lines.push(format!(" CPU: {:5.1}% {}", system_metrics.cpu_usage, cpu_bar));
|
||||||
|
|
||||||
// GPU usage with progress bar (if available)
|
|
||||||
if let Some(gpu_usage) = system_metrics.gpu_usage {
|
if let Some(gpu_usage) = system_metrics.gpu_usage {
|
||||||
let gpu_bar = Self::create_progress_bar(gpu_usage, 20);
|
let gpu_bar = Self::create_progress_bar(gpu_usage, 20);
|
||||||
lines.push(format!(" GPU: {:5.1}% {}", gpu_usage, gpu_bar));
|
lines.push(format!(" GPU: {:5.1}% {}", gpu_usage, gpu_bar));
|
||||||
|
|
@ -78,10 +60,39 @@ impl StatusPanel {
|
||||||
lines.push(" GPU: Not available".to_string());
|
lines.push(" GPU: Not available".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let total_mem = self.system.total_memory() as f32 / 1024.0 / 1024.0 / 1024.0;
|
||||||
|
let used_mem = self.system.used_memory() as f32 / 1024.0 / 1024.0 / 1024.0;
|
||||||
|
let mem_percentage = (used_mem / total_mem) * 100.0;
|
||||||
|
let mem_bar = Self::create_progress_bar(mem_percentage, 20);
|
||||||
|
lines.push(format!(" MEM: {:5.1}% {} ({:.1}/{:.1} GB)", mem_percentage, mem_bar, used_mem, total_mem));
|
||||||
|
|
||||||
|
lines.push("".to_string());
|
||||||
|
lines.push("╔═══════════════════════════════════════╗".to_string());
|
||||||
|
lines.push("║ COMPONENTS STATUS ║".to_string());
|
||||||
|
lines.push("╚═══════════════════════════════════════╝".to_string());
|
||||||
|
lines.push("".to_string());
|
||||||
|
|
||||||
|
let components = vec![
|
||||||
|
("Tables", "postgres", "5432"),
|
||||||
|
("Cache", "valkey-server", "6379"),
|
||||||
|
("Drive", "minio", "9000"),
|
||||||
|
("LLM", "llama-server", "8081"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (comp_name, process, port) in components {
|
||||||
|
let status = if Self::check_component_running(process) {
|
||||||
|
format!("🟢 ONLINE [Port: {}]", port)
|
||||||
|
} else {
|
||||||
|
"🔴 OFFLINE".to_string()
|
||||||
|
};
|
||||||
|
lines.push(format!(" {:<10} {}", comp_name, status));
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push("".to_string());
|
||||||
|
lines.push("╔═══════════════════════════════════════╗".to_string());
|
||||||
|
lines.push("║ ACTIVE BOTS ║".to_string());
|
||||||
|
lines.push("╚═══════════════════════════════════════╝".to_string());
|
||||||
lines.push("".to_string());
|
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() {
|
if let Ok(mut conn) = self.app_state.conn.try_lock() {
|
||||||
match bots
|
match bots
|
||||||
|
|
@ -93,8 +104,36 @@ impl StatusPanel {
|
||||||
if bot_list.is_empty() {
|
if bot_list.is_empty() {
|
||||||
lines.push(" No active bots".to_string());
|
lines.push(" No active bots".to_string());
|
||||||
} else {
|
} else {
|
||||||
for (bot_name, _bot_id) in bot_list {
|
for (bot_name, bot_id) in bot_list {
|
||||||
lines.push(format!(" 🤖 {}", bot_name));
|
let marker = if let Some(ref selected) = selected_bot {
|
||||||
|
if selected == &bot_name { "►" } else { " " }
|
||||||
|
} else {
|
||||||
|
" "
|
||||||
|
};
|
||||||
|
lines.push(format!(" {} 🤖 {}", marker, bot_name));
|
||||||
|
|
||||||
|
if let Some(ref selected) = selected_bot {
|
||||||
|
if selected == &bot_name {
|
||||||
|
lines.push("".to_string());
|
||||||
|
lines.push(" ┌─ Bot Configuration ─────────┐".to_string());
|
||||||
|
|
||||||
|
let config_manager = ConfigManager::new(self.app_state.conn.clone());
|
||||||
|
|
||||||
|
let llm_model = config_manager.get_config(&bot_id, "llm-model", None)
|
||||||
|
.unwrap_or_else(|_| "N/A".to_string());
|
||||||
|
lines.push(format!(" Model: {}", llm_model));
|
||||||
|
|
||||||
|
let ctx_size = config_manager.get_config(&bot_id, "llm-server-ctx-size", None)
|
||||||
|
.unwrap_or_else(|_| "N/A".to_string());
|
||||||
|
lines.push(format!(" Context: {}", ctx_size));
|
||||||
|
|
||||||
|
let temp = config_manager.get_config(&bot_id, "llm-temperature", None)
|
||||||
|
.unwrap_or_else(|_| "N/A".to_string());
|
||||||
|
lines.push(format!(" Temp: {}", temp));
|
||||||
|
|
||||||
|
lines.push(" └─────────────────────────────┘".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -107,35 +146,32 @@ impl StatusPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push("".to_string());
|
lines.push("".to_string());
|
||||||
lines.push("───────────────────────────────────────".to_string());
|
lines.push("╔═══════════════════════════════════════╗".to_string());
|
||||||
lines.push(" SESSIONS".to_string());
|
lines.push("║ SESSIONS ║".to_string());
|
||||||
lines.push("───────────────────────────────────────".to_string());
|
lines.push("╚═══════════════════════════════════════╝".to_string());
|
||||||
|
|
||||||
let session_count = self.app_state.response_channels.try_lock()
|
let session_count = self.app_state.response_channels.try_lock()
|
||||||
.map(|channels| channels.len())
|
.map(|channels| channels.len())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
lines.push(format!(" Active: {}", session_count));
|
lines.push(format!(" Active Sessions: {}", session_count));
|
||||||
|
|
||||||
lines.push("".to_string());
|
lines.join("\n")
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a visual progress bar for percentage values
|
|
||||||
fn create_progress_bar(percentage: f32, width: usize) -> String {
|
fn create_progress_bar(percentage: f32, width: usize) -> String {
|
||||||
let filled = (percentage / 100.0 * width as f32).round() as usize;
|
let filled = (percentage / 100.0 * width as f32).round() as usize;
|
||||||
let empty = width.saturating_sub(filled);
|
let empty = width.saturating_sub(filled);
|
||||||
|
|
||||||
let filled_chars = "█".repeat(filled);
|
let filled_chars = "█".repeat(filled);
|
||||||
let empty_chars = "░".repeat(empty);
|
let empty_chars = "░".repeat(empty);
|
||||||
|
|
||||||
format!("[{}{}]", filled_chars, empty_chars)
|
format!("[{}{}]", filled_chars, empty_chars)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn check_component_running(process_name: &str) -> bool {
|
||||||
|
std::process::Command::new("pgrep")
|
||||||
|
.arg("-f")
|
||||||
|
.arg(process_name)
|
||||||
|
.output()
|
||||||
|
.map(|output| !output.stdout.is_empty())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue