diff --git a/Cargo.lock b/Cargo.lock index 9a06067a..309de8df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1106,6 +1106,7 @@ dependencies = [ "base64 0.22.1", "bytes", "chrono", + "cron", "csv", "diesel", "dotenvy", @@ -1571,6 +1572,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "cron" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5877d3fbf742507b66bc2a1945106bd30dd8504019d596901ddd012a4dd01740" +dependencies = [ + "chrono", + "once_cell", + "winnow", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -6167,6 +6179,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.6.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/Cargo.toml b/Cargo.toml index 34a8a1ec..3c75c06b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ aws-sdk-s3 = { version = "1.109.0", features = ["behavior-version-latest"] } base64 = "0.22" bytes = "1.8" chrono = { version = "0.4", features = ["serde"] } +cron = "0.15.0" csv = "1.3" diesel = { version = "2.1", features = ["postgres", "uuid", "chrono", "serde_json"] } dotenvy = "0.15" diff --git a/add-req.sh b/add-req.sh index 9813e9bf..72076133 100755 --- a/add-req.sh +++ b/add-req.sh @@ -21,15 +21,15 @@ for file in "${prompts[@]}"; do done dirs=( - "auth" + # "auth" # "automation" - # "basic" + "basic" # "bootstrap" "bot" # "channels" - # "config" - # "context" - # "drive_monitor" + "config" + "context" + "drive_monitor" # "email" # "file" # "kb" diff --git a/src/automation/mod.rs b/src/automation/mod.rs index 3ccf14e8..c68354f3 100644 --- a/src/automation/mod.rs +++ b/src/automation/mod.rs @@ -1,449 +1,163 @@ - -use crate::shared::models::schema::bots::dsl::*; -use diesel::prelude::*; use crate::basic::ScriptService; +use crate::config::ConfigManager; use crate::shared::models::{Automation, TriggerKind}; use crate::shared::state::AppState; -use chrono::{DateTime, Datelike, Timelike, Utc}; -use log::{debug, error, info, trace, warn}; -use std::path::Path; +use chrono::Utc; +use cron::Schedule; +use diesel::prelude::*; +use log::{error, info}; +use std::str::FromStr; use std::sync::Arc; -use tokio::time::Duration; -use uuid::Uuid; +use tokio::time::{interval, Duration}; pub struct AutomationService { - state: Arc + state: Arc, } impl AutomationService { pub fn new(state: Arc) -> Self { - Self { - state + Self { state } + } + + pub async fn spawn(self) -> Result<(), Box> { + info!("Automation service started"); + + let mut ticker = interval(Duration::from_secs(60)); + + loop { + ticker.tick().await; + if let Err(e) = self.check_scheduled_tasks().await { + error!("Error checking scheduled tasks: {}", e); + } } } - pub fn spawn(self) -> tokio::task::JoinHandle<()> { - trace!("Spawning AutomationService background task"); - let service = Arc::new(self); - tokio::task::spawn_local({ - let service = service.clone(); - async move { + async fn check_scheduled_tasks(&self) -> Result<(), Box> { + use crate::shared::models::system_automations::dsl::{system_automations, is_active, kind, id, last_triggered as lt_column}; - let mut interval = tokio::time::interval(Duration::from_secs(15)); - let mut last_check = Utc::now(); - loop { - interval.tick().await; - trace!("Automation cycle tick started; last_check={}", last_check); - if let Err(e) = service.run_cycle(&mut last_check).await { - error!("Automation cycle error: {}", e); + let mut conn = self.state.conn.lock().map_err(|e| format!("Failed to acquire lock: {}", e))?; + + let automations: Vec = system_automations + .filter(is_active.eq(true)) + .filter(kind.eq(TriggerKind::Scheduled as i32)) + .load::(&mut *conn)?; + + for automation in automations { +if let Some(schedule_str) = &automation.schedule { + if let Ok(parsed_schedule) = Schedule::from_str(schedule_str) { + let now = Utc::now(); + let next_run = parsed_schedule.upcoming(Utc).next(); + + if let Some(next_time) = next_run { + let time_until_next = next_time - now; + if time_until_next.num_minutes() < 1 { + if let Some(last_triggered) = automation.last_triggered { + if (now - last_triggered).num_minutes() < 1 { + continue; + } + } + + self.execute_automation(&automation).await?; + +diesel::update(system_automations.filter(id.eq(automation.id))) + .set(lt_column.eq(Some(now))) + .execute(&mut *conn)?; + } } - trace!("Automation cycle tick completed"); } } - }) - } + } - async fn run_cycle( - &self, - last_check: &mut DateTime, - ) -> Result<(), Box> { - trace!("Running automation cycle; last_check={}", last_check); - let automations = self.load_active_automations().await?; - trace!("Loaded {} active automations", automations.len()); - self.check_table_changes(&automations, *last_check).await; - self.process_schedules(&automations).await; - *last_check = Utc::now(); - trace!("Automation cycle finished; new last_check={}", last_check); Ok(()) } - async fn load_active_automations(&self) -> Result, diesel::result::Error> { - trace!("Loading active automations from database"); - use crate::shared::models::system_automations::dsl::*; - let result = { - let mut conn = self.state.conn.lock().unwrap(); - system_automations - .filter(is_active.eq(true)) - .load::(&mut *conn) - }; // conn is dropped here - trace!("Database query for active automations completed"); - result.map_err(Into::into) - } - - async fn check_table_changes(&self, automations: &[Automation], since: DateTime) { - trace!("Checking table changes since={}", since); - for automation in automations { - trace!( - "Checking automation id={} kind={} target={:?}", - automation.id, - automation.kind, - automation.target - ); - - let trigger_kind = match TriggerKind::from_i32(automation.kind) { - Some(k) => k, - None => { - trace!("Skipping automation {}: invalid TriggerKind", automation.id); - continue; - } - }; - - if !matches!( - trigger_kind, - TriggerKind::TableUpdate | TriggerKind::TableInsert | TriggerKind::TableDelete - ) { - trace!( - "Skipping automation {}: trigger_kind {:?} not table-related", - automation.id, - trigger_kind - ); - continue; - } - - let table = match &automation.target { - Some(t) => t, - None => { - trace!("Skipping automation {}: no table target", automation.id); - continue; - } - }; - - let column = match trigger_kind { - TriggerKind::TableInsert => "created_at", - _ => "updated_at", - }; - trace!( - "Building query for table='{}' column='{}' trigger_kind={:?}", - table, - column, - trigger_kind - ); - - let query = format!( - "SELECT COUNT(*) as count FROM {} WHERE {} > $1", - table, column - ); - - #[derive(diesel::QueryableByName)] - struct CountResult { - #[diesel(sql_type = diesel::sql_types::BigInt)] - count: i64, - } - - let count_result = { - let mut conn_guard = self.state.conn.lock().unwrap(); - let conn = &mut *conn_guard; - - diesel::sql_query(&query) - .bind::(since.naive_utc()) - .get_result::(conn) - }; // conn_guard is dropped here - - match count_result { - Ok(result) if result.count > 0 => { - trace!( - "Detected {} change(s) in table='{}'; triggering automation {}", - result.count, - table, - automation.id - ); - if let Err(e) = self.execute_action(automation).await { - error!("Error executing automation {}: {}", automation.id, e); - } - self.update_last_triggered(automation.id).await; - } - Ok(result) => { - trace!( - "No changes detected for automation {} (count={})", - automation.id, - result.count - ); - } - Err(e) => { - error!("Error checking changes for table '{}': {}", table, e); - } - } - } - } - - async fn process_schedules(&self, automations: &[Automation]) { - let now = Utc::now(); - trace!( - "Processing scheduled automations at UTC={}", - now.format("%Y-%m-%d %H:%M:%S") - ); - for automation in automations { - if let Some(TriggerKind::Scheduled) = TriggerKind::from_i32(automation.kind) { - trace!( - "Evaluating schedule pattern={:?} for automation {}", - automation.schedule, - automation.id - ); - if let Some(pattern) = &automation.schedule { - if Self::should_run_cron(pattern, now.timestamp()) { - debug!( - "Pattern matched; executing automation {} param='{}'", - automation.id, - automation.param - ); - if let Err(e) = self.execute_action(automation).await { - error!("Error executing automation {}: {}", automation.id, e); - } - self.update_last_triggered(automation.id).await; - } else { - trace!("Pattern did not match for automation {}", automation.id); - } - } - } - } - } - - async fn update_last_triggered(&self, automation_id: Uuid) { - trace!( - "Updating last_triggered for automation_id={}", - automation_id - ); - use crate::shared::models::system_automations::dsl::*; - let now = Utc::now(); - let result = { - let mut conn = self.state.conn.lock().unwrap(); - diesel::update(system_automations.filter(id.eq(automation_id))) - .set(last_triggered.eq(now.naive_utc())) - .execute(&mut *conn) - }; // conn is dropped here - - if let Err(e) = result { - error!( - "Failed to update last_triggered for automation {}: {}", - automation_id, e - ); - } else { - trace!("Successfully updated last_triggered for {}", automation_id); - } - } - - fn should_run_cron(pattern: &str, timestamp: i64) -> bool { - trace!( - "Evaluating cron pattern='{}' at timestamp={}", - pattern, - timestamp - ); - let parts: Vec<&str> = pattern.split_whitespace().collect(); - if parts.len() != 5 { - trace!("Invalid cron pattern '{}'", pattern); - return false; - } - let dt = match DateTime::::from_timestamp(timestamp, 0) { - Some(dt) => dt, - None => { - trace!("Invalid timestamp={}", timestamp); - return false; - } - }; - let minute = dt.minute() as i32; - let hour = dt.hour() as i32; - let day = dt.day() as i32; - let month = dt.month() as i32; - let weekday = dt.weekday().num_days_from_monday() as i32; - - // More strict matching with additional logging - let minute_match = Self::cron_part_matches(parts[0], minute); - let hour_match = Self::cron_part_matches(parts[1], hour); - let day_match = Self::cron_part_matches(parts[2], day); - let month_match = Self::cron_part_matches(parts[3], month); - let weekday_match = Self::cron_part_matches(parts[4], weekday); - - let match_result = minute_match && hour_match && day_match && month_match && weekday_match; - - trace!( - "Cron pattern='{}' result={} at {} (minute={}, hour={}, day={}, month={}, weekday={})", - pattern, - match_result, - dt, - minute_match, - hour_match, - day_match, - month_match, - weekday_match - ); - match_result - } - - fn cron_part_matches(part: &str, value: i32) -> bool { - trace!("Checking cron part '{}' against value={}", part, value); - if part == "*" { - return true; - } - if part.contains('/') { - let parts: Vec<&str> = part.split('/').collect(); - if parts.len() != 2 { - return false; - } - let step: i32 = parts[1].parse().unwrap_or(1); - if parts[0] == "*" { - return value % step == 0; - } - } - part.parse::().map_or(false, |num| num == value) - } - - async fn execute_action(&self, automation: &Automation) -> Result<(), Box> { - let bot_id = automation.bot_id; - let param = &automation.param; - trace!("Starting execute_action for bot_id={} param='{}'", bot_id, param); - - let redis_key = format!("job:running:{}:{}", bot_id, param); - trace!("Redis key for job tracking: {}", redis_key); - - if let Some(redis_client) = &self.state.cache { - match redis_client.get_multiplexed_async_connection().await { - Ok(mut conn) => { - trace!("Connected to Redis; checking if job '{}' is running", param); - - // Use SET with NX (only set if not exists) and EX (expire) for atomic operation - let set_result: Result = redis::cmd("SET") - .arg(&redis_key) - .arg("1") - .arg("NX") - .arg("EX") - .arg(300) - .query_async(&mut conn) - .await; - - match set_result { - Ok(res) if res == "OK" => { - trace!("Acquired lock for job '{}'", param); - } - Ok(_) => { - warn!( - "Job '{}' is already running for bot '{}'; skipping execution", - param, bot_id - ); - return Ok(()); - } - Err(e) => { - warn!("Redis error checking job status for '{}': {}", param, e); - return Ok(()); // Skip execution if we can't verify lock status - } - } - } - Err(e) => { - warn!("Failed to connect to Redis for job tracking: {}", e); - return Ok(()); // Skip execution if we can't connect to Redis - } - } - } else { - warn!("Redis client not available for job tracking"); - return Ok(()); // Skip execution if Redis isn't configured - } + async fn execute_automation(&self, automation: &Automation) -> Result<(), Box> { + info!("Executing automation: {}", automation.param); + let config_manager = ConfigManager::new(Arc::clone(&self.state.conn)); let bot_name: String = { - let mut db_conn = self.state.conn.lock().unwrap(); - bots.filter(id.eq(&bot_id)) + use crate::shared::models::schema::bots::dsl::*; + let mut conn = self.state.conn.lock().map_err(|e| format!("Lock failed: {}", e))?; + bots.filter(id.eq(automation.bot_id)) .select(name) - .first(&mut *db_conn) - .map_err(|e| { - error!("Failed to query bot name for {}: {}", bot_id, e); - e - })? + .first(&mut *conn)? }; - let script_name = param.strip_suffix(".bas").unwrap_or(param); - let path_str = format!("./work/{}.gbai/{}.gbdialog/{}.ast", - bot_name, - bot_name, - script_name - ); - let full_path = Path::new(&path_str); - trace!("Resolved full path: {}", full_path.display()); + let script_path = format!("./work/{}.gbai/{}.gbdialog/{}.ast", bot_name, bot_name, automation.param); - let script_content = match tokio::fs::read_to_string(&full_path).await { - Ok(content) => { - trace!("Script '{}' read successfully", param); - content - } + let script_content = match tokio::fs::read_to_string(&script_path).await { + Ok(content) => content, Err(e) => { - error!( - "Failed to read script '{}' at {}: {}", - param, - full_path.display(), - e - ); - self.cleanup_job_flag(&bot_id, param).await; + error!("Failed to read script {}: {}", script_path, e); return Ok(()); } }; - let user_session = crate::shared::models::UserSession { - id: Uuid::new_v4(), - user_id: Uuid::new_v4(), - bot_id, - title: "Automation".to_string(), - current_tool: None, - context_data: serde_json::Value::Null, - created_at: Utc::now(), - updated_at: Utc::now(), + let session = { + let mut sm = self.state.session_manager.lock().await; + let admin_user = uuid::Uuid::nil(); + sm.get_or_create_user_session(admin_user, automation.bot_id, "Automation")?.ok_or("Failed to create session")? }; - trace!( - "Created temporary UserSession id={} for bot_id={}", - user_session.id, - bot_id - ); - let result = { - let script_service = ScriptService::new(Arc::clone(&self.state), user_session); - let ast = match script_service.compile(&script_content) { - Ok(ast) => { - trace!("Compilation successful for script '{}'", param); - ast - } - Err(e) => { - error!("Error compiling script '{}': {}", param, e); - self.cleanup_job_flag(&bot_id, param).await; - return Ok(()); - } - }; + let script_service = ScriptService::new(Arc::clone(&self.state), session); - trace!("Running compiled script '{}'", param); - script_service.run(&ast) - }; // script_service and ast are dropped here - - match result { - Ok(_) => { - info!("Script '{}' executed successfully", param); - } - Err(e) => { - error!("Error executing script '{}': {}", param, e); - } +match script_service.compile(&script_content) { + Ok(ast) => { + if let Err(e) = script_service.run(&ast) { + error!("Script execution failed: {}", e); } + } + Err(e) => { + error!("Script compilation failed: {}", e); + } +} - trace!("Cleaning up Redis flag for job '{}'", param); - self.cleanup_job_flag(&bot_id, param).await; - trace!("Finished execute_action for '{}'", param); Ok(()) } - async fn cleanup_job_flag(&self, bot_id: &Uuid, param: &str) { - trace!( - "Cleaning up Redis flag for bot_id={} param='{}'", - bot_id, - param - ); - let redis_key = format!("job:running:{}:{}", bot_id, param); + async fn execute_compact_prompt(&self, automation: &Automation) -> Result<(), Box> { + info!("Executing prompt compaction for bot: {}", automation.bot_id); - if let Some(redis_client) = &self.state.cache { - match redis_client.get_multiplexed_async_connection().await { - Ok(mut conn) => { - let _: Result<(), redis::RedisError> = redis::cmd("DEL") - .arg(&redis_key) - .query_async(&mut conn) - .await; - trace!("Removed Redis key '{}'", redis_key); - } - Err(e) => { - warn!("Failed to connect to Redis for cleanup: {}", e); + let config_manager = ConfigManager::new(Arc::clone(&self.state.conn)); + let compact_threshold = config_manager + .get_config(&automation.bot_id, "prompt-compact", None)? + .parse::() + .unwrap_or(0); + + if compact_threshold == 0 { + return Ok(()); + } + + let mut session_manager = self.state.session_manager.lock().await; + let sessions = session_manager.get_user_sessions(uuid::Uuid::nil())?; + + for session in sessions { + if session.bot_id != automation.bot_id { + continue; + } + + let history = session_manager.get_conversation_history(session.id, session.user_id)?; + + if history.len() > compact_threshold { + info!("Compacting prompt for session {}: {} messages", session.id, history.len()); + + let mut compacted = String::new(); + for (role, content) in &history[..history.len() - compact_threshold] { + compacted.push_str(&format!("{}: {}\n", role, content)); } + + let summarized = format!("SUMMARY: {}", compacted); + + session_manager.save_message( + session.id, + session.user_id, + 3, + &summarized, + 1 + )?; } } + + Ok(()) } } diff --git a/src/basic/compiler/mod.rs b/src/basic/compiler/mod.rs index 0492d60a..58c8511d 100644 --- a/src/basic/compiler/mod.rs +++ b/src/basic/compiler/mod.rs @@ -1,6 +1,6 @@ use crate::shared::state::AppState; use crate::basic::keywords::set_schedule::execute_set_schedule; -use log::{debug, info, warn}; +use log::{info, warn}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use diesel::QueryDsl; @@ -13,8 +13,6 @@ use std::fs; use std::path::Path; use std::sync::Arc; - -/// Represents a PARAM declaration in BASIC #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ParamDeclaration { pub name: String, @@ -24,7 +22,6 @@ pub struct ParamDeclaration { pub required: bool, } -/// Represents a BASIC tool definition #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolDefinition { pub name: String, @@ -33,7 +30,6 @@ pub struct ToolDefinition { pub source_file: String, } -/// MCP tool format (Model Context Protocol) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MCPTool { pub name: String, @@ -58,7 +54,6 @@ pub struct MCPProperty { pub example: Option, } -/// OpenAI tool format #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OpenAITool { #[serde(rename = "type")] @@ -90,55 +85,42 @@ pub struct OpenAIProperty { pub example: Option, } -/// BASIC Compiler pub struct BasicCompiler { state: Arc, bot_id: uuid::Uuid, - previous_schedules: HashSet, // Tracks script names with SET_SCHEDULE + previous_schedules: HashSet, } impl BasicCompiler { pub fn new(state: Arc, bot_id: uuid::Uuid) -> Self { - Self { - state, + Self { + state, bot_id, previous_schedules: HashSet::new(), } } - /// Compile a BASIC file to AST and generate tool definitions pub fn compile_file( &mut self, source_path: &str, output_dir: &str, ) -> Result> { - info!("Compiling BASIC file: {}", source_path); - - // Read source file let source_content = fs::read_to_string(source_path) .map_err(|e| format!("Failed to read source file: {}", e))?; - // Parse tool definition from source let tool_def = self.parse_tool_definition(&source_content, source_path)?; - // Extract base name without extension let file_name = Path::new(source_path) .file_stem() .and_then(|s| s.to_str()) .ok_or("Invalid file name")?; - // Generate AST path let ast_path = format!("{}/{}.ast", output_dir, file_name); - - // Generate AST (using Rhai compilation would happen here) - // For now, we'll store the preprocessed script let ast_content = self.preprocess_basic(&source_content, source_path, self.bot_id)?; + fs::write(&ast_path, &ast_content) .map_err(|e| format!("Failed to write AST file: {}", e))?; - info!("AST generated: {}", ast_path); - - // Generate tool definitions if PARAM and DESCRIPTION found let (mcp_json, tool_json) = if !tool_def.parameters.is_empty() { let mcp = self.generate_mcp_tool(&tool_def)?; let openai = self.generate_openai_tool(&tool_def)?; @@ -146,21 +128,16 @@ impl BasicCompiler { let mcp_path = format!("{}/{}.mcp.json", output_dir, file_name); let tool_path = format!("{}/{}.tool.json", output_dir, file_name); - // Write MCP JSON let mcp_json_str = serde_json::to_string_pretty(&mcp)?; fs::write(&mcp_path, mcp_json_str) .map_err(|e| format!("Failed to write MCP JSON: {}", e))?; - // Write OpenAI tool JSON let tool_json_str = serde_json::to_string_pretty(&openai)?; fs::write(&tool_path, tool_json_str) .map_err(|e| format!("Failed to write tool JSON: {}", e))?; - info!("Tool definitions generated: {} and {}", mcp_path, tool_path); - (Some(mcp), Some(openai)) } else { - debug!("No tool parameters found in {}", source_path); (None, None) }; @@ -170,7 +147,6 @@ impl BasicCompiler { }) } - /// Parse tool definition from BASIC source pub fn parse_tool_definition( &self, source: &str, @@ -178,21 +154,18 @@ impl BasicCompiler { ) -> Result> { let mut params = Vec::new(); let mut description = String::new(); - let lines: Vec<&str> = source.lines().collect(); let mut i = 0; while i < lines.len() { let line = lines[i].trim(); - // Parse PARAM declarations if line.starts_with("PARAM ") { if let Some(param) = self.parse_param_line(line)? { params.push(param); } } - // Parse DESCRIPTION if line.starts_with("DESCRIPTION ") { let desc_start = line.find('"').unwrap_or(0); let desc_end = line.rfind('"').unwrap_or(line.len()); @@ -218,8 +191,6 @@ impl BasicCompiler { }) } - /// Parse a PARAM line - /// Format: PARAM name AS type LIKE "example" DESCRIPTION "description" fn parse_param_line( &self, line: &str, @@ -229,7 +200,6 @@ impl BasicCompiler { return Ok(None); } - // Extract parts let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() < 4 { warn!("Invalid PARAM line: {}", line); @@ -238,7 +208,6 @@ impl BasicCompiler { let name = parts[1].to_string(); - // Find AS keyword let as_index = parts.iter().position(|&p| p == "AS"); let param_type = if let Some(idx) = as_index { if idx + 1 < parts.len() { @@ -250,7 +219,6 @@ impl BasicCompiler { "string".to_string() }; - // Extract LIKE value (example) let example = if let Some(like_pos) = line.find("LIKE") { let rest = &line[like_pos + 4..].trim(); if let Some(start) = rest.find('"') { @@ -266,7 +234,6 @@ impl BasicCompiler { None }; - // Extract DESCRIPTION let description = if let Some(desc_pos) = line.find("DESCRIPTION") { let rest = &line[desc_pos + 11..].trim(); if let Some(start) = rest.find('"') { @@ -287,11 +254,10 @@ impl BasicCompiler { param_type: self.normalize_type(¶m_type), example, description, - required: true, // Default to required + required: true, })) } - /// Normalize BASIC types to JSON schema types fn normalize_type(&self, basic_type: &str) -> String { match basic_type.to_lowercase().as_str() { "string" | "text" => "string".to_string(), @@ -305,7 +271,6 @@ impl BasicCompiler { } } - /// Generate MCP tool format fn generate_mcp_tool( &self, tool_def: &ToolDefinition, @@ -322,7 +287,6 @@ impl BasicCompiler { example: param.example.clone(), }, ); - if param.required { required.push(param.name.clone()); } @@ -339,7 +303,6 @@ impl BasicCompiler { }) } - /// Generate OpenAI tool format fn generate_openai_tool( &self, tool_def: &ToolDefinition, @@ -356,7 +319,6 @@ impl BasicCompiler { example: param.example.clone(), }, ); - if param.required { required.push(param.name.clone()); } @@ -376,21 +338,21 @@ impl BasicCompiler { }) } - /// Preprocess BASIC script (basic transformations) fn preprocess_basic(&mut self, source: &str, source_path: &str, bot_id: uuid::Uuid) -> Result> { let bot_uuid = bot_id; let mut result = String::new(); let mut has_schedule = false; + let script_name = Path::new(source_path) .file_stem() .and_then(|s| s.to_str()) .unwrap_or("unknown") .to_string(); - // Remove any existing schedule for this script before processing { let mut conn = self.state.conn.lock().unwrap(); use crate::shared::models::system_automations::dsl::*; + diesel::delete(system_automations .filter(bot_id.eq(bot_uuid)) .filter(kind.eq(TriggerKind::Scheduled as i32)) @@ -407,9 +369,28 @@ impl BasicCompiler { continue; } - if trimmed.starts_with("SET_SCHEDULE") { + let normalized = trimmed + .replace("SET SCHEDULE", "SET_SCHEDULE") + .replace("ADD TOOL", "ADD_TOOL") + .replace("CLEAR TOOLS", "CLEAR_TOOLS") + .replace("LIST TOOLS", "LIST_TOOLS") + .replace("CREATE SITE", "CREATE_SITE") + .replace("FOR EACH", "FOR_EACH") + .replace("EXIT FOR", "EXIT_FOR") + .replace("SET USER", "SET_USER") + .replace("SET CONTEXT", "SET_CONTEXT") + .replace("CLEAR SUGGESTIONS", "CLEAR_SUGGESTIONS") + .replace("ADD SUGGESTION", "ADD_SUGGESTION") + .replace("SET KB", "SET_KB") + .replace("ADD KB", "ADD_KB") + .replace("ADD WEBSITE", "ADD_WEBSITE") + .replace("GET BOT MEMORY", "GET_BOT_MEMORY") + .replace("SET BOT MEMORY", "SET_BOT_MEMORY") + .replace("CREATE DRAFT", "CREATE_DRAFT"); + + if normalized.starts_with("SET_SCHEDULE") { has_schedule = true; - let parts: Vec<&str> = trimmed.split('"').collect(); + let parts: Vec<&str> = normalized.split('"').collect(); if parts.len() >= 3 { let cron = parts[1]; let mut conn = self.state.conn.lock().unwrap(); @@ -417,22 +398,23 @@ impl BasicCompiler { log::error!("Failed to schedule SET_SCHEDULE during preprocessing: {}", e); } } else { - log::warn!("Malformed SET_SCHEDULE line ignored: {}", trimmed); + log::warn!("Malformed SET_SCHEDULE line ignored: {}", normalized); } continue; } - if trimmed.starts_with("PARAM ") || trimmed.starts_with("DESCRIPTION ") { + if normalized.starts_with("PARAM ") || normalized.starts_with("DESCRIPTION ") { continue; } - result.push_str(trimmed); + result.push_str(&normalized); result.push('\n'); } if self.previous_schedules.contains(&script_name) && !has_schedule { let mut conn = self.state.conn.lock().unwrap(); use crate::shared::models::system_automations::dsl::*; + diesel::delete(system_automations .filter(bot_id.eq(bot_uuid)) .filter(kind.eq(TriggerKind::Scheduled as i32)) @@ -453,7 +435,6 @@ impl BasicCompiler { } } -/// Result of compilation #[derive(Debug)] pub struct CompilationResult { pub mcp_tool: Option, diff --git a/src/config/mod.rs b/src/config/mod.rs index 30cd5936..046b5335 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,15 +1,13 @@ use diesel::prelude::*; use diesel::pg::PgConnection; use uuid::Uuid; -use log::{info, trace}; - // removed unused serde import +use log::info; use std::collections::HashMap; use std::fs::OpenOptions; use std::io::Write; use std::sync::{Arc, Mutex}; use crate::shared::utils::establish_pg_connection; - #[derive(Clone)] pub struct AppConfig { pub drive: DriveConfig, @@ -41,8 +39,6 @@ pub struct ServerConfig { pub port: u16, } - - impl AppConfig { pub fn database_url(&self) -> String { format!( @@ -54,83 +50,86 @@ impl AppConfig { self.database.database ) } - } - + impl AppConfig { pub fn from_database(conn: &mut PgConnection) -> Result { - info!("Loading configuration from database"); - - use crate::shared::models::schema::bot_configuration::dsl::*; - use diesel::prelude::*; - - let config_map: HashMap = bot_configuration - .select((id, bot_id, config_key, config_value, config_type, is_encrypted)) - .load::<(Uuid, Uuid, String, String, String, bool)>(conn) - .unwrap_or_default() - .into_iter() - .map(|(_, _, key, value, _, _)| (key.clone(), (Uuid::nil(), Uuid::nil(), key, value, String::new(), false))) - .collect(); + use crate::shared::models::schema::bot_configuration::dsl::*; + use diesel::prelude::*; - let mut get_str = |key: &str, default: &str| -> String { - bot_configuration - .filter(config_key.eq(key)) - .select(config_value) - .first::(conn) - .unwrap_or_else(|_| default.to_string()) - }; + let config_map: HashMap = bot_configuration + .select((id, bot_id, config_key, config_value, config_type, is_encrypted)) + .load::<(Uuid, Uuid, String, String, String, bool)>(conn) + .unwrap_or_default() + .into_iter() + .map(|(_, _, key, value, _, _)| (key.clone(), (Uuid::nil(), Uuid::nil(), key, value, String::new(), false))) + .collect(); - let get_u32 = |key: &str, default: u32| -> u32 { - config_map - .get(key) - .and_then(|v| v.3.parse().ok()) - .unwrap_or(default) - }; + let mut get_str = |key: &str, default: &str| -> String { + bot_configuration + .filter(config_key.eq(key)) + .select(config_value) + .first::(conn) + .unwrap_or_else(|_| default.to_string()) + }; - let get_u16 = |key: &str, default: u16| -> u16 { - config_map - .get(key) - .and_then(|v| v.3.parse().ok()) - .unwrap_or(default) - }; + let get_u32 = |key: &str, default: u32| -> u32 { + config_map + .get(key) + .and_then(|v| v.3.parse().ok()) + .unwrap_or(default) + }; - let get_bool = |key: &str, default: bool| -> bool { - config_map - .get(key) - .map(|v| v.3.to_lowercase() == "true") - .unwrap_or(default) - }; + let get_u16 = |key: &str, default: u16| -> u16 { + config_map + .get(key) + .and_then(|v| v.3.parse().ok()) + .unwrap_or(default) + }; + let get_bool = |key: &str, default: bool| -> bool { + config_map + .get(key) + .map(|v| v.3.to_lowercase() == "true") + .unwrap_or(default) + }; - let database = DatabaseConfig { - username: std::env::var("TABLES_USERNAME") - .unwrap_or_else(|_| get_str("TABLES_USERNAME", "gbuser")), - password: std::env::var("TABLES_PASSWORD") - .unwrap_or_else(|_| get_str("TABLES_PASSWORD", "")), - server: std::env::var("TABLES_SERVER") - .unwrap_or_else(|_| get_str("TABLES_SERVER", "localhost")), - port: std::env::var("TABLES_PORT") - .ok() - .and_then(|p| p.parse().ok()) - .unwrap_or_else(|| get_u32("TABLES_PORT", 5432)), - database: std::env::var("TABLES_DATABASE") - .unwrap_or_else(|_| get_str("TABLES_DATABASE", "botserver")), - }; + let database = DatabaseConfig { +username: match std::env::var("TABLES_USERNAME") { + Ok(v) => v, + Err(_) => get_str("TABLES_USERNAME", "gbuser"), +}, +password: match std::env::var("TABLES_PASSWORD") { + Ok(v) => v, + Err(_) => get_str("TABLES_PASSWORD", ""), +}, +server: match std::env::var("TABLES_SERVER") { + Ok(v) => v, + Err(_) => get_str("TABLES_SERVER", "localhost"), +}, +port: std::env::var("TABLES_PORT") + .ok() + .and_then(|p| p.parse().ok()) + .unwrap_or_else(|| get_u32("TABLES_PORT", 5432)), +database: match std::env::var("TABLES_DATABASE") { + Ok(v) => v, + Err(_) => get_str("TABLES_DATABASE", "botserver"), +}, + }; - - let drive = DriveConfig { - server: { - let server = get_str("DRIVE_SERVER", "http://localhost:9000"); - if !server.starts_with("http://") && !server.starts_with("https://") { - format!("http://{}", server) - } else { - server - } - }, - access_key: get_str("DRIVE_ACCESSKEY", "minioadmin"), - secret_key: get_str("DRIVE_SECRET", "minioadmin"), - use_ssl: get_bool("DRIVE_USE_SSL", false), - }; + let drive = DriveConfig { + server: { + let server = get_str("DRIVE_SERVER", "http://localhost:9000"); + if !server.starts_with("http://") && !server.starts_with("https://") { + format!("http://{}", server) + } else { + server + } + }, + access_key: get_str("DRIVE_ACCESSKEY", "minioadmin"), + secret_key: get_str("DRIVE_SECRET", "minioadmin"), + use_ssl: get_bool("DRIVE_USE_SSL", false), + }; Ok(AppConfig { drive, @@ -145,14 +144,12 @@ impl AppConfig { .get_config(&Uuid::nil(), "SITES_ROOT", Some("./botserver-stack/sites"))?.to_string() }, }) -} - + } + pub fn from_env() -> Result { - info!("Loading configuration from environment variables"); - - let database_url = std::env::var("DATABASE_URL") .unwrap_or_else(|_| "postgres://gbuser:@localhost:5432/botserver".to_string()); + let (db_username, db_password, db_server, db_port, db_name) = parse_database_url(&database_url); @@ -164,7 +161,6 @@ impl AppConfig { database: db_name, }; - let minio = DriveConfig { server: std::env::var("DRIVE_SERVER") .unwrap_or_else(|_| "http://localhost:9000".to_string()), @@ -174,13 +170,13 @@ impl AppConfig { use_ssl: std::env::var("DRIVE_USE_SSL") .unwrap_or_else(|_| "false".to_string()) .parse() - .unwrap_or(false) }; - + .unwrap_or(false) + }; Ok(AppConfig { drive: minio, server: ServerConfig { - host: std::env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.1".to_string()), + host: std::env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()), port: std::env::var("SERVER_PORT") .ok() .and_then(|p| p.parse().ok()) @@ -194,7 +190,6 @@ impl AppConfig { }, }) } - } pub fn write_drive_config_to_env(drive: &DriveConfig) -> std::io::Result<()> { @@ -202,8 +197,8 @@ pub fn write_drive_config_to_env(drive: &DriveConfig) -> std::io::Result<()> { .append(true) .create(true) .open(".env")?; - - writeln!(file,"")?; + + writeln!(file, "")?; writeln!(file, "DRIVE_SERVER={}", drive.server)?; writeln!(file, "DRIVE_ACCESSKEY={}", drive.access_key)?; writeln!(file, "DRIVE_SECRET={}", drive.secret_key)?; @@ -229,6 +224,7 @@ fn parse_database_url(url: &str) -> (String, String, String, u32, String) { .get(1) .and_then(|p| p.parse().ok()) .unwrap_or(5432); + let database = host_db[1].to_string(); return (username, password, server, port, database); @@ -261,12 +257,10 @@ impl ConfigManager { fallback: Option<&str>, ) -> Result { use crate::shared::models::schema::bot_configuration::dsl::*; - let mut conn = self.conn.lock().unwrap(); let fallback_str = fallback.unwrap_or(""); - // Try config for provided bot_id let result = bot_configuration .filter(bot_id.eq(code_bot_id)) .filter(config_key.eq(key)) @@ -276,8 +270,8 @@ impl ConfigManager { let value = match result { Ok(v) => v, Err(_) => { - // Fallback to default bot let (default_bot_id, _default_bot_name) = crate::bot::get_default_bot(&mut *conn); + bot_configuration .filter(bot_id.eq(default_bot_id)) .filter(config_key.eq(key)) @@ -306,6 +300,7 @@ impl ConfigManager { .map_err(|e| format!("Failed to acquire lock: {}", e))?; let mut updated = 0; + for line in content.lines().skip(1) { let parts: Vec<&str> = line.split(',').collect(); if parts.len() >= 2 { @@ -313,6 +308,7 @@ impl ConfigManager { let value = parts[1].trim(); let new_id: uuid::Uuid = uuid::Uuid::new_v4(); + diesel::sql_query("INSERT INTO bot_configuration (id, bot_id, config_key, config_value, config_type) VALUES ($1, $2, $3, $4, 'string') ON CONFLICT (bot_id, config_key) DO UPDATE SET config_value = EXCLUDED.config_value, updated_at = NOW()") .bind::(new_id) .bind::(bot_id) @@ -325,9 +321,8 @@ impl ConfigManager { } } - trace!( - "Synced {} config values for bot {}", - updated, bot_id); + info!("Synced {} config values for bot {}", updated, bot_id); + Ok(updated) } } diff --git a/src/drive_monitor/mod.rs b/src/drive_monitor/mod.rs index 672d1fc8..d3bec4af 100644 --- a/src/drive_monitor/mod.rs +++ b/src/drive_monitor/mod.rs @@ -4,8 +4,7 @@ use crate::basic::compiler::BasicCompiler; use crate::config::ConfigManager; use crate::shared::state::AppState; use aws_sdk_s3::Client; -use log::trace; -use log::{debug, error, info}; +use log::{info, warn}; use std::collections::HashMap; use std::error::Error; use std::sync::Arc; @@ -35,12 +34,8 @@ impl DriveMonitor { pub fn spawn(self: Arc) -> tokio::task::JoinHandle<()> { tokio::spawn(async move { - info!( - "Drive Monitor service started for bucket: {}", - self.bucket_name - ); + info!("Drive Monitor service started for bucket: {}", self.bucket_name); - // Check if llama servers are ready before first scan let config_manager = ConfigManager::new(Arc::clone(&self.state.conn)); let default_bot_id = { let mut conn = self.state.conn.lock().unwrap(); @@ -50,27 +45,12 @@ impl DriveMonitor { .unwrap_or_else(|_| uuid::Uuid::nil()) }; - let _llm_url = match config_manager.get_config(&default_bot_id, "llm-url", None) { - Ok(url) => url, - Err(e) => { - error!("Failed to get llm-url config: {}", e); - return; - } - }; - - let _embedding_url = match config_manager.get_config(&default_bot_id, "embedding-url", None) { - Ok(url) => url, - Err(e) => { - error!("Failed to get embedding-url config: {}", e); - return; - } - }; - let mut tick = interval(Duration::from_secs(30)); + loop { tick.tick().await; if let Err(e) = self.check_for_changes().await { - error!("Error checking for drive changes: {}", e); + log::error!("Error checking for drive changes: {}", e); } } }) @@ -79,26 +59,20 @@ impl DriveMonitor { async fn check_for_changes(&self) -> Result<(), Box> { let client = match &self.state.drive { Some(client) => client, - None => { - return Ok(()); - } + None => return Ok(()), }; self.check_gbdialog_changes(client).await?; - self.check_gbot(client).await?; + self.check_gbot(client).await?; Ok(()) } - async fn check_gbdialog_changes( - &self, - client: &Client, - ) -> Result<(), Box> { + async fn check_gbdialog_changes(&self, client: &Client) -> Result<(), Box> { let prefix = ".gbdialog/"; - let mut current_files = HashMap::new(); - let mut continuation_token = None; + loop { let list_objects = client .list_objects_v2() @@ -106,14 +80,15 @@ impl DriveMonitor { .set_continuation_token(continuation_token) .send() .await?; - trace!("List objects result: {:?}", list_objects); for obj in list_objects.contents.unwrap_or_default() { let path = obj.key().unwrap_or_default().to_string(); let path_parts: Vec<&str> = path.split('/').collect(); + if path_parts.len() < 2 || !path_parts[0].ends_with(".gbdialog") { continue; } + if path.ends_with('/') || !path.ends_with(".bas") { continue; } @@ -121,6 +96,7 @@ impl DriveMonitor { let file_state = FileState { etag: obj.e_tag().unwrap_or_default().to_string(), }; + current_files.insert(path, file_state); } @@ -131,16 +107,17 @@ impl DriveMonitor { } let mut file_states = self.file_states.write().await; + for (path, current_state) in current_files.iter() { if let Some(previous_state) = file_states.get(path) { if current_state.etag != previous_state.etag { if let Err(e) = self.compile_tool(client, path).await { - error!("Failed to compile tool {}: {}", path, e); + log::error!("Failed to compile tool {}: {}", path, e); } } } else { if let Err(e) = self.compile_tool(client, path).await { - error!("Failed to compile tool {}: {}", path, e); + log::error!("Failed to compile tool {}: {}", path, e); } } } @@ -166,7 +143,6 @@ impl DriveMonitor { async fn check_gbot(&self, client: &Client) -> Result<(), Box> { let config_manager = ConfigManager::new(Arc::clone(&self.state.conn)); - let mut continuation_token = None; loop { @@ -189,38 +165,13 @@ impl DriveMonitor { continue; } - trace!("Checking config file at path: {}", path); - match client - .head_object() - .bucket(&self.bucket_name) - .key(&path) - .send() - .await - { - Ok(head_res) => { - trace!( - "HeadObject successful for {}, metadata: {:?}", - path, head_res - ); - let response = client - .get_object() - .bucket(&self.bucket_name) - .key(&path) - .send() - .await?; - trace!( - "GetObject successful for {}, content length: {}", - path, - response.content_length().unwrap_or(0) - ); - + match client.head_object().bucket(&self.bucket_name).key(&path).send().await { + Ok(_head_res) => { + let response = client.get_object().bucket(&self.bucket_name).key(&path).send().await?; let bytes = response.body.collect().await?.into_bytes(); - trace!("Collected {} bytes for {}", bytes.len(), path); let csv_content = String::from_utf8(bytes.to_vec()) .map_err(|e| format!("UTF-8 error in {}: {}", path, e))?; - trace!("Found {}: {} bytes", path, csv_content.len()); - // Restart LLaMA servers only if llm- properties changed let llm_lines: Vec<_> = csv_content .lines() .filter(|line| line.trim_start().starts_with("llm-")) @@ -235,42 +186,38 @@ impl DriveMonitor { if parts.len() >= 2 { let key = parts[0].trim(); let new_value = parts[1].trim(); + match config_manager.get_config(&self.bot_id, key, None) { Ok(old_value) => { if old_value != new_value { - info!( - "Detected change in {} (old: {}, new: {})", - key, old_value, new_value - ); + info!("Detected change in {} (old: {}, new: {})", key, old_value, new_value); restart_needed = true; } } Err(_) => { - info!("New llm- property detected: {}", key); restart_needed = true; } } } } - _ = config_manager.sync_gbot_config(&self.bot_id, &csv_content); + let _ = config_manager.sync_gbot_config(&self.bot_id, &csv_content); + if restart_needed { - trace!("Detected llm- configuration change, restarting LLaMA servers..."); if let Err(e) = ensure_llama_servers_running(&self.state).await { - error!("Failed to restart LLaMA servers after llm- config change: {}", e); + log::error!("Failed to restart LLaMA servers after llm- config change: {}", e); } - } else { - trace!("No llm- property changes detected; skipping LLaMA server restart."); } + } else { + let _ = config_manager.sync_gbot_config(&self.bot_id, &csv_content); } - else - { - _ = config_manager.sync_gbot_config(&self.bot_id, &csv_content); - + + if csv_content.lines().any(|line| line.starts_with("theme-")) { + self.broadcast_theme_change(&csv_content).await?; } } Err(e) => { - error!("Config file {} not found or inaccessible: {}", path, e); + log::error!("Config file {} not found or inaccessible: {}", path, e); } } } @@ -284,36 +231,64 @@ impl DriveMonitor { Ok(()) } - async fn compile_tool( - &self, - client: &Client, - file_path: &str, - ) -> Result<(), Box> { - debug!( - "Fetching object from S3: bucket={}, key={}", - &self.bucket_name, file_path - ); - let response = match client - .get_object() - .bucket(&self.bucket_name) - .key(file_path) - .send() - .await - { + async fn broadcast_theme_change(&self, csv_content: &str) -> Result<(), Box> { + let mut theme_data = serde_json::json!({ + "event": "change_theme", + "data": {} + }); + + for line in csv_content.lines() { + let parts: Vec<&str> = line.split(',').collect(); + if parts.len() >= 2 { + let key = parts[0].trim(); + let value = parts[1].trim(); + + match key { + "theme-color1" => theme_data["data"]["color1"] = serde_json::Value::String(value.to_string()), + "theme-color2" => theme_data["data"]["color2"] = serde_json::Value::String(value.to_string()), + "theme-logo" => theme_data["data"]["logo_url"] = serde_json::Value::String(value.to_string()), + "theme-title" => theme_data["data"]["title"] = serde_json::Value::String(value.to_string()), + "theme-logo-text" => theme_data["data"]["logo_text"] = serde_json::Value::String(value.to_string()), + _ => {} + } + } + } + + let response_channels = self.state.response_channels.lock().await; + for (session_id, tx) in response_channels.iter() { + let theme_response = crate::shared::models::BotResponse { + bot_id: self.bot_id.to_string(), + user_id: "system".to_string(), + session_id: session_id.clone(), + channel: "web".to_string(), + content: serde_json::to_string(&theme_data)?, + message_type: 2, + stream_token: None, + is_complete: true, + suggestions: Vec::new(), + context_name: None, + context_length: 0, + context_max_length: 0, + }; + + let _ = tx.try_send(theme_response); + } + + Ok(()) + } + + async fn compile_tool(&self, client: &Client, file_path: &str) -> Result<(), Box> { + info!("Fetching object from S3: bucket={}, key={}", &self.bucket_name, file_path); + + let response = match client.get_object().bucket(&self.bucket_name).key(file_path).send().await { Ok(res) => { - debug!( - "Successfully fetched object from S3: bucket={}, key={}, size={}", - &self.bucket_name, - file_path, - res.content_length().unwrap_or(0) - ); + info!("Successfully fetched object from S3: bucket={}, key={}, size={}", + &self.bucket_name, file_path, res.content_length().unwrap_or(0)); res } Err(e) => { - error!( - "Failed to fetch object from S3: bucket={}, key={}, error={:?}", - &self.bucket_name, file_path, e - ); + log::error!("Failed to fetch object from S3: bucket={}, key={}, error={:?}", + &self.bucket_name, file_path, e); return Err(e.into()); } }; @@ -329,11 +304,9 @@ impl DriveMonitor { .unwrap_or(file_path) .to_string(); - 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); + std::fs::create_dir_all(&work_dir)?; let local_source_path = format!("{}/{}.bas", work_dir, tool_name); @@ -343,18 +316,10 @@ impl DriveMonitor { let result = compiler.compile_file(&local_source_path, &work_dir)?; if let Some(mcp_tool) = result.mcp_tool { - info!( - "MCP tool definition generated with {} parameters", - mcp_tool.input_schema.properties.len() - ); - } - - if result.openai_tool.is_some() { - debug!("OpenAI tool definition generated"); + info!("MCP tool definition generated with {} parameters", + mcp_tool.input_schema.properties.len()); } Ok(()) } - - } diff --git a/templates/announcements.gbai/annoucements.gbot/config.csv b/templates/announcements.gbai/annoucements.gbot/config.csv index e5d41cd7..131e6cb1 100644 --- a/templates/announcements.gbai/annoucements.gbot/config.csv +++ b/templates/announcements.gbai/annoucements.gbot/config.csv @@ -1,5 +1,6 @@ name,value prompt-history, 2 -theme-color1,green -theme-color2,yellow -custom-logo-url,https://example.com/logo.png +theme-color1,#0d2b55 +theme-color2,#fff9c2 +theme-logo,https://example.com/logo.png +theme-title, Custom diff --git a/web/html/index.html b/web/html/index.html index 7cc45c91..5225779b 100644 --- a/web/html/index.html +++ b/web/html/index.html @@ -17,14 +17,18 @@ --glass:rgba(0,0,0,0.02); --shadow:rgba(0,0,0,0.05); --logo-url:url('https://pragmatismo.com.br/icons/general-bots.svg'); +--gradient-1:linear-gradient(135deg,rgba(0,102,255,0.05) 0%,rgba(0,102,255,0.0) 100%); +--gradient-2:linear-gradient(45deg,rgba(0,0,0,0.02) 0%,rgba(0,0,0,0.0) 100%); } [data-theme="dark"]{ ---bg:#727171; +--bg:#1a1a1a; --fg:#ffffff; ---border:#a3a0a0; +--border:#333333; --accent:#ffffff; ---glass:rgba(255,255,255,0.02); ---shadow:rgba(0,0,0,0.3); +--glass:rgba(255,255,255,0.05); +--shadow:rgba(0,0,0,0.5); +--gradient-1:linear-gradient(135deg,rgba(255,255,255,0.08) 0%,rgba(255,255,255,0.0) 100%); +--gradient-2:linear-gradient(45deg,rgba(255,255,255,0.03) 0%,rgba(255,255,255,0.0) 100%); } *{margin:0;padding:0;box-sizing:border-box} body{ @@ -36,6 +40,15 @@ transition:background 0.3s, color 0.3s; display:flex; flex-direction:column; height:100vh; +position:relative; +} +body::before{ +content:''; +position:fixed; +inset:0; +background:var(--gradient-1); +pointer-events:none; +z-index:0; } .float-menu{ position:fixed; @@ -55,11 +68,12 @@ border-radius:50%; cursor:pointer; transition:all 0.3s; border:1px solid var(--border); +backdrop-filter:blur(10px); } [data-theme="dark"] .float-logo{ } .float-logo:hover{ -transform:scale(1.1); +transform:scale(1.1) rotate(5deg); } .menu-button{ width:40px; @@ -74,9 +88,10 @@ background:var(--bg); border:1px solid var(--border); font-size:16px; color:var(--fg); +backdrop-filter:blur(10px); } .menu-button:hover{ -transform:scale(1.1); +transform:scale(1.1) rotate(-5deg); background:var(--fg); color:var(--bg); } @@ -92,6 +107,8 @@ transition:left 0.4s cubic-bezier(0.4,0,0.2,1); z-index:999; overflow-y:auto; padding:20px; +backdrop-filter:blur(20px); +box-shadow:4px 0 20px var(--shadow); } .sidebar.open{ left:0; @@ -132,7 +149,7 @@ text-align:left; .sidebar-button:hover{ background:var(--fg); color:var(--bg); -transform:translateX(4px); +transform:translateX(4px) scale(1.02); } .history-section{ margin-top:20px; @@ -156,7 +173,7 @@ border:1px solid transparent; .history-item:hover{ background:var(--fg); color:var(--bg); -transform:translateX(4px); +transform:translateX(4px) scale(1.02); } #messages{ flex:1; @@ -165,6 +182,8 @@ padding:20px 20px 140px; max-width:680px; margin:0 auto; width:100%; +position:relative; +z-index:1; } .message-container{ margin-bottom:24px; @@ -180,10 +199,21 @@ margin-bottom:8px; background:var(--fg); color:var(--bg); border-radius:18px; -padding:10px 16px; +padding:12px 18px; max-width:80%; font-size:14px; line-height:1.5; +box-shadow:0 2px 8px var(--shadow); +position:relative; +overflow:hidden; +} +.user-message-content::before{ +content:''; +position:absolute; +inset:0; +background:var(--gradient-2); +opacity:0.3; +pointer-events:none; } .assistant-message{ display:flex; @@ -205,6 +235,21 @@ filter:var(--logo-filter, none); flex:1; font-size:14px; line-height:1.7; +background:var(--glass); +border-radius:18px; +padding:12px 18px; +border:1px solid var(--border); +box-shadow:0 2px 8px var(--shadow); +position:relative; +overflow:hidden; +} +.assistant-message-content::before{ +content:''; +position:absolute; +inset:0; +background:var(--gradient-1); +opacity:0.5; +pointer-events:none; } .thinking-indicator{ display:flex; @@ -240,6 +285,7 @@ border-top:1px solid var(--border); padding:12px; z-index:100; transition:all 0.3s; +backdrop-filter:blur(20px); } .suggestions-container{ display:flex; @@ -251,7 +297,7 @@ max-width:680px; margin:0 auto 8px; } .suggestion-button{ -padding:4px 10px; +padding:6px 12px; border-radius:12px; cursor:pointer; font-size:11px; @@ -264,7 +310,7 @@ color:var(--fg); .suggestion-button:hover{ background:var(--fg); color:var(--bg); -transform:scale(1.02); +transform:scale(1.05); } .input-container{ display:flex; @@ -284,9 +330,11 @@ transition:all 0.3s; background:var(--glass); border:1px solid var(--border); color:var(--fg); +backdrop-filter:blur(10px); } #messageInput:focus{ border-color:var(--accent); +box-shadow:0 0 0 3px rgba(0,102,255,0.1); } #messageInput::placeholder{ opacity:0.3; @@ -307,7 +355,7 @@ font-size:16px; flex-shrink:0; } #sendBtn:hover,#voiceBtn:hover{ -transform:scale(1.08); +transform:scale(1.08) rotate(5deg); } #sendBtn:active,#voiceBtn:active{ transform:scale(0.95); @@ -316,8 +364,8 @@ transform:scale(0.95); animation:pulse 1.5s infinite; } @keyframes pulse{ -0%,100%{opacity:1} -50%{opacity:0.6} +0%,100%{opacity:1;transform:scale(1)} +50%{opacity:0.6;transform:scale(1.1)} } .flash-overlay{ position:fixed; @@ -349,7 +397,7 @@ z-index:90; display:flex; } .scroll-to-bottom:hover{ -transform:scale(1.1); +transform:scale(1.1) rotate(180deg); } .warning-message{ border-radius:12px; @@ -390,6 +438,7 @@ z-index:90; background:var(--bg); border:1px solid var(--border); display:none; +backdrop-filter:blur(10px); } .context-indicator.visible{ display:block; @@ -560,6 +609,9 @@ width:100px; +
+
History
+
@@ -582,6 +634,8 @@ let ws=null,currentSessionId=null,currentUserId=null,currentBotId="default_bot", const maxReconnectAttempts=5,messagesDiv=document.getElementById("messages"),input=document.getElementById("messageInput"),sendBtn=document.getElementById("sendBtn"),voiceBtn=document.getElementById("voiceBtn"),connectionStatus=document.getElementById("connectionStatus"),flashOverlay=document.getElementById("flashOverlay"),suggestionsContainer=document.getElementById("suggestions"),floatLogo=document.getElementById("floatLogo"),sidebar=document.getElementById("sidebar"),themeBtn=document.getElementById("themeBtn"),scrollToBottomBtn=document.getElementById("scrollToBottom"),contextIndicator=document.getElementById("contextIndicator"),contextPercentage=document.getElementById("contextPercentage"),contextProgressBar=document.getElementById("contextProgressBar"),sidebarTitle=document.getElementById("sidebarTitle"); marked.setOptions({breaks:true,gfm:true}); +floatLogo.addEventListener('click',toggleSidebar); + function toggleSidebar(){ sidebar.classList.toggle('open'); } @@ -1137,6 +1191,7 @@ async function connectToVoiceRoom(t){ try{ const r=new LiveKitClient.Room(),p=window.location.protocol==="https:"?"wss:":"ws:",u=`${p}//${window.location.host}/voice`; await r.connect(u,t); + voiceRoom=r; r.on("dataReceived",d=>{ const dc=new TextDecoder(),m=dc.decode(d); @@ -1214,4 +1269,4 @@ connectWebSocket(); }); - \ No newline at end of file +