//! HTML rendering functions for task UI use crate::auto_task::TaskManifest; use crate::core::shared::state::AppState; use std::sync::Arc; /// Build HTML for the progress log section from step_results JSON pub fn build_terminal_html(step_results: &Option, status: &str) -> String { let mut html = String::new(); let mut output_lines: Vec<(String, bool)> = Vec::new(); if let Some(serde_json::Value::Array(steps)) = step_results { for step in steps.iter() { let step_status = step.get("status").and_then(|v| v.as_str()).unwrap_or(""); let is_current = step_status == "running" || step_status == "Running"; if let Some(serde_json::Value::Array(logs)) = step.get("logs") { for log_entry in logs.iter() { if let Some(msg) = log_entry.get("message").and_then(|v| v.as_str()) { if !msg.trim().is_empty() { output_lines.push((msg.to_string(), is_current)); } } if let Some(code) = log_entry.get("code").and_then(|v| v.as_str()) { if !code.trim().is_empty() { for line in code.lines().take(20) { output_lines.push((format!(" {}", line), is_current)); } } } if let Some(output) = log_entry.get("output").and_then(|v| v.as_str()) { if !output.trim().is_empty() { for line in output.lines().take(10) { output_lines.push((format!("→ {}", line), is_current)); } } } } } } } if output_lines.is_empty() { let msg = match status { "running" => "Agent working...", "pending" => "Waiting to start...", "completed" | "done" => "✓ Task completed", "failed" | "error" => "✗ Task failed", "paused" => "Task paused", _ => "Initializing..." }; html.push_str(&format!(r#"
{}
"#, msg)); } else { let start = if output_lines.len() > 15 { output_lines.len() - 15 } else { 0 }; for (line, is_current) in output_lines[start..].iter() { let class = if *is_current { "terminal-line current" } else { "terminal-line" }; let escaped = line.replace('<', "<").replace('>', ">"); html.push_str(&format!(r#"
{}
"#, class, escaped)); } } html } pub fn build_taskmd_html(state: &Arc, task_id: &str, title: &str, runtime: &str, db_manifest: Option<&serde_json::Value>) -> (String, String) { log::info!("Building TASK.md view for task_id: {}", task_id); // First, try to get manifest from in-memory cache (for active/running tasks) if let Ok(manifests) = state.task_manifests.read() { if let Some(manifest) = manifests.get(task_id) { log::info!("Found manifest in memory for task: {} with {} sections", manifest.app_name, manifest.sections.len()); let status_html = build_status_section_html(manifest, title, runtime); let progress_html = build_progress_log_html(manifest); return (status_html, progress_html); } } // If not in memory, try to load from database (for completed/historical tasks) if let Some(manifest_json) = db_manifest { log::info!("Found manifest in database for task: {}", task_id); if let Ok(manifest) = serde_json::from_value::(manifest_json.clone()) { log::info!("Parsed DB manifest for task: {} with {} sections", manifest.app_name, manifest.sections.len()); let status_html = build_status_section_html(&manifest, title, runtime); let progress_html = build_progress_log_html(&manifest); return (status_html, progress_html); } else { // Try parsing as web JSON format (the format we store) if let Some(web_manifest) = super::utils::parse_web_manifest_json(manifest_json) { log::info!("Parsed web manifest from DB for task: {}", task_id); let status_html = build_status_section_from_web_json(&web_manifest, title, runtime); let progress_html = build_progress_log_from_web_json(&web_manifest); return (status_html, progress_html); } log::warn!("Failed to parse manifest JSON for task: {}", task_id); } } log::info!("No manifest found for task: {}", task_id); let default_status = format!(r#"
{} Runtime: {}
"#, title, runtime); (default_status, r#"
No steps executed yet
"#.to_string()) } fn build_status_section_from_web_json(manifest: &serde_json::Value, title: &str, runtime: &str) -> String { let mut html = String::new(); let current_action = manifest .get("current_status") .and_then(|s| s.get("current_action")) .and_then(|a| a.as_str()) .unwrap_or("Processing..."); let estimated_seconds = manifest .get("estimated_seconds") .and_then(|e| e.as_u64()) .unwrap_or(0); let estimated = if estimated_seconds >= 60 { format!("{} min", estimated_seconds / 60) } else { format!("{} sec", estimated_seconds) }; let runtime_display = if runtime == "0s" || runtime == "calculating..." { "Not started".to_string() } else { runtime.to_string() }; html.push_str(&format!(r#"
{} Runtime: {}
{} Estimated: {}
"#, title, runtime_display, current_action, estimated)); html } fn build_progress_log_from_web_json(manifest: &serde_json::Value) -> String { let mut html = String::new(); html.push_str(r#"
"#); let total_steps = manifest .get("total_steps") .and_then(|t| t.as_u64()) .unwrap_or(60) as u32; let sections = match manifest.get("sections").and_then(|s| s.as_array()) { Some(s) => s, None => { html.push_str("
"); return html; } }; for section in sections { let section_id = section.get("id").and_then(|i| i.as_str()).unwrap_or("unknown"); let section_name = section.get("name").and_then(|n| n.as_str()).unwrap_or("Unknown"); let section_status = section.get("status").and_then(|s| s.as_str()).unwrap_or("Pending"); // Progress fields are nested inside a "progress" object in the web JSON format let progress = section.get("progress"); let current_step = progress .and_then(|p| p.get("current")) .and_then(|c| c.as_u64()) .unwrap_or(0) as u32; let global_step_start = progress .and_then(|p| p.get("global_start")) .and_then(|g| g.as_u64()) .unwrap_or(0) as u32; let section_class = match section_status.to_lowercase().as_str() { "completed" => "completed expanded", "running" => "running expanded", "failed" => "failed", "skipped" => "skipped", _ => "pending", }; let global_current = global_step_start + current_step; html.push_str(&format!(r#"
{} Step {}/{} {}
"#, section_class, section_id, section_name, global_current, total_steps, section_class, section_status, section_class)); // Render children if let Some(children) = section.get("children").and_then(|c| c.as_array()) { for child in children { let child_id = child.get("id").and_then(|i| i.as_str()).unwrap_or("unknown"); let child_name = child.get("name").and_then(|n| n.as_str()).unwrap_or("Unknown"); let child_status = child.get("status").and_then(|s| s.as_str()).unwrap_or("Pending"); // Progress fields are nested inside a "progress" object in the web JSON format let child_progress = child.get("progress"); let child_current = child_progress .and_then(|p| p.get("current")) .and_then(|c| c.as_u64()) .unwrap_or(0) as u32; let child_total = child_progress .and_then(|p| p.get("total")) .and_then(|t| t.as_u64()) .unwrap_or(0) as u32; let child_class = match child_status.to_lowercase().as_str() { "completed" => "completed expanded", "running" => "running expanded", "failed" => "failed", "skipped" => "skipped", _ => "pending", }; html.push_str(&format!(r#"
{} Step {}/{} {}
"#, child_class, child_id, child_name, child_current, child_total, child_class, child_status)); // Render items if let Some(items) = child.get("items").and_then(|i| i.as_array()) { for item in items { let item_name = item.get("name").and_then(|n| n.as_str()).unwrap_or("Unknown"); let item_status = item.get("status").and_then(|s| s.as_str()).unwrap_or("Pending"); let duration = item.get("duration_seconds").and_then(|d| d.as_u64()); let item_class = match item_status.to_lowercase().as_str() { "completed" => "completed", "running" => "running", _ => "pending", }; let check_mark = if item_status.to_lowercase() == "completed" { "✓" } else { "" }; let duration_str = duration .map(|s| if s >= 60 { format!("Duration: {} min", s / 60) } else { format!("Duration: {} sec", s) }) .unwrap_or_default(); html.push_str(&format!(r#"
{}
{} {}
"#, item_class, item_class, item_name, duration_str, item_class, check_mark)); } } html.push_str("
"); // Close tree-items and tree-child } } html.push_str("
"); // Close tree-children and tree-section } html.push_str(""); // Close taskmd-tree html } fn build_status_section_html(manifest: &TaskManifest, _title: &str, runtime: &str) -> String { let mut html = String::new(); let current_action = manifest.current_status.current_action.as_deref().unwrap_or("Processing..."); // Format estimated time nicely let estimated = if manifest.estimated_seconds >= 60 { format!("{} min", manifest.estimated_seconds / 60) } else { format!("{} sec", manifest.estimated_seconds) }; // Format runtime nicely let runtime_display = if runtime == "0s" || runtime == "calculating..." { "Not started".to_string() } else { runtime.to_string() }; html.push_str(&format!(r#"
{} Runtime: {} | Est: {}
"#, current_action, runtime_display, estimated)); if let Some(ref dp) = manifest.current_status.decision_point { html.push_str(&format!(r#"
Decision Point Coming (Step {}/{}) {}
"#, dp.step_current, dp.step_total, dp.message)); } html } fn build_progress_log_html(manifest: &TaskManifest) -> String { let mut html = String::new(); html.push_str(r#"
"#); let total_steps = manifest.total_steps; log::info!("Building progress log, {} sections, total_steps={}", manifest.sections.len(), total_steps); for section in &manifest.sections { log::info!("Section '{}': children={}, items={}, item_groups={}", section.name, section.children.len(), section.items.len(), section.item_groups.len()); let section_class = match section.status { crate::auto_task::SectionStatus::Completed => "completed expanded", crate::auto_task::SectionStatus::Running => "running expanded", crate::auto_task::SectionStatus::Failed => "failed", crate::auto_task::SectionStatus::Skipped => "skipped", _ => "pending", }; let status_text = match section.status { crate::auto_task::SectionStatus::Completed => "Completed", crate::auto_task::SectionStatus::Running => "Running", crate::auto_task::SectionStatus::Failed => "Failed", crate::auto_task::SectionStatus::Skipped => "Skipped", _ => "Pending", }; // Use global step count (e.g., "Step 24/60") let global_current = section.global_step_start + section.current_step; html.push_str(&format!(r#"
{} Step {}/{} {}
"#, section_class, section.id, section.name, global_current, total_steps, section_class, status_text, section_class)); for child in §ion.children { log::info!(" Child '{}': items={}, item_groups={}", child.name, child.items.len(), child.item_groups.len()); let child_class = match child.status { crate::auto_task::SectionStatus::Completed => "completed expanded", crate::auto_task::SectionStatus::Running => "running expanded", crate::auto_task::SectionStatus::Failed => "failed", crate::auto_task::SectionStatus::Skipped => "skipped", _ => "pending", }; let child_status = match child.status { crate::auto_task::SectionStatus::Completed => "Completed", crate::auto_task::SectionStatus::Running => "Running", crate::auto_task::SectionStatus::Failed => "Failed", crate::auto_task::SectionStatus::Skipped => "Skipped", _ => "Pending", }; html.push_str(&format!(r#"
{} Step {}/{} {}
"#, child_class, child.id, child.name, child.current_step, child.total_steps, child_class, child_status)); // Render item groups first (grouped fields like "email, password_hash, email_verified") for group in &child.item_groups { let group_class = match group.status { crate::auto_task::ItemStatus::Completed => "completed", crate::auto_task::ItemStatus::Running => "running", _ => "pending", }; let check_mark = if group.status == crate::auto_task::ItemStatus::Completed { "✓" } else { "" }; let group_duration = group.duration_seconds .map(|s| if s >= 60 { format!("Duration: {} min", s / 60) } else { format!("Duration: {} sec", s) }) .unwrap_or_default(); let group_name = group.display_name(); html.push_str(&format!(r#"
{} {} {}
"#, group_class, group.id, group_class, group_name, group_duration, group_class, check_mark)); } // Then individual items for item in &child.items { let item_class = match item.status { crate::auto_task::ItemStatus::Completed => "completed", crate::auto_task::ItemStatus::Running => "running", _ => "pending", }; let check_mark = if item.status == crate::auto_task::ItemStatus::Completed { "✓" } else { "" }; let item_duration = item.duration_seconds .map(|s| if s >= 60 { format!("Duration: {} min", s / 60) } else { format!("Duration: {} sec", s) }) .unwrap_or_default(); html.push_str(&format!(r#"
{} {} {}
"#, item_class, item.id, item_class, item.name, item_duration, item_class, check_mark)); } html.push_str("
"); } // Render section-level item groups for group in §ion.item_groups { let group_class = match group.status { crate::auto_task::ItemStatus::Completed => "completed", crate::auto_task::ItemStatus::Running => "running", _ => "pending", }; let check_mark = if group.status == crate::auto_task::ItemStatus::Completed { "✓" } else { "" }; let group_duration = group.duration_seconds .map(|s| if s >= 60 { format!("Duration: {} min", s / 60) } else { format!("Duration: {} sec", s) }) .unwrap_or_default(); let group_name = group.display_name(); html.push_str(&format!(r#"
{} {} {}
"#, group_class, group.id, group_class, group_name, group_duration, group_class, check_mark)); } // Render section-level items for item in §ion.items { let item_class = match item.status { crate::auto_task::ItemStatus::Completed => "completed", crate::auto_task::ItemStatus::Running => "running", _ => "pending", }; let check_mark = if item.status == crate::auto_task::ItemStatus::Completed { "✓" } else { "" }; let item_duration = item.duration_seconds .map(|s| if s >= 60 { format!("Duration: {} min", s / 60) } else { format!("Duration: {} sec", s) }) .unwrap_or_default(); html.push_str(&format!(r#"
{} {} {}
"#, item_class, item.id, item_class, item.name, item_duration, item_class, check_mark)); } html.push_str("
"); } html.push_str("
"); if manifest.sections.is_empty() { return r#"
No steps executed yet
"#.to_string(); } html }