From 1d7d0e10c0e3b2effa245e0dca91aceafea96b3f Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Sun, 2 Nov 2025 20:57:53 -0300 Subject: [PATCH] feat(automation): add unique constraint and refactor action execution - Added UNIQUE constraint on system_automations (bot_id, kind, param) to prevent duplicate automations - Refactored execute_action to accept full Automation struct instead of just param - Simplified bot name resolution by using automation.bot_id directly - Improved error handling in action execution with proper error propagation - Removed redundant bot name lookup logic by leveraging existing bot_id --- migrations/6.0.10.sql | 15 + migrations/6.0.9.sql | 5 + src/automation/mod.rs | 54 ++- src/basic/compiler/mod.rs | 40 +- src/basic/keywords/get.rs | 40 +- src/basic/keywords/set_schedule.rs | 30 +- src/basic/mod.rs | 2 - src/drive_monitor/mod.rs | 2 +- web/html/index.html | 597 +++++++++-------------------- 9 files changed, 284 insertions(+), 501 deletions(-) create mode 100644 migrations/6.0.10.sql diff --git a/migrations/6.0.10.sql b/migrations/6.0.10.sql new file mode 100644 index 00000000..57757254 --- /dev/null +++ b/migrations/6.0.10.sql @@ -0,0 +1,15 @@ +-- Migration 6.0.10: Add unique constraint for system_automations upsert +-- Description: Creates a unique constraint matching the ON CONFLICT target in set_schedule.rs + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'system_automations_bot_kind_param_unique' + ) THEN + ALTER TABLE public.system_automations + ADD CONSTRAINT system_automations_bot_kind_param_unique + UNIQUE (bot_id, kind, param); + END IF; +END +$$; diff --git a/migrations/6.0.9.sql b/migrations/6.0.9.sql index 38d646c9..acdd9ee5 100644 --- a/migrations/6.0.9.sql +++ b/migrations/6.0.9.sql @@ -9,3 +9,8 @@ ADD COLUMN IF NOT EXISTS bot_id UUID NOT NULL; -- Create an index on bot_id for faster lookups CREATE INDEX IF NOT EXISTS idx_system_automations_bot_id ON public.system_automations (bot_id); + + +ALTER TABLE public.system_automations +ADD CONSTRAINT IF NOT EXISTS system_automations_bot_kind_param_unique +UNIQUE (bot_id, kind, param); diff --git a/src/automation/mod.rs b/src/automation/mod.rs index d9b8cbd1..c1d8f33d 100644 --- a/src/automation/mod.rs +++ b/src/automation/mod.rs @@ -1,8 +1,9 @@ +use crate::shared::models::schema::bots::dsl::*; +use diesel::prelude::*; use crate::basic::ScriptService; use crate::shared::models::{Automation, TriggerKind}; use crate::shared::state::AppState; use chrono::{DateTime, Datelike, Timelike, Utc}; -use diesel::prelude::*; use log::{debug, error, info, trace, warn}; use std::path::Path; use std::sync::Arc; @@ -150,7 +151,9 @@ impl AutomationService { table, automation.id ); - self.execute_action(&automation.param).await; + 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) => { @@ -187,7 +190,9 @@ impl AutomationService { automation.id, automation.param ); - self.execute_action(&automation.param).await; + 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); @@ -275,10 +280,10 @@ impl AutomationService { part.parse::().map_or(false, |num| num == value) } - async fn execute_action(&self, param: &str) { - trace!("Starting execute_action with param='{}'", param); - let (bot_id, _) = crate::bot::get_default_bot(&mut self.state.conn.lock().unwrap()); - trace!("Resolved bot_id={} for param='{}'", bot_id, param); + 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); @@ -297,7 +302,6 @@ impl AutomationService { "Job '{}' is already running for bot '{}'; skipping execution", param, bot_id ); - return; } let _: Result<(), redis::RedisError> = redis::cmd("SETEX") @@ -314,26 +318,15 @@ impl AutomationService { } } - // Get bot name from database - let bot_name = { - use crate::shared::models::bots; - let mut conn = self.state.conn.lock().unwrap(); - match bots::table - .filter(bots::id.eq(bot_id)) - .select(bots::name) - .first::(&mut *conn) - .optional() - { - Ok(Some(name)) => name, - Ok(None) => { - warn!("No bot found with id {}, using default name", bot_id); - crate::bot::get_default_bot(&mut self.state.conn.lock().unwrap()).1 - } - Err(e) => { - error!("Failed to query bot name: {}", e); - crate::bot::get_default_bot(&mut self.state.conn.lock().unwrap()).1 - } - } + let bot_name: String = { + let mut db_conn = self.state.conn.lock().unwrap(); + bots.filter(id.eq(&bot_id)) + .select(name) + .first(&mut *db_conn) + .map_err(|e| { + error!("Failed to query bot name for {}: {}", bot_id, e); + e + })? }; let script_name = param.strip_suffix(".bas").unwrap_or(param); @@ -358,7 +351,7 @@ impl AutomationService { e ); self.cleanup_job_flag(&bot_id, param).await; - return; + return Ok(()); } }; @@ -389,7 +382,7 @@ impl AutomationService { Err(e) => { error!("Error compiling script '{}': {}", param, e); self.cleanup_job_flag(&bot_id, param).await; - return; + return Ok(()); } }; @@ -409,6 +402,7 @@ impl AutomationService { 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) { diff --git a/src/basic/compiler/mod.rs b/src/basic/compiler/mod.rs index a34db195..02ab5434 100644 --- a/src/basic/compiler/mod.rs +++ b/src/basic/compiler/mod.rs @@ -1,4 +1,5 @@ use crate::shared::state::AppState; +use crate::basic::keywords::set_schedule::execute_set_schedule; use log::{debug, info, warn}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -88,11 +89,12 @@ pub struct OpenAIProperty { /// BASIC Compiler pub struct BasicCompiler { state: Arc, + bot_id: uuid::Uuid, } impl BasicCompiler { - pub fn new(state: Arc) -> Self { - Self { state } + pub fn new(state: Arc, bot_id: uuid::Uuid) -> Self { + Self { state, bot_id } } /// Compile a BASIC file to AST and generate tool definitions @@ -121,7 +123,7 @@ impl BasicCompiler { // Generate AST (using Rhai compilation would happen here) // For now, we'll store the preprocessed script - let ast_content = self.preprocess_basic(&source_content)?; + 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))?; @@ -368,7 +370,7 @@ impl BasicCompiler { } /// Preprocess BASIC script (basic transformations) - fn preprocess_basic(&self, source: &str) -> Result> { + fn preprocess_basic(&self, source: &str, source_path: &str, bot_id: uuid::Uuid) -> Result> { let mut result = String::new(); for line in source.lines() { @@ -379,6 +381,32 @@ impl BasicCompiler { continue; } + // Handle SET_SCHEDULE keyword during preprocessing + if trimmed.starts_with("SET_SCHEDULE") { + // Expected format: SET_SCHEDULE "cron_expression" + // Extract the quoted cron expression + let parts: Vec<&str> = trimmed.split('"').collect(); + if parts.len() >= 3 { + let cron = parts[1]; + + // Get the current script's name (file stem) + use std::path::Path; + let script_name = Path::new(source_path) + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("unknown") + .to_string(); + + let mut conn = self.state.conn.lock().unwrap(); + if let Err(e) = execute_set_schedule(&mut *conn, cron, &script_name, bot_id) { + log::error!("Failed to schedule SET_SCHEDULE during preprocessing: {}", e); + } + } else { + log::warn!("Malformed SET_SCHEDULE line ignored: {}", trimmed); + } + continue; + } + // Skip PARAM and DESCRIPTION lines (metadata) if trimmed.starts_with("PARAM ") || trimmed.starts_with("DESCRIPTION ") { continue; @@ -407,7 +435,7 @@ mod tests { #[test] fn test_normalize_type() { - let compiler = BasicCompiler::new(Arc::new(AppState::default())); + let compiler = BasicCompiler::new(Arc::new(AppState::default()), uuid::Uuid::nil()); assert_eq!(compiler.normalize_type("string"), "string"); assert_eq!(compiler.normalize_type("integer"), "integer"); @@ -418,7 +446,7 @@ mod tests { #[test] fn test_parse_param_line() { - let compiler = BasicCompiler::new(Arc::new(AppState::default())); + let compiler = BasicCompiler::new(Arc::new(AppState::default()), uuid::Uuid::nil()); let line = r#"PARAM name AS string LIKE "John Doe" DESCRIPTION "User's full name""#; let result = compiler.parse_param_line(line).unwrap(); diff --git a/src/basic/keywords/get.rs b/src/basic/keywords/get.rs index cb15029b..e25e2207 100644 --- a/src/basic/keywords/get.rs +++ b/src/basic/keywords/get.rs @@ -1,15 +1,17 @@ +use crate::shared::models::schema::bots::dsl::*; +use diesel::prelude::*; +use crate::kb::minio_handler; use crate::shared::models::UserSession; use crate::shared::state::AppState; -use log::{debug, error, info}; +use log::{debug, error, info, trace}; use reqwest::{self, Client}; -use crate::kb::minio_handler; use rhai::{Dynamic, Engine}; use std::error::Error; use std::path::Path; use std::sync::Arc; use std::time::Duration; -pub fn get_keyword(state: Arc, _user: UserSession, engine: &mut Engine) { +pub fn get_keyword(state: Arc, user_session: UserSession, engine: &mut Engine) { let state_clone = Arc::clone(&state); engine @@ -45,7 +47,9 @@ pub fn get_keyword(state: Arc, _user: UserSession, engine: &mut Engine execute_get(&url_for_blocking).await } else { info!("Local file GET request from bucket: {}", url_for_blocking); - get_from_bucket(&state_for_blocking, &url_for_blocking).await + get_from_bucket(&state_for_blocking, &url_for_blocking, + user_session.bot_id) + .await } }); tx.send(result).err() @@ -151,6 +155,7 @@ pub async fn execute_get(url: &str) -> Result Result> { debug!("Getting file from bucket: {}", file_path); @@ -160,26 +165,37 @@ pub async fn get_from_bucket( } let client = state.drive.as_ref().ok_or("S3 client not configured")?; + let bot_name: String = { + let mut db_conn = state.conn.lock().unwrap(); + bots.filter(id.eq(&bot_id)) + .select(name) + .first(&mut *db_conn) + .map_err(|e| { + error!("Failed to query bot name for {}: {}", bot_id, e); + e + })? + }; let bucket_name = { - - let bucket = format!("default.gbai"); - debug!("Resolved bucket name: {}", bucket); + let bucket = format!("{}.gbai", bot_name); + trace!("Resolved GET bucket name: {}", bucket); bucket }; let bytes = match tokio::time::timeout( Duration::from_secs(30), - minio_handler::get_file_content(client, &bucket_name, file_path) - ).await { + minio_handler::get_file_content(client, &bucket_name, file_path), + ) + .await + { Ok(Ok(data)) => data, Ok(Err(e)) => { - error!("S3 read failed: {}", e); + error!("drive read failed: {}", e); return Err(format!("S3 operation failed: {}", e).into()); } Err(_) => { - error!("S3 read timed out"); - return Err("S3 operation timed out".into()); + error!("drive read timed out"); + return Err("drive operation timed out".into()); } }; diff --git a/src/basic/keywords/set_schedule.rs b/src/basic/keywords/set_schedule.rs index fa70f9ed..8e7678fb 100644 --- a/src/basic/keywords/set_schedule.rs +++ b/src/basic/keywords/set_schedule.rs @@ -1,36 +1,10 @@ use diesel::prelude::*; use log::info; -use rhai::Dynamic; -use rhai::Engine; use serde_json::{json, Value}; use uuid::Uuid; use crate::shared::models::TriggerKind; -use crate::shared::models::UserSession; -use crate::shared::state::AppState; -pub fn set_schedule_keyword(state: &AppState, user: UserSession, engine: &mut Engine) { - let state_clone = state.clone(); - - engine - .register_custom_syntax(&["SET_SCHEDULE", "$string$", "$string$"], true, { - move |context, inputs| { - let cron = context.eval_expression_tree(&inputs[0])?.to_string(); - let script_name = context.eval_expression_tree(&inputs[1])?.to_string(); - - let mut conn = state_clone.conn.lock().unwrap(); - let result = execute_set_schedule(&mut *conn, &cron, &script_name, user.bot_id) - .map_err(|e| format!("DB error: {}", e))?; - - if let Some(rows_affected) = result.get("rows_affected") { - Ok(Dynamic::from(rows_affected.as_i64().unwrap_or(0))) - } else { - Err("No rows affected".into()) - } - } - }) - .unwrap(); -} pub fn execute_set_schedule( conn: &mut diesel::PgConnection, @@ -39,7 +13,7 @@ pub fn execute_set_schedule( bot_uuid: Uuid, ) -> Result> { info!( - "Starting execute_set_schedule with cron: {}, script: {}, bot_id: {:?}", + "Scheduling SET SCHEDULE cron: {}, script: {}, bot_id: {:?}", cron, script_name, bot_uuid ); @@ -55,7 +29,7 @@ pub fn execute_set_schedule( let result = diesel::insert_into(system_automations) .values(&new_automation) - .on_conflict((bot_id, param)) + .on_conflict((bot_id, kind, param)) .do_update() .set(( schedule.eq(cron), diff --git a/src/basic/mod.rs b/src/basic/mod.rs index d426825a..bca7fac4 100644 --- a/src/basic/mod.rs +++ b/src/basic/mod.rs @@ -28,7 +28,6 @@ use self::keywords::print::print_keyword; use self::keywords::remove_tool::remove_tool_keyword; use self::keywords::set::set_keyword; use self::keywords::set_kb::{add_kb_keyword, set_kb_keyword}; -use self::keywords::set_schedule::set_schedule_keyword; use self::keywords::wait::wait_keyword; use self::keywords::add_suggestion::add_suggestion_keyword; @@ -68,7 +67,6 @@ impl ScriptService { wait_keyword(&state, user.clone(), &mut engine); print_keyword(&state, user.clone(), &mut engine); on_keyword(&state, user.clone(), &mut engine); - set_schedule_keyword(&state, user.clone(), &mut engine); hear_keyword(state.clone(), user.clone(), &mut engine); talk_keyword(state.clone(), user.clone(), &mut engine); set_context_keyword(state.clone(), user.clone(), &mut engine); diff --git a/src/drive_monitor/mod.rs b/src/drive_monitor/mod.rs index fcaffb14..44c1559b 100644 --- a/src/drive_monitor/mod.rs +++ b/src/drive_monitor/mod.rs @@ -404,7 +404,7 @@ impl DriveMonitor { let local_source_path = format!("{}/{}.bas", work_dir, tool_name); std::fs::write(&local_source_path, &source_content)?; - let compiler = BasicCompiler::new(Arc::clone(&self.state)); + let compiler = BasicCompiler::new(Arc::clone(&self.state), self.bot_id); let result = compiler.compile_file(&local_source_path, &work_dir)?; if let Some(mcp_tool) = result.mcp_tool { diff --git a/web/html/index.html b/web/html/index.html index 0e9ac2ac..17025229 100644 --- a/web/html/index.html +++ b/web/html/index.html @@ -12,25 +12,27 @@ @import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap"); :root { - --primary: #00f0ff; - --primary-glow: rgba(0, 240, 255, 0.5); - --secondary: #ff00e5; - --accent: #ffd700; - --bg-dark: #0a0a0f; - --bg-card: rgba(15, 15, 25, 0.8); + --primary: #ffffff; + --primary-glow: rgba(255, 255, 255, 0.2); + --secondary: #e0e0e0; + --accent: #ffffff; + --bg-dark: #000000; + --bg-card: rgba(20, 20, 20, 0.8); --text-primary: #ffffff; - --text-secondary: #a0a0b0; + --text-secondary: #888888; + --border-color: rgba(255, 255, 255, 0.1); } [data-theme="light"] { - --primary: #0066cc; - --primary-glow: rgba(0, 102, 204, 0.3); - --secondary: #cc00cc; - --accent: #ff9900; - --bg-dark: #f5f7fa; - --bg-card: rgba(255, 255, 255, 0.9); - --text-primary: #1a1a1a; + --primary: #000000; + --primary-glow: rgba(0, 0, 0, 0.1); + --secondary: #333333; + --accent: #000000; + --bg-dark: #ffffff; + --bg-card: rgba(250, 250, 250, 0.9); + --text-primary: #000000; --text-secondary: #666666; + --border-color: rgba(0, 0, 0, 0.1); } * { @@ -41,17 +43,13 @@ body { font-family: "Space Grotesk", sans-serif; - background: radial-gradient(ellipse at top, #0f0f1e 0%, #0a0a0f 100%); + background: var(--bg-dark); color: var(--text-primary); overflow: hidden; position: relative; transition: background 0.3s ease, color 0.3s ease; } - [data-theme="light"] body { - background: radial-gradient(ellipse at top, #e6e9f0 0%, #f5f7fa 100%); - } - body::before { content: ''; position: fixed; @@ -59,17 +57,13 @@ left: 0; width: 100%; height: 100%; - background: - radial-gradient(circle at 20% 50%, rgba(0, 240, 255, 0.1) 0%, transparent 50%), - radial-gradient(circle at 80% 80%, rgba(255, 0, 229, 0.1) 0%, transparent 50%); + background: radial-gradient(circle at 30% 40%, rgba(255, 255, 255, 0.03) 0%, transparent 50%); pointer-events: none; z-index: 0; } [data-theme="light"] body::before { - background: - radial-gradient(circle at 20% 50%, rgba(0, 102, 204, 0.1) 0%, transparent 50%), - radial-gradient(circle at 80% 80%, rgba(204, 0, 204, 0.1) 0%, transparent 50%); + background: radial-gradient(circle at 30% 40%, rgba(0, 0, 0, 0.02) 0%, transparent 50%); } .grain-overlay { @@ -94,20 +88,18 @@ top: 0; width: 320px; height: 100vh; - background: linear-gradient(135deg, rgba(15, 15, 25, 0.98) 0%, rgba(10, 10, 15, 0.98) 100%); + background: var(--bg-card); backdrop-filter: blur(40px); - border-right: 1px solid rgba(0, 240, 255, 0.2); + border-right: 1px solid var(--border-color); transition: left 0.4s cubic-bezier(0.4, 0, 0.2, 1); z-index: 100; overflow-y: auto; padding: 24px; - box-shadow: 10px 0 50px rgba(0, 0, 0, 0.5); + box-shadow: 10px 0 50px rgba(0, 0, 0, 0.3); } [data-theme="light"] .sidebar { - background: linear-gradient(135deg, rgba(255, 255, 255, 0.98) 0%, rgba(245, 247, 250, 0.98) 100%); - border-right: 1px solid rgba(0, 102, 204, 0.2); - box-shadow: 10px 0 50px rgba(0, 0, 0, 0.1); + box-shadow: 10px 0 30px rgba(0, 0, 0, 0.05); } .sidebar.open { @@ -119,8 +111,8 @@ left: 24px; top: 24px; z-index: 101; - background: rgba(0, 240, 255, 0.1); - border: 1px solid rgba(0, 240, 255, 0.3); + background: var(--bg-card); + border: 1px solid var(--border-color); color: var(--primary); padding: 12px; border-radius: 12px; @@ -135,28 +127,17 @@ backdrop-filter: blur(20px); } - [data-theme="light"] .sidebar-toggle { - background: rgba(0, 102, 204, 0.1); - border: 1px solid rgba(0, 102, 204, 0.3); - color: var(--primary); - } - .sidebar-toggle:hover { - background: rgba(0, 240, 255, 0.2); + background: var(--text-primary); + color: var(--bg-dark); transform: scale(1.05); - box-shadow: 0 0 30px var(--primary-glow); - } - - [data-theme="light"] .sidebar-toggle:hover { - background: rgba(0, 102, 204, 0.2); - box-shadow: 0 0 30px var(--primary-glow); } .new-chat { width: 100%; padding: 16px; - background: linear-gradient(135deg, rgba(0, 240, 255, 0.15) 0%, rgba(255, 0, 229, 0.15) 100%); - border: 1px solid rgba(0, 240, 255, 0.3); + background: var(--bg-card); + border: 1px solid var(--border-color); border-radius: 12px; color: var(--primary); cursor: pointer; @@ -168,30 +149,19 @@ backdrop-filter: blur(10px); } - [data-theme="light"] .new-chat { - background: linear-gradient(135deg, rgba(0, 102, 204, 0.15) 0%, rgba(204, 0, 204, 0.15) 100%); - border: 1px solid rgba(0, 102, 204, 0.3); - color: var(--primary); - } - .new-chat:hover { - background: linear-gradient(135deg, rgba(0, 240, 255, 0.25) 0%, rgba(255, 0, 229, 0.25) 100%); + background: var(--text-primary); + color: var(--bg-dark); transform: translateY(-2px); - box-shadow: 0 10px 30px rgba(0, 240, 255, 0.3); - } - - [data-theme="light"] .new-chat:hover { - background: linear-gradient(135deg, rgba(0, 102, 204, 0.25) 0%, rgba(204, 0, 204, 0.25) 100%); - box-shadow: 0 10px 30px rgba(0, 102, 204, 0.3); } .voice-toggle { width: 100%; padding: 16px; - background: rgba(100, 255, 180, 0.1); - border: 1px solid rgba(100, 255, 180, 0.3); + background: var(--bg-card); + border: 1px solid var(--border-color); border-radius: 12px; - color: #64ffb4; + color: var(--text-secondary); cursor: pointer; margin-bottom: 16px; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); @@ -200,49 +170,28 @@ backdrop-filter: blur(10px); } - [data-theme="light"] .voice-toggle { - background: rgba(0, 200, 83, 0.1); - border: 1px solid rgba(0, 200, 83, 0.3); - color: #00c853; - } - .voice-toggle:hover { - background: rgba(100, 255, 180, 0.2); + background: var(--text-primary); + color: var(--bg-dark); transform: translateY(-2px); } - [data-theme="light"] .voice-toggle:hover { - background: rgba(0, 200, 83, 0.2); - } - .voice-toggle.recording { - background: rgba(255, 100, 120, 0.2); - border-color: rgba(255, 100, 120, 0.4); - color: #ff6478; + background: rgba(100, 100, 100, 0.2); + border-color: var(--text-secondary); animation: recordingPulse 2s infinite; } - [data-theme="light"] .voice-toggle.recording { - background: rgba(244, 67, 54, 0.2); - border-color: rgba(244, 67, 54, 0.4); - color: #f44336; - } - @keyframes recordingPulse { - 0%, 100% { box-shadow: 0 0 0 0 rgba(255, 100, 120, 0.4); } - 50% { box-shadow: 0 0 20px 10px rgba(255, 100, 120, 0); } - } - - [data-theme="light"] @keyframes recordingPulse { - 0%, 100% { box-shadow: 0 0 0 0 rgba(244, 67, 54, 0.4); } - 50% { box-shadow: 0 0 20px 10px rgba(244, 67, 54, 0); } + 0%, 100% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.4); } + 50% { box-shadow: 0 0 20px 10px rgba(255, 255, 255, 0); } } .history-item { padding: 12px; margin-bottom: 8px; - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.05); + background: var(--bg-card); + border: 1px solid var(--border-color); border-radius: 10px; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); @@ -250,22 +199,12 @@ word-wrap: break-word; } - [data-theme="light"] .history-item { - background: rgba(0, 0, 0, 0.03); - border: 1px solid rgba(0, 0, 0, 0.05); - } - .history-item:hover { - background: rgba(0, 240, 255, 0.1); - border-color: rgba(0, 240, 255, 0.3); + background: var(--text-primary); + color: var(--bg-dark); transform: translateX(4px); } - [data-theme="light"] .history-item:hover { - background: rgba(0, 102, 204, 0.1); - border-color: rgba(0, 102, 204, 0.3); - } - .main { margin-left: 0; width: 100%; @@ -283,20 +222,15 @@ } header { - background: rgba(15, 15, 25, 0.7); + background: var(--bg-card); backdrop-filter: blur(40px) saturate(180%); - border-bottom: 1px solid rgba(255, 255, 255, 0.05); + border-bottom: 1px solid var(--border-color); padding: 20px 40px 20px 90px; display: flex; align-items: center; justify-content: space-between; } - [data-theme="light"] header { - background: rgba(255, 255, 255, 0.7); - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - } - .logo { display: flex; align-items: center; @@ -306,14 +240,14 @@ .logo-icon { width: 48px; height: 48px; - background: linear-gradient(135deg, var(--primary), var(--secondary)); + background: var(--primary); border-radius: 14px; display: flex; align-items: center; justify-content: center; font-size: 28px; font-weight: 700; - color: #fff; + color: var(--bg-dark); box-shadow: 0 8px 32px var(--primary-glow); position: relative; overflow: hidden; @@ -324,32 +258,17 @@ height: 100%; object-fit: contain; padding: 6px; + filter: invert(1); } - .logo-icon::before { - content: ''; - position: absolute; - inset: -2px; - background: linear-gradient(135deg, var(--primary), var(--secondary)); - border-radius: 16px; - opacity: 0.5; - filter: blur(8px); - z-index: -1; + [data-theme="light"] .logo-icon img { + filter: invert(0); } .logo-text { font-size: 28px; font-weight: 700; - background: linear-gradient(135deg, var(--primary), var(--secondary)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - animation: gradientShift 3s ease infinite; - } - - @keyframes gradientShift { - 0%, 100% { filter: hue-rotate(0deg); } - 50% { filter: hue-rotate(20deg); } + color: var(--text-primary); } #messages { @@ -382,14 +301,14 @@ .empty-icon { width: 100px; height: 100px; - background: linear-gradient(135deg, var(--primary), var(--secondary)); + background: var(--primary); border-radius: 28px; display: inline-flex; align-items: center; justify-content: center; font-size: 56px; font-weight: 700; - color: #fff; + color: var(--bg-dark); margin-bottom: 24px; position: relative; animation: floatIcon 3s ease-in-out infinite; @@ -401,18 +320,11 @@ height: 100%; object-fit: contain; padding: 12px; + filter: invert(1); } - .empty-icon::before { - content: ''; - position: absolute; - inset: -3px; - background: linear-gradient(135deg, var(--primary), var(--secondary)); - border-radius: 30px; - opacity: 0.3; - filter: blur(20px); - z-index: -1; - animation: pulseGlow 2s ease-in-out infinite; + [data-theme="light"] .empty-icon img { + filter: invert(0); } @keyframes floatIcon { @@ -420,18 +332,10 @@ 50% { transform: translateY(-10px); } } - @keyframes pulseGlow { - 0%, 100% { opacity: 0.3; transform: scale(1); } - 50% { opacity: 0.6; transform: scale(1.05); } - } - .empty-title { font-size: 40px; font-weight: 700; - background: linear-gradient(135deg, var(--primary), var(--secondary)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; + color: var(--text-primary); margin-bottom: 12px; } @@ -453,8 +357,8 @@ } .user-message-content { - background: linear-gradient(135deg, rgba(0, 240, 255, 0.15) 0%, rgba(255, 0, 229, 0.15) 100%); - border: 1px solid rgba(0, 240, 255, 0.3); + background: var(--bg-card); + border: 1px solid var(--border-color); border-radius: 20px 20px 4px 20px; padding: 16px 20px; max-width: 70%; @@ -465,9 +369,7 @@ } [data-theme="light"] .user-message-content { - background: linear-gradient(135deg, rgba(0, 102, 204, 0.15) 0%, rgba(204, 0, 204, 0.15) 100%); - border: 1px solid rgba(0, 102, 204, 0.3); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.05); } .assistant-message { @@ -479,14 +381,14 @@ .assistant-avatar { width: 44px; height: 44px; - background: linear-gradient(135deg, var(--primary), var(--secondary)); + background: var(--primary); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 22px; font-weight: 700; - color: #fff; + color: var(--bg-dark); flex-shrink: 0; box-shadow: 0 8px 24px var(--primary-glow); position: relative; @@ -498,22 +400,16 @@ height: 100%; object-fit: contain; padding: 6px; + filter: invert(1); } - .assistant-avatar::before { - content: ''; - position: absolute; - inset: -2px; - background: linear-gradient(135deg, var(--primary), var(--secondary)); - border-radius: 14px; - opacity: 0.3; - filter: blur(8px); - z-index: -1; + [data-theme="light"] .assistant-avatar img { + filter: invert(0); } .assistant-message-content { - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.08); + background: var(--bg-card); + border: 1px solid var(--border-color); border-radius: 20px 20px 20px 4px; padding: 16px 20px; flex: 1; @@ -524,9 +420,7 @@ } [data-theme="light"] .assistant-message-content { - background: rgba(0, 0, 0, 0.03); - border: 1px solid rgba(0, 0, 0, 0.08); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.05); } .thinking-indicator { @@ -534,7 +428,7 @@ gap: 16px; align-items: center; padding: 16px 20px; - color: var(--primary); + color: var(--text-secondary); } .typing-dots { @@ -545,10 +439,9 @@ .typing-dot { width: 10px; height: 10px; - background: var(--primary); + background: var(--text-secondary); border-radius: 50%; animation: bounce 1.4s infinite ease-in-out; - box-shadow: 0 0 20px var(--primary-glow); } .typing-dot:nth-child(1) { animation-delay: -0.32s; } @@ -560,53 +453,39 @@ } footer { - background: rgba(15, 15, 25, 0.7); + background: var(--bg-card); backdrop-filter: blur(40px) saturate(180%); - border-top: 1px solid rgba(255, 255, 255, 0.05); + border-top: 1px solid var(--border-color); padding: 24px 40px; position: relative; } - [data-theme="light"] footer { - background: rgba(255, 255, 255, 0.7); - border-top: 1px solid rgba(0, 0, 0, 0.05); - } - .suggestions-container { display: flex; flex-wrap: wrap; - gap: 10px; + gap: 6px; margin-bottom: 16px; justify-content: center; } .suggestion-button { - padding: 10px 18px; - background: rgba(0, 240, 255, 0.1); - border: 1px solid rgba(0, 240, 255, 0.3); - color: var(--primary); - border-radius: 20px; + padding: 6px 12px; + background: var(--bg-card); + border: 1px solid var(--border-color); + color: var(--text-secondary); + border-radius: 16px; cursor: pointer; - font-size: 13px; + font-size: 11px; font-weight: 500; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); backdrop-filter: blur(10px); } - [data-theme="light"] .suggestion-button { - background: rgba(0, 102, 204, 0.1); - border: 1px solid rgba(0, 102, 204, 0.3); - } - .suggestion-button:hover { - background: rgba(0, 240, 255, 0.2); - transform: translateY(-2px); - box-shadow: 0 8px 24px rgba(0, 240, 255, 0.3); - } - - [data-theme="light"] .suggestion-button:hover { - background: rgba(0, 102, 204, 0.2); - box-shadow: 0 8px 24px rgba(0, 102, 204, 0.3); + background: var(--text-primary); + color: var(--bg-dark); + border-color: var(--text-primary); + transform: translateY(-1px); } .input-container { @@ -619,8 +498,8 @@ #messageInput { flex: 1; - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.1); + background: var(--bg-card); + border: 1px solid var(--border-color); border-radius: 16px; padding: 16px 24px; color: var(--text-primary); @@ -631,22 +510,9 @@ backdrop-filter: blur(10px); } - [data-theme="light"] #messageInput { - background: rgba(0, 0, 0, 0.05); - border: 1px solid rgba(0, 0, 0, 0.1); - color: var(--text-primary); - } - #messageInput:focus { - border-color: rgba(0, 240, 255, 0.5); - background: rgba(255, 255, 255, 0.08); - box-shadow: 0 0 30px rgba(0, 240, 255, 0.2); - } - - [data-theme="light"] #messageInput:focus { - border-color: rgba(0, 102, 204, 0.5); - background: rgba(0, 0, 0, 0.08); - box-shadow: 0 0 30px rgba(0, 102, 204, 0.2); + border-color: var(--text-primary); + box-shadow: 0 0 0 3px var(--primary-glow); } #messageInput::placeholder { @@ -654,11 +520,11 @@ } #sendBtn { - background: linear-gradient(135deg, var(--primary), var(--secondary)); + background: var(--primary); border: none; border-radius: 16px; padding: 16px 32px; - color: #fff; + color: var(--bg-dark); font-weight: 600; font-size: 15px; cursor: pointer; @@ -668,30 +534,17 @@ overflow: hidden; } - #sendBtn::before { - content: ''; - position: absolute; - inset: 0; - background: linear-gradient(135deg, rgba(255, 255, 255, 0.2), transparent); - opacity: 0; - transition: opacity 0.3s; - } - - #sendBtn:hover::before { - opacity: 1; - } - #sendBtn:hover { transform: translateY(-2px); box-shadow: 0 12px 36px var(--primary-glow); } #newChatBtn { - background: rgba(0, 240, 255, 0.1); - border: 1px solid rgba(0, 240, 255, 0.3); + background: var(--bg-card); + border: 1px solid var(--border-color); border-radius: 12px; padding: 12px 24px; - color: var(--primary); + color: var(--text-primary); font-weight: 600; font-size: 14px; cursor: pointer; @@ -699,54 +552,32 @@ backdrop-filter: blur(10px); } - [data-theme="light"] #newChatBtn { - background: rgba(0, 102, 204, 0.1); - border: 1px solid rgba(0, 102, 204, 0.3); - } - #newChatBtn:hover { - background: rgba(0, 240, 255, 0.2); + background: var(--text-primary); + color: var(--bg-dark); transform: translateY(-2px); - box-shadow: 0 8px 24px rgba(0, 240, 255, 0.3); - } - - [data-theme="light"] #newChatBtn:hover { - background: rgba(0, 102, 204, 0.2); - box-shadow: 0 8px 24px rgba(0, 102, 204, 0.3); } .voice-status { - background: rgba(100, 255, 180, 0.15); - border-bottom: 1px solid rgba(100, 255, 180, 0.3); + background: var(--bg-card); + border-bottom: 1px solid var(--border-color); padding: 16px 20px; text-align: center; - color: #64ffb4; + color: var(--text-secondary); font-weight: 600; backdrop-filter: blur(20px); } - [data-theme="light"] .voice-status { - background: rgba(0, 200, 83, 0.15); - border-bottom: 1px solid rgba(0, 200, 83, 0.3); - color: #00c853; - } - .warning-message { - background: rgba(255, 200, 0, 0.1); - border: 1px solid rgba(255, 200, 0, 0.3); + background: var(--bg-card); + border: 1px solid var(--border-color); border-radius: 12px; padding: 16px 20px; margin-bottom: 20px; - color: #ffc800; + color: var(--text-secondary); backdrop-filter: blur(10px); } - [data-theme="light"] .warning-message { - background: rgba(255, 152, 0, 0.1); - border: 1px solid rgba(255, 152, 0, 0.3); - color: #ff9800; - } - .connection-status { position: fixed; top: 24px; @@ -759,29 +590,19 @@ } .connection-status.connecting { - background-color: var(--accent); + background-color: var(--text-secondary); animation: connectingPulse 1.5s infinite; } .connection-status.connected { - background-color: #64ffb4; - box-shadow: 0 0 20px rgba(100, 255, 180, 0.6); + background-color: var(--primary); + box-shadow: 0 0 20px var(--primary-glow); animation: connectedPulse 2s infinite; } - [data-theme="light"] .connection-status.connected { - background-color: #00c853; - box-shadow: 0 0 20px rgba(0, 200, 83, 0.6); - } - .connection-status.disconnected { - background-color: #ff6478; - box-shadow: 0 0 20px rgba(255, 100, 120, 0.6); - } - - [data-theme="light"] .connection-status.disconnected { - background-color: #f44336; - box-shadow: 0 0 20px rgba(244, 67, 54, 0.6); + background-color: var(--text-secondary); + opacity: 0.5; } @keyframes connectingPulse { @@ -791,12 +612,7 @@ @keyframes connectedPulse { 0%, 100% { opacity: 0.8; transform: scale(1); } - 50% { opacity: 1; transform: scale(1.3); box-shadow: 0 0 30px rgba(100, 255, 180, 0.8); } - } - - [data-theme="light"] @keyframes connectedPulse { - 0%, 100% { opacity: 0.8; transform: scale(1); } - 50% { opacity: 1; transform: scale(1.3); box-shadow: 0 0 30px rgba(0, 200, 83, 0.8); } + 50% { opacity: 1; transform: scale(1.3); } } .markdown-content h1, @@ -805,10 +621,7 @@ margin-top: 24px; margin-bottom: 12px; font-weight: 600; - background: linear-gradient(135deg, var(--primary), var(--secondary)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; + color: var(--text-primary); } .markdown-content h1 { font-size: 28px; } @@ -831,24 +644,18 @@ } .markdown-content code { - background: rgba(0, 240, 255, 0.1); + background: var(--bg-card); padding: 3px 8px; border-radius: 6px; font-family: "JetBrains Mono", monospace; font-size: 14px; - color: var(--primary); - border: 1px solid rgba(0, 240, 255, 0.2); - } - - [data-theme="light"] .markdown-content code { - background: rgba(0, 102, 204, 0.1); - color: var(--primary); - border: 1px solid rgba(0, 102, 204, 0.2); + color: var(--text-primary); + border: 1px solid var(--border-color); } .markdown-content pre { - background: rgba(0, 0, 0, 0.4); - border: 1px solid rgba(255, 255, 255, 0.1); + background: var(--bg-card); + border: 1px solid var(--border-color); border-radius: 12px; padding: 20px; overflow-x: auto; @@ -856,20 +663,11 @@ backdrop-filter: blur(10px); } - [data-theme="light"] .markdown-content pre { - background: rgba(0, 0, 0, 0.05); - border: 1px solid rgba(0, 0, 0, 0.1); - } - .markdown-content pre code { background: none; padding: 0; border: none; - color: #e0e0e0; - } - - [data-theme="light"] .markdown-content pre code { - color: #333333; + color: var(--text-primary); } .markdown-content table { @@ -880,28 +678,19 @@ .markdown-content table th, .markdown-content table td { - border: 1px solid rgba(255, 255, 255, 0.1); + border: 1px solid var(--border-color); padding: 12px; text-align: left; } - [data-theme="light"] .markdown-content table th, - [data-theme="light"] .markdown-content table td { - border: 1px solid rgba(0, 0, 0, 0.1); - } - .markdown-content table th { - background: rgba(0, 240, 255, 0.1); + background: var(--bg-card); font-weight: 600; - color: var(--primary); - } - - [data-theme="light"] .markdown-content table th { - background: rgba(0, 102, 204, 0.1); + color: var(--text-primary); } .markdown-content blockquote { - border-left: 3px solid var(--primary); + border-left: 3px solid var(--text-primary); padding-left: 20px; margin: 16px 0; color: var(--text-secondary); @@ -910,7 +699,7 @@ .markdown-content strong { font-weight: 600; - color: var(--primary); + color: var(--text-primary); } .markdown-content em { @@ -919,18 +708,13 @@ } .markdown-content a { - color: var(--primary); - text-decoration: none; - border-bottom: 1px solid rgba(0, 240, 255, 0.3); + color: var(--text-primary); + text-decoration: underline; transition: all 0.3s; } - [data-theme="light"] .markdown-content a { - border-bottom: 1px solid rgba(0, 102, 204, 0.3); - } - .markdown-content a:hover { - border-bottom-color: var(--primary); + opacity: 0.7; } ::-webkit-scrollbar { @@ -938,28 +722,16 @@ } ::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.02); - } - - [data-theme="light"] ::-webkit-scrollbar-track { - background: rgba(0, 0, 0, 0.02); + background: var(--bg-card); } ::-webkit-scrollbar-thumb { - background: rgba(0, 240, 255, 0.3); + background: var(--border-color); border-radius: 5px; } - [data-theme="light"] ::-webkit-scrollbar-thumb { - background: rgba(0, 102, 204, 0.3); - } - ::-webkit-scrollbar-thumb:hover { - background: rgba(0, 240, 255, 0.5); - } - - [data-theme="light"] ::-webkit-scrollbar-thumb:hover { - background: rgba(0, 102, 204, 0.5); + background: var(--text-secondary); } .scroll-to-bottom { @@ -968,10 +740,10 @@ right: 24px; width: 48px; height: 48px; - background: linear-gradient(135deg, var(--primary), var(--secondary)); + background: var(--primary); border: none; border-radius: 50%; - color: #fff; + color: var(--bg-dark); font-size: 20px; cursor: pointer; display: flex; @@ -984,16 +756,15 @@ .scroll-to-bottom:hover { transform: scale(1.1); - box-shadow: 0 12px 36px var(--primary-glow); } .continue-button { display: inline-block; - background: rgba(0, 240, 255, 0.1); - border: 1px solid rgba(0, 240, 255, 0.3); + background: var(--bg-card); + border: 1px solid var(--border-color); border-radius: 10px; padding: 10px 20px; - color: var(--primary); + color: var(--text-primary); font-weight: 600; cursor: pointer; margin-top: 12px; @@ -1001,20 +772,10 @@ font-size: 14px; } - [data-theme="light"] .continue-button { - background: rgba(0, 102, 204, 0.1); - border: 1px solid rgba(0, 102, 204, 0.3); - } - .continue-button:hover { - background: rgba(0, 240, 255, 0.2); + background: var(--text-primary); + color: var(--bg-dark); transform: translateY(-2px); - box-shadow: 0 8px 24px rgba(0, 240, 255, 0.3); - } - - [data-theme="light"] .continue-button:hover { - background: rgba(0, 102, 204, 0.2); - box-shadow: 0 8px 24px rgba(0, 102, 204, 0.3); } .context-indicator { @@ -1022,9 +783,9 @@ bottom: 110px; right: 24px; width: 140px; - background: rgba(15, 15, 25, 0.95); + background: var(--bg-card); backdrop-filter: blur(20px); - border: 1px solid rgba(0, 240, 255, 0.3); + border: 1px solid var(--border-color); border-radius: 12px; padding: 12px; font-size: 12px; @@ -1034,48 +795,30 @@ } [data-theme="light"] .context-indicator { - background: rgba(255, 255, 255, 0.95); - border: 1px solid rgba(0, 102, 204, 0.3); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.05); } .context-progress { height: 6px; - background: rgba(255, 255, 255, 0.1); + background: var(--bg-card); border-radius: 3px; margin-top: 8px; overflow: hidden; } - [data-theme="light"] .context-progress { - background: rgba(0, 0, 0, 0.1); - } - .context-progress-bar { height: 100%; - background: linear-gradient(90deg, #64ffb4, var(--primary)); + background: var(--text-primary); border-radius: 3px; transition: width 0.3s ease, background-color 0.3s ease; } - [data-theme="light"] .context-progress-bar { - background: linear-gradient(90deg, #00c853, var(--primary)); - } - .context-progress-bar.warning { - background: linear-gradient(90deg, #ffc800, var(--accent)); - } - - [data-theme="light"] .context-progress-bar.warning { - background: linear-gradient(90deg, #ff9800, var(--accent)); + background: var(--text-secondary); } .context-progress-bar.danger { - background: linear-gradient(90deg, #ff6478, var(--secondary)); - } - - [data-theme="light"] .context-progress-bar.danger { - background: linear-gradient(90deg, #f44336, var(--secondary)); + background: var(--text-secondary); } .theme-toggle { @@ -1083,8 +826,8 @@ top: 24px; right: 60px; z-index: 101; - background: rgba(0, 240, 255, 0.1); - border: 1px solid rgba(0, 240, 255, 0.3); + background: var(--bg-card); + border: 1px solid var(--border-color); color: var(--primary); padding: 12px; border-radius: 12px; @@ -1099,20 +842,10 @@ backdrop-filter: blur(20px); } - [data-theme="light"] .theme-toggle { - background: rgba(0, 102, 204, 0.1); - border: 1px solid rgba(0, 102, 204, 0.3); - } - .theme-toggle:hover { - background: rgba(0, 240, 255, 0.2); + background: var(--text-primary); + color: var(--bg-dark); transform: scale(1.05); - box-shadow: 0 0 30px var(--primary-glow); - } - - [data-theme="light"] .theme-toggle:hover { - background: rgba(0, 102, 204, 0.2); - box-shadow: 0 0 30px var(--primary-glow); } @media (max-width: 768px) { @@ -1242,7 +975,7 @@
- +