use crate::auto_task::app_logs::{log_generator_error, log_generator_info}; use crate::basic::keywords::table_definition::{ generate_create_table_sql, FieldDefinition, TableDefinition, }; use crate::core::config::ConfigManager; use crate::core::shared::get_content_type; use crate::core::shared::models::UserSession; use crate::core::shared::state::{AgentActivity, AppState}; use aws_sdk_s3::primitives::ByteStream; use chrono::{DateTime, Utc}; use diesel::prelude::*; use diesel::sql_query; use log::{error, info, trace, warn}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use uuid::Uuid; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GeneratedApp { pub id: String, pub name: String, pub description: String, pub pages: Vec, pub tables: Vec, pub tools: Vec, pub schedulers: Vec, pub created_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GeneratedFile { pub filename: String, pub content: String, pub file_type: FileType, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GeneratedPage { pub filename: String, pub title: String, pub page_type: PageType, pub content: String, pub route: String, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum FileType { Html, Css, Js, Bas, Json, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum PageType { List, Form, Detail, Dashboard, } impl std::fmt::Display for PageType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::List => write!(f, "list"), Self::Form => write!(f, "form"), Self::Detail => write!(f, "detail"), Self::Dashboard => write!(f, "dashboard"), } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GeneratedScript { pub name: String, pub filename: String, pub script_type: ScriptType, pub content: String, pub triggers: Vec, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum ScriptType { Tool, Scheduler, Monitor, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppStructure { pub name: String, pub description: String, pub domain: String, pub tables: Vec, pub features: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SyncResult { pub tables_created: usize, pub fields_added: usize, pub migrations_applied: usize, } #[derive(Debug, Clone, Deserialize)] struct LlmGeneratedApp { name: String, description: String, #[serde(default)] _domain: String, tables: Vec, files: Vec, tools: Option>, schedulers: Option>, } #[derive(Debug, Clone, Deserialize)] struct LlmTable { name: String, fields: Vec, } #[derive(Debug, Clone, Deserialize)] struct LlmField { name: String, #[serde(rename = "type")] field_type: String, nullable: Option, reference: Option, default: Option, } #[derive(Debug, Clone, Deserialize)] struct LlmFile { filename: String, content: String, #[serde(rename = "type", default)] _file_type: Option, } pub struct AppGenerator { state: Arc, task_id: Option, generation_start: Option, files_written: Vec, tables_synced: Vec, bytes_generated: u64, } impl AppGenerator { pub fn new(state: Arc) -> Self { Self { state, task_id: None, generation_start: None, files_written: Vec::new(), tables_synced: Vec::new(), bytes_generated: 0, } } pub fn with_task_id(state: Arc, task_id: impl Into) -> Self { Self { state, task_id: Some(task_id.into()), generation_start: None, files_written: Vec::new(), tables_synced: Vec::new(), bytes_generated: 0, } } fn emit_activity(&self, step: &str, message: &str, current: u8, total: u8, activity: AgentActivity) { if let Some(ref task_id) = self.task_id { self.state.emit_activity(task_id, step, message, current, total, activity); } } fn calculate_speed(&self, items_done: u32) -> (f32, Option) { if let Some(start) = self.generation_start { let elapsed = start.elapsed().as_secs_f32(); if elapsed > 0.0 { let speed = (items_done as f32 / elapsed) * 60.0; return (speed, None); } } (0.0, None) } fn build_activity(&self, phase: &str, items_done: u32, items_total: Option, current_item: Option<&str>) -> AgentActivity { let (speed, eta) = self.calculate_speed(items_done); let mut activity = AgentActivity::new(phase) .with_progress(items_done, items_total) .with_bytes(self.bytes_generated); if speed > 0.0 { activity = activity.with_speed(speed, eta); } if !self.files_written.is_empty() { activity = activity.with_files(self.files_written.clone()); } if !self.tables_synced.is_empty() { activity = activity.with_tables(self.tables_synced.clone()); } if let Some(item) = current_item { activity = activity.with_current_item(item); } activity } pub async fn generate_app( &mut self, intent: &str, session: &UserSession, ) -> Result> { const TOTAL_STEPS: u8 = 8; self.generation_start = Some(std::time::Instant::now()); self.files_written.clear(); self.tables_synced.clear(); self.bytes_generated = 0; info!( "Generating app from intent: {}", &intent[..intent.len().min(100)] ); log_generator_info( "pending", &format!( "Starting app generation: {}", &intent[..intent.len().min(50)] ), ); if let Some(ref task_id) = self.task_id { self.state.emit_task_started(task_id, &format!("Generating app: {}", &intent[..intent.len().min(50)]), TOTAL_STEPS); } let activity = self.build_activity("analyzing", 0, Some(TOTAL_STEPS as u32), Some("Sending request to LLM")); self.emit_activity( "llm_request", "Analyzing request with AI...", 1, TOTAL_STEPS, activity ); info!("[APP_GENERATOR] Calling generate_complete_app_with_llm for intent: {}", &intent[..intent.len().min(50)]); let llm_start = std::time::Instant::now(); let llm_app = match self.generate_complete_app_with_llm(intent, session.bot_id).await { Ok(app) => { let llm_elapsed = llm_start.elapsed(); info!("[APP_GENERATOR] LLM generation completed in {:?}: app={}, files={}, tables={}", llm_elapsed, app.name, app.files.len(), app.tables.len()); log_generator_info( &app.name, "LLM successfully generated app structure and files", ); let total_bytes: u64 = app.files.iter().map(|f| f.content.len() as u64).sum(); self.bytes_generated = total_bytes; let activity = self.build_activity( "parsing", 1, Some(TOTAL_STEPS as u32), Some(&format!("Generated {} with {} files", app.name, app.files.len())) ); self.emit_activity( "llm_response", &format!("AI generated {} structure", app.name), 2, TOTAL_STEPS, activity ); app } Err(e) => { let llm_elapsed = llm_start.elapsed(); error!("[APP_GENERATOR] LLM generation failed after {:?}: {}", llm_elapsed, e); log_generator_error("unknown", "LLM app generation failed", &e.to_string()); if let Some(ref task_id) = self.task_id { self.state.emit_task_error(task_id, "llm_request", &e.to_string()); } return Err(e); } }; let activity = self.build_activity("parsing", 2, Some(TOTAL_STEPS as u32), Some(&format!("Processing {} structure", llm_app.name))); self.emit_activity("parse_structure", &format!("Parsing {} structure...", llm_app.name), 3, TOTAL_STEPS, activity); let tables = Self::convert_llm_tables(&llm_app.tables); if !tables.is_empty() { let table_names: Vec = tables.iter().map(|t| t.name.clone()).collect(); let activity = self.build_activity( "database", 3, Some(TOTAL_STEPS as u32), Some(&format!("Creating tables: {}", table_names.join(", "))) ); self.emit_activity( "create_tables", &format!("Creating {} database tables...", tables.len()), 4, TOTAL_STEPS, activity ); let tables_bas_content = Self::generate_table_definitions(&tables)?; if let Err(e) = self.append_to_tables_bas(session.bot_id, &tables_bas_content) { log_generator_error( &llm_app.name, "Failed to append to tables.bas", &e.to_string(), ); } match self.sync_tables_to_database(&tables) { Ok(result) => { log_generator_info( &llm_app.name, &format!( "Tables synced: {} created, {} fields", result.tables_created, result.fields_added ), ); self.tables_synced = table_names; let activity = self.build_activity( "database", 4, Some(TOTAL_STEPS as u32), Some(&format!("{} tables, {} fields created", result.tables_created, result.fields_added)) ); self.emit_activity( "tables_synced", "Database tables created", 4, TOTAL_STEPS, activity ); } Err(e) => { log_generator_error(&llm_app.name, "Failed to sync tables", &e.to_string()); } } } let bot_name = self.get_bot_name(session.bot_id)?; let bucket_name = format!("{}.gbai", bot_name.to_lowercase()); let drive_app_path = format!(".gbdrive/apps/{}", llm_app.name); let total_files = llm_app.files.len(); let activity = self.build_activity("writing", 0, Some(total_files as u32), Some("Preparing files")); self.emit_activity( "write_files", &format!("Writing {} app files...", total_files), 5, TOTAL_STEPS, activity ); let mut pages = Vec::new(); for (idx, file) in llm_app.files.iter().enumerate() { let drive_path = format!("{}/{}", drive_app_path, file.filename); self.files_written.push(file.filename.clone()); self.bytes_generated += file.content.len() as u64; let activity = self.build_activity( "writing", (idx + 1) as u32, Some(total_files as u32), Some(&file.filename) ); self.emit_activity( "write_file", &format!("Writing {}", file.filename), 5, TOTAL_STEPS, activity ); if let Err(e) = self .write_to_drive(&bucket_name, &drive_path, &file.content) .await { log_generator_error( &llm_app.name, &format!("Failed to write {}", file.filename), &e.to_string(), ); } let file_type = Self::detect_file_type(&file.filename); pages.push(GeneratedFile { filename: file.filename.clone(), content: file.content.clone(), file_type, }); } self.files_written.push("designer.js".to_string()); let activity = self.build_activity("configuring", total_files as u32, Some(total_files as u32), Some("designer.js")); self.emit_activity("write_designer", "Creating designer configuration...", 6, TOTAL_STEPS, activity); let designer_js = Self::generate_designer_js(&llm_app.name); self.bytes_generated += designer_js.len() as u64; self.write_to_drive( &bucket_name, &format!("{}/designer.js", drive_app_path), &designer_js, ) .await?; let mut tools = Vec::new(); if let Some(llm_tools) = &llm_app.tools { let tools_count = llm_tools.len(); let activity = self.build_activity("tools", 0, Some(tools_count as u32), Some("Creating BASIC tools")); self.emit_activity( "write_tools", &format!("Creating {} tools...", tools_count), 7, TOTAL_STEPS, activity ); for (idx, tool) in llm_tools.iter().enumerate() { let tool_path = format!(".gbdialog/tools/{}", tool.filename); self.files_written.push(format!("tools/{}", tool.filename)); self.bytes_generated += tool.content.len() as u64; let activity = self.build_activity("tools", (idx + 1) as u32, Some(tools_count as u32), Some(&tool.filename)); self.emit_activity("write_tool", &format!("Writing tool {}", tool.filename), 7, TOTAL_STEPS, activity); if let Err(e) = self .write_to_drive(&bucket_name, &tool_path, &tool.content) .await { log_generator_error( &llm_app.name, &format!("Failed to write tool {}", tool.filename), &e.to_string(), ); } tools.push(GeneratedFile { filename: tool.filename.clone(), content: tool.content.clone(), file_type: FileType::Bas, }); } } let mut schedulers = Vec::new(); if let Some(llm_schedulers) = &llm_app.schedulers { let sched_count = llm_schedulers.len(); let activity = self.build_activity("schedulers", 0, Some(sched_count as u32), Some("Creating schedulers")); self.emit_activity( "write_schedulers", &format!("Creating {} schedulers...", sched_count), 7, TOTAL_STEPS, activity ); for (idx, scheduler) in llm_schedulers.iter().enumerate() { let scheduler_path = format!(".gbdialog/schedulers/{}", scheduler.filename); self.files_written.push(format!("schedulers/{}", scheduler.filename)); self.bytes_generated += scheduler.content.len() as u64; let activity = self.build_activity("schedulers", (idx + 1) as u32, Some(sched_count as u32), Some(&scheduler.filename)); self.emit_activity("write_scheduler", &format!("Writing scheduler {}", scheduler.filename), 7, TOTAL_STEPS, activity); if let Err(e) = self .write_to_drive(&bucket_name, &scheduler_path, &scheduler.content) .await { log_generator_error( &llm_app.name, &format!("Failed to write scheduler {}", scheduler.filename), &e.to_string(), ); } schedulers.push(GeneratedFile { filename: scheduler.filename.clone(), content: scheduler.content.clone(), file_type: FileType::Bas, }); } } let activity = self.build_activity("syncing", TOTAL_STEPS as u32 - 1, Some(TOTAL_STEPS as u32), Some("Deploying to site")); self.emit_activity("sync_site", "Syncing app to site...", 8, TOTAL_STEPS, activity); self.sync_app_to_site_root(&bucket_name, &llm_app.name, session.bot_id) .await?; let elapsed = self.generation_start.map(|s| s.elapsed().as_secs()).unwrap_or(0); log_generator_info( &llm_app.name, &format!( "App generated: {} files, {} tables, {} tools in {}s", pages.len(), tables.len(), tools.len(), elapsed ), ); info!( "App '{}' generated in s3://{}/{}", llm_app.name, bucket_name, drive_app_path ); if let Some(ref task_id) = self.task_id { let final_activity = AgentActivity::new("completed") .with_progress(TOTAL_STEPS as u32, Some(TOTAL_STEPS as u32)) .with_bytes(self.bytes_generated) .with_files(self.files_written.clone()) .with_tables(self.tables_synced.clone()); let event = crate::core::shared::state::TaskProgressEvent::new(task_id, "complete", &format!( "App '{}' created: {} files, {} tables, {} bytes in {}s", llm_app.name, pages.len(), tables.len(), self.bytes_generated, elapsed )) .with_progress(TOTAL_STEPS, TOTAL_STEPS) .with_activity(final_activity) .completed(); self.state.broadcast_task_progress(event); } Ok(GeneratedApp { id: Uuid::new_v4().to_string(), name: llm_app.name, description: llm_app.description, pages, tables, tools, schedulers, created_at: Utc::now(), }) } fn get_platform_prompt() -> &'static str { r##" GENERAL BOTS PLATFORM - APP GENERATION You are an expert full-stack developer generating complete applications for General Bots platform. === AVAILABLE APIs === DATABASE (/api/db/): - GET /api/db/{table} - List records (query: limit, offset, order_by, order_dir, search, field=value) - GET /api/db/{table}/{id} - Get single record - GET /api/db/{table}/count - Count records - POST /api/db/{table} - Create record (JSON body) - PUT /api/db/{table}/{id} - Update record - DELETE /api/db/{table}/{id} - Delete record DRIVE (/api/drive/): - GET /api/drive/list?path=/folder - List files - GET /api/drive/download?path=/file - Download - POST /api/drive/upload - Upload (multipart) - DELETE /api/drive/delete?path=/file - Delete COMMUNICATION: - POST /api/mail/send - {"to", "subject", "body"} - POST /api/whatsapp/send - {"to", "message"} - POST /api/llm/generate - {"prompt", "max_tokens"} === HTMX REQUIREMENTS === All HTML pages MUST use HTMX exclusively. NO fetch(), NO XMLHttpRequest, NO inline onclick. Key attributes: - hx-get, hx-post, hx-put, hx-delete - HTTP methods - hx-target="#id" - Response destination - hx-swap="innerHTML|outerHTML|beforeend|delete" - Insert method - hx-trigger="click|submit|load|every 5s|keyup changed delay:300ms" - hx-indicator="#spinner" - Loading indicator - hx-confirm="Message?" - Confirmation - hx-vals='{"key":"value"}' - Extra values - hx-headers='{"X-Custom":"value"}' - Headers === REQUIRED HTML STRUCTURE === Every HTML file must include: ```html Page Title ``` === BASIC SCRIPTS (.bas) === Tools (triggered by chat): ``` HEAR "keyword1", "keyword2" result = GET FROM "table" WHERE field = value TALK "Response: " + result END HEAR ``` Schedulers (cron-based): ``` SET SCHEDULE "0 9 * * *" data = GET FROM "table" SEND MAIL TO "email" WITH SUBJECT "Report" BODY data END SCHEDULE ``` BASIC Keywords: - TALK "message" - Send message - ASK "question" - Get input - GET FROM "table" WHERE field=val - Query - SAVE TO "table" WITH field1, field2 - Insert - SEND MAIL TO "x" WITH SUBJECT "y" BODY "z" - result = LLM "prompt" - AI generation === FIELD TYPES === guid, string, text, integer, decimal, boolean, date, datetime, json === GENERATION RULES === 1. Generate COMPLETE, WORKING code - no placeholders, no "...", no "add more here" 2. Use semantic HTML5 (header, main, nav, section, article, footer) 3. Include loading states (hx-indicator) 4. Include error handling 5. Make it beautiful, modern, responsive 6. Include dark mode support in CSS 7. Tables should have id, created_at, updated_at fields 8. Forms must validate required fields 9. Lists must have search, pagination, edit/delete actions "## } async fn generate_complete_app_with_llm( &self, intent: &str, bot_id: Uuid, ) -> Result> { let platform = Self::get_platform_prompt(); let prompt = format!( r#"{platform} === USER REQUEST === "{intent}" === YOLO MODE - JUST BUILD IT === DO NOT ask questions. DO NOT request clarification. Just CREATE the app NOW. If user says "calculator" → build a full-featured calculator with basic ops, scientific functions, history If user says "CRM" → build customer management with contacts, companies, deals, notes If user says "inventory" → build stock tracking with products, categories, movements If user says "booking" → build appointment scheduler with calendar, slots, confirmations If user says ANYTHING → interpret creatively and BUILD SOMETHING AWESOME Respond with a single JSON object: {{ "name": "app-name-lowercase-dashes", "description": "What this app does", "domain": "healthcare|sales|inventory|booking|utility|etc", "tables": [ {{ "name": "table_name", "fields": [ {{"name": "id", "type": "guid", "nullable": false}}, {{"name": "created_at", "type": "datetime", "nullable": false, "default": "now()"}}, {{"name": "updated_at", "type": "datetime", "nullable": false, "default": "now()"}}, {{"name": "field_name", "type": "string", "nullable": true, "reference": null}} ] }} ], "files": [ {{"filename": "index.html", "content": "...complete HTML..."}}, {{"filename": "styles.css", "content": ":root {{...}} body {{...}} ...complete CSS..."}}, {{"filename": "table_name.html", "content": "...list page..."}}, {{"filename": "table_name_form.html", "content": "...form page..."}} ], "tools": [ {{"filename": "app_helper.bas", "content": "HEAR \"help\"\n TALK \"I can help with...\"\nEND HEAR"}} ], "schedulers": [ {{"filename": "daily_report.bas", "content": "SET SCHEDULE \"0 9 * * *\"\n ...\nEND SCHEDULE"}} ] }} CRITICAL RULES: - For utilities (calculator, timer, converter, BMI, mortgage): tables = [], focus on interactive HTML/JS - For data apps (CRM, inventory): design proper tables and CRUD pages - Generate ALL files completely - no placeholders, no "...", no shortcuts - CSS must be comprehensive with variables, responsive design, dark mode - Every HTML page needs proper structure with all required scripts - Replace APP_NAME_HERE with actual app name in data-app-name attribute - BE CREATIVE - add extra features the user didn't ask for but would love Respond with valid JSON only. NO QUESTIONS. JUST BUILD."# ); let response = self.call_llm(&prompt, bot_id).await?; Self::parse_llm_app_response(&response) } fn parse_llm_app_response( response: &str, ) -> Result> { let cleaned = response .trim() .trim_start_matches("```json") .trim_start_matches("```") .trim_end_matches("```") .trim(); match serde_json::from_str::(cleaned) { Ok(app) => { if app.files.is_empty() { return Err("LLM generated no files".into()); } Ok(app) } Err(e) => { error!("Failed to parse LLM response: {}", e); error!("Response was: {}", &response[..response.len().min(500)]); Err(format!("Failed to parse LLM response: {}", e).into()) } } } fn convert_llm_tables(llm_tables: &[LlmTable]) -> Vec { llm_tables .iter() .map(|t| { let fields = t .fields .iter() .enumerate() .map(|(i, f)| FieldDefinition { name: f.name.clone(), field_type: f.field_type.clone(), is_key: f.name == "id", is_nullable: f.nullable.unwrap_or(true), reference_table: f.reference.clone(), default_value: f.default.clone(), field_order: i as i32, ..Default::default() }) .collect(); TableDefinition { name: t.name.clone(), connection_name: "default".to_string(), fields, ..Default::default() } }) .collect() } fn detect_file_type(filename: &str) -> FileType { let ext = filename.rsplit('.').next().unwrap_or("").to_lowercase(); match ext.as_str() { "css" => FileType::Css, "js" => FileType::Js, "bas" => FileType::Bas, "json" => FileType::Json, _ => FileType::Html, } } async fn call_llm( &self, prompt: &str, bot_id: Uuid, ) -> Result> { #[cfg(feature = "llm")] { let config_manager = ConfigManager::new(self.state.conn.clone()); let model = config_manager .get_config(&bot_id, "llm-model", None) .unwrap_or_else(|_| { config_manager .get_config(&Uuid::nil(), "llm-model", None) .unwrap_or_else(|_| "gpt-4".to_string()) }); let key = config_manager .get_config(&bot_id, "llm-key", None) .unwrap_or_else(|_| { config_manager .get_config(&Uuid::nil(), "llm-key", None) .unwrap_or_default() }); let llm_config = serde_json::json!({ "temperature": 0.7, "max_tokens": 16000 }); let prompt_len = prompt.len(); info!("[APP_GENERATOR] Starting LLM call: model={}, prompt_len={} chars", model, prompt_len); let start = std::time::Instant::now(); match self .state .llm_provider .generate(prompt, &llm_config, &model, &key) .await { Ok(response) => { let elapsed = start.elapsed(); info!("[APP_GENERATOR] LLM call succeeded: response_len={} chars, elapsed={:?}", response.len(), elapsed); return Ok(response); } Err(e) => { let elapsed = start.elapsed(); error!("[APP_GENERATOR] LLM call failed after {:?}: {}", elapsed, e); return Err(e); } } } #[cfg(not(feature = "llm"))] { Err("LLM feature not enabled. App generation requires LLM.".into()) } } fn generate_table_definitions( tables: &[TableDefinition], ) -> Result> { use std::fmt::Write; let mut output = String::new(); for table in tables { let _ = writeln!(output, "\nTABLE {}", table.name); for field in &table.fields { let mut line = format!(" {} AS {}", field.name, field.field_type.to_uppercase()); if field.is_key { line.push_str(" KEY"); } if !field.is_nullable { line.push_str(" REQUIRED"); } if let Some(ref default) = field.default_value { let _ = write!(line, " DEFAULT {}", default); } if let Some(ref refs) = field.reference_table { let _ = write!(line, " REFERENCES {}", refs); } let _ = writeln!(output, "{}", line); } let _ = writeln!(output, "END TABLE\n"); } Ok(output) } fn append_to_tables_bas( &self, bot_id: Uuid, content: &str, ) -> Result<(), Box> { let bot_name = self.get_bot_name(bot_id)?; let bucket = format!("{}.gbai", bot_name.to_lowercase()); let path = ".gbdata/tables.bas"; let mut conn = self.state.conn.get()?; #[derive(QueryableByName)] struct ContentRow { #[diesel(sql_type = diesel::sql_types::Text)] content: String, } let existing: Option = sql_query("SELECT content FROM drive_files WHERE bucket = $1 AND path = $2 LIMIT 1") .bind::(&bucket) .bind::(path) .load::(&mut conn) .ok() .and_then(|rows| rows.into_iter().next().map(|r| r.content)); let new_content = match existing { Some(existing_content) => format!("{}\n{}", existing_content, content), None => content.to_string(), }; sql_query( "INSERT INTO drive_files (id, bucket, path, content, content_type, created_at, updated_at) VALUES ($1, $2, $3, $4, 'text/plain', NOW(), NOW()) ON CONFLICT (bucket, path) DO UPDATE SET content = $4, updated_at = NOW()", ) .bind::(Uuid::new_v4()) .bind::(&bucket) .bind::(path) .bind::(&new_content) .execute(&mut conn)?; Ok(()) } fn get_bot_name( &self, bot_id: Uuid, ) -> Result> { let mut conn = self.state.conn.get()?; #[derive(QueryableByName)] struct BotRow { #[diesel(sql_type = diesel::sql_types::Text)] name: String, } let result: Vec = sql_query("SELECT name FROM bots WHERE id = $1 LIMIT 1") .bind::(bot_id) .load(&mut conn)?; result .into_iter() .next() .map(|r| r.name) .ok_or_else(|| format!("Bot not found: {}", bot_id).into()) } async fn write_to_drive( &self, bucket: &str, path: &str, content: &str, ) -> Result<(), Box> { if let Some(ref s3) = self.state.s3_client { let body = ByteStream::from(content.as_bytes().to_vec()); let content_type = get_content_type(path); s3.put_object() .bucket(bucket) .key(path) .body(body) .content_type(content_type) .send() .await?; trace!("Wrote to S3: s3://{}/{}", bucket, path); } else { self.write_to_db_fallback(bucket, path, content)?; } Ok(()) } fn write_to_db_fallback( &self, bucket: &str, path: &str, content: &str, ) -> Result<(), Box> { let mut conn = self.state.conn.get()?; let content_type = get_content_type(path); sql_query( "INSERT INTO drive_files (id, bucket, path, content, content_type, size, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW()) ON CONFLICT (bucket, path) DO UPDATE SET content = EXCLUDED.content, content_type = EXCLUDED.content_type, size = EXCLUDED.size, updated_at = NOW()", ) .bind::(Uuid::new_v4()) .bind::(bucket) .bind::(path) .bind::(content) .bind::(content_type) .bind::(content.len() as i64) .execute(&mut conn)?; trace!("Wrote to DB: {}/{}", bucket, path); Ok(()) } fn sync_tables_to_database( &self, tables: &[TableDefinition], ) -> Result> { let mut tables_created = 0; let mut fields_added = 0; let mut conn = self.state.conn.get()?; for table in tables { let create_sql = generate_create_table_sql(table, "postgres"); match sql_query(&create_sql).execute(&mut conn) { Ok(_) => { tables_created += 1; fields_added += table.fields.len(); info!("Created table: {}", table.name); } Err(e) => { warn!("Table {} may already exist: {}", table.name, e); } } } Ok(SyncResult { tables_created, fields_added, migrations_applied: tables_created, }) } async fn sync_app_to_site_root( &self, bucket: &str, app_name: &str, bot_id: Uuid, ) -> Result<(), Box> { let source_path = format!(".gbdrive/apps/{}", app_name); let site_path = Self::get_site_path(bot_id); if let Some(ref s3) = self.state.s3_client { let list_result = s3 .list_objects_v2() .bucket(bucket) .prefix(&source_path) .send() .await?; if let Some(contents) = list_result.contents { for object in contents { if let Some(key) = object.key { let relative_path = key.trim_start_matches(&source_path).trim_start_matches('/'); let dest_key = format!("{}/{}/{}", site_path, app_name, relative_path); s3.copy_object() .bucket(bucket) .copy_source(format!("{}/{}", bucket, key)) .key(&dest_key) .send() .await?; trace!("Synced {} to {}", key, dest_key); } } } } let _ = self.store_app_metadata(bot_id, app_name, &format!("{}/{}", site_path, app_name)); info!("App synced to site root: {}/{}", site_path, app_name); Ok(()) } fn store_app_metadata( &self, bot_id: Uuid, app_name: &str, app_path: &str, ) -> Result<(), Box> { let mut conn = self.state.conn.get()?; let app_id = Uuid::new_v4(); sql_query( "INSERT INTO generated_apps (id, bot_id, name, app_path, is_active, created_at) VALUES ($1, $2, $3, $4, true, NOW()) ON CONFLICT (bot_id, name) DO UPDATE SET app_path = EXCLUDED.app_path, updated_at = NOW()", ) .bind::(app_id) .bind::(bot_id) .bind::(app_name) .bind::(app_path) .execute(&mut conn)?; Ok(()) } fn get_site_path(_bot_id: Uuid) -> String { ".gbdrive/site".to_string() } fn generate_designer_js(app_name: &str) -> String { format!( r#"(function() {{ const APP_NAME = '{app_name}'; const currentPage = window.location.pathname.split('/').pop() || 'index.html'; const style = document.createElement('style'); style.textContent = ` .designer-fab {{ position: fixed; bottom: 20px; right: 20px; width: 56px; height: 56px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; cursor: pointer; box-shadow: 0 4px 12px rgba(102,126,234,0.4); font-size: 24px; z-index: 9999; transition: transform 0.2s; }} .designer-fab:hover {{ transform: scale(1.1); }} .designer-panel {{ position: fixed; bottom: 90px; right: 20px; width: 380px; max-height: 500px; background: white; border-radius: 16px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); z-index: 9998; display: none; flex-direction: column; overflow: hidden; }} .designer-panel.open {{ display: flex; }} .designer-header {{ padding: 16px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; font-weight: 600; display: flex; justify-content: space-between; align-items: center; }} .designer-close {{ background: none; border: none; color: white; font-size: 20px; cursor: pointer; }} .designer-messages {{ flex: 1; overflow-y: auto; padding: 16px; max-height: 300px; }} .designer-msg {{ margin: 8px 0; padding: 10px 14px; border-radius: 12px; max-width: 85%; word-wrap: break-word; }} .designer-msg.user {{ background: #667eea; color: white; margin-left: auto; }} .designer-msg.ai {{ background: #f0f0f0; color: #333; }} .designer-input {{ display: flex; padding: 12px; border-top: 1px solid #eee; gap: 8px; }} .designer-input input {{ flex: 1; padding: 10px 14px; border: 1px solid #ddd; border-radius: 20px; outline: none; }} .designer-input button {{ padding: 10px 16px; background: #667eea; color: white; border: none; border-radius: 20px; cursor: pointer; }} `; document.head.appendChild(style); const fab = document.createElement('button'); fab.className = 'designer-fab'; fab.innerHTML = '🎨'; fab.title = 'Designer AI'; document.body.appendChild(fab); const panel = document.createElement('div'); panel.className = 'designer-panel'; panel.innerHTML = `
🎨 Designer AI
Hi! I can help you modify this app. What would you like to change?
`; document.body.appendChild(panel); fab.onclick = () => panel.classList.toggle('open'); panel.querySelector('.designer-close').onclick = () => panel.classList.remove('open'); const input = panel.querySelector('input'); const sendBtn = panel.querySelector('.designer-input button'); const messages = panel.querySelector('.designer-messages'); async function sendMessage() {{ const msg = input.value.trim(); if (!msg) return; messages.innerHTML += `
${{msg}}
`; input.value = ''; messages.scrollTop = messages.scrollHeight; try {{ const res = await fetch('/api/designer/modify', {{ method: 'POST', headers: {{ 'Content-Type': 'application/json' }}, body: JSON.stringify({{ app_name: APP_NAME, current_page: currentPage, message: msg }}) }}); const data = await res.json(); messages.innerHTML += `
${{data.message || 'Done!'}}
`; if (data.success && data.changes && data.changes.length > 0) {{ setTimeout(() => location.reload(), 1500); }} }} catch (e) {{ messages.innerHTML += `
Sorry, something went wrong. Try again.
`; if (window.AppLogger) window.AppLogger.error('Designer error', e.toString()); }} messages.scrollTop = messages.scrollHeight; }} sendBtn.onclick = sendMessage; input.onkeypress = (e) => {{ if (e.key === 'Enter') sendMessage(); }}; }})();"#, app_name = app_name ) } }