From 88a2610a6298f42566a78c64ed7514890d22379f Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Fri, 12 Dec 2025 23:21:01 -0300 Subject: [PATCH] Lowercase botserver/botbook references --- PROMPT.md | 31 +- README.md | 6 +- ui/suite/tasks/autotask.js | 1355 +++++++++++++++++++++++------------- 3 files changed, 905 insertions(+), 487 deletions(-) diff --git a/PROMPT.md b/PROMPT.md index 5dd7e58..9ea148c 100644 --- a/PROMPT.md +++ b/PROMPT.md @@ -135,6 +135,35 @@ botbook/ # Documentation - NO custom JavaScript where HTMX can handle it ``` +### JavaScript Usage Guidelines + +**JS is ONLY acceptable when HTMX cannot handle the requirement:** + +| Use Case | Solution | +|----------|----------| +| Data fetching | HTMX `hx-get`, `hx-post` | +| Form submission | HTMX `hx-post`, `hx-put` | +| Real-time updates | HTMX WebSocket extension `hx-ext="ws"` | +| Content swapping | HTMX `hx-target`, `hx-swap` | +| Polling | HTMX `hx-trigger="every 5s"` | +| Loading states | HTMX `hx-indicator` | +| **Modal show/hide** | **JS required** - DOM manipulation | +| **Toast notifications** | **JS required** - dynamic element creation | +| **Clipboard operations** | **JS required** - `navigator.clipboard` API | +| **Keyboard shortcuts** | **JS required** - `keydown` event handling | +| **WebSocket state mgmt** | **JS required** - connection lifecycle | +| **Complex animations** | **JS required** - GSAP or custom | +| **Client-side validation** | **JS required** - before submission UX | + +**When writing JS:** +``` +- Keep it minimal - one function per concern +- No frameworks (React, Vue, etc.) - vanilla JS only +- Use vendor libs sparingly (htmx, marked, gsap, alpine) +- All JS must work with HTMX lifecycle (htmx:afterSwap, etc.) +- Prefer CSS for animations when possible +``` + ### Local Assets Only All external libraries are bundled locally - NEVER use CDN: @@ -346,7 +375,7 @@ grep -r "unpkg.com\|cdnjs\|jsdelivr" ui/ ``` src/main.rs # Entry point, mode detection src/lib.rs # Feature-gated exports -src/http_client.rs # BotServerClient wrapper +src/http_client.rs # botserverClient wrapper src/ui_server/mod.rs # Axum router, static files ui/suite/index.html # Main UI entry ui/suite/base.html # Base template diff --git a/README.md b/README.md index a3c7d93..c920a3c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # General Bots Desktop -An AI-powered desktop automation tool that records and plays back user interactions useful for legacy systems and common desktop tasks. The BotDesktop automation tool fills a critical gap in the enterprise automation landscape by addressing legacy systems and desktop applications that lack modern APIs or integration capabilities. While BotServer excels at creating conversational bots for modern channels like web, mobile and messaging platforms, many organizations still rely heavily on traditional desktop applications, mainframe systems, and custom internal tools that can only be accessed through their user interface. BotDesktop's ability to record and replay user interactions provides a practical bridge between these legacy systems and modern automation needs. +An AI-powered desktop automation tool that records and plays back user interactions useful for legacy systems and common desktop tasks. The BotDesktop automation tool fills a critical gap in the enterprise automation landscape by addressing legacy systems and desktop applications that lack modern APIs or integration capabilities. While botserver excels at creating conversational bots for modern channels like web, mobile and messaging platforms, many organizations still rely heavily on traditional desktop applications, mainframe systems, and custom internal tools that can only be accessed through their user interface. BotDesktop's ability to record and replay user interactions provides a practical bridge between these legacy systems and modern automation needs. ![image](https://github.com/user-attachments/assets/477b7472-81d8-4e38-a541-70a7e2496a02) @@ -10,10 +10,10 @@ An AI-powered desktop automation tool that records and plays back user interacti The tool's AI-powered approach to desktop automation represents a significant advancement over traditional robotic process automation (RPA) tools. By leveraging machine learning to understand screen elements and user interactions, BotDesktop can adapt to minor UI changes and variations that would break conventional scripted automation. This resilience is particularly valuable in enterprise environments where applications receive regular updates or where slight variations exist between different versions or installations of the same software. The AI component also simplifies the creation of automation scripts - instead of requiring complex programming, users can simply demonstrate the desired actions which BotDesktop observes and learns to replicate. -From an integration perspective, BotDesktop complements BotServer by enabling end-to-end automation scenarios that span both modern and legacy systems. For example, a bot created in BotServer could collect information from users through a modern chat interface, then use BotDesktop to input that data into a legacy desktop application that lacks API access. This hybrid approach allows organizations to modernize their user interactions while still leveraging their existing IT investments. Additionally, BotDesktop can automate routine desktop tasks like file management, data entry, and application monitoring that fall outside the scope of conversational bot interactions. +From an integration perspective, BotDesktop complements botserver by enabling end-to-end automation scenarios that span both modern and legacy systems. For example, a bot created in botserver could collect information from users through a modern chat interface, then use BotDesktop to input that data into a legacy desktop application that lacks API access. This hybrid approach allows organizations to modernize their user interactions while still leveraging their existing IT investments. Additionally, BotDesktop can automate routine desktop tasks like file management, data entry, and application monitoring that fall outside the scope of conversational bot interactions. -The combined toolset of BotServer and BotDesktop provides organizations with comprehensive automation capabilities across their entire technology stack. While BotServer handles the modern, API-driven interactions with users across multiple channels, BotDesktop extends automation capabilities to the desktop environment where many critical business processes still reside. This dual approach allows organizations to progressively modernize their systems while maintaining operational efficiency through automation of both new and legacy components. The result is a more flexible and complete automation solution that can adapt to various technical environments and business needs. +The combined toolset of botserver and BotDesktop provides organizations with comprehensive automation capabilities across their entire technology stack. While botserver handles the modern, API-driven interactions with users across multiple channels, BotDesktop extends automation capabilities to the desktop environment where many critical business processes still reside. This dual approach allows organizations to progressively modernize their systems while maintaining operational efficiency through automation of both new and legacy components. The result is a more flexible and complete automation solution that can adapt to various technical environments and business needs. ## Setup 1. Install dependencies: diff --git a/ui/suite/tasks/autotask.js b/ui/suite/tasks/autotask.js index 0fc15b4..8df662f 100644 --- a/ui/suite/tasks/autotask.js +++ b/ui/suite/tasks/autotask.js @@ -8,40 +8,40 @@ // ============================================================================= const AutoTaskState = { - currentFilter: 'all', - tasks: [], - compiledPlan: null, - pendingDecisions: [], - pendingApprovals: [], - refreshInterval: null, - wsConnection: null + currentFilter: "all", + tasks: [], + compiledPlan: null, + pendingDecisions: [], + pendingApprovals: [], + refreshInterval: null, + wsConnection: null, }; // ============================================================================= // INITIALIZATION // ============================================================================= -document.addEventListener('DOMContentLoaded', function() { - initAutoTask(); +document.addEventListener("DOMContentLoaded", function () { + initAutoTask(); }); function initAutoTask() { - // Initialize WebSocket for real-time updates - initWebSocket(); + // Initialize WebSocket for real-time updates + initWebSocket(); - // Start auto-refresh - startAutoRefresh(); + // Start auto-refresh + startAutoRefresh(); - // Setup event listeners - setupEventListeners(); + // Setup event listeners + setupEventListeners(); - // Load initial stats - updateStats(); + // Load initial stats + updateStats(); - // Setup keyboard shortcuts - setupKeyboardShortcuts(); + // Setup keyboard shortcuts + setupKeyboardShortcuts(); - console.log('AutoTask initialized'); + console.log("AutoTask initialized"); } // ============================================================================= @@ -49,57 +49,57 @@ function initAutoTask() { // ============================================================================= function initWebSocket() { - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.host}/ws/autotask`; + const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + const wsUrl = `${protocol}//${window.location.host}/ws/autotask`; - try { - AutoTaskState.wsConnection = new WebSocket(wsUrl); + try { + AutoTaskState.wsConnection = new WebSocket(wsUrl); - AutoTaskState.wsConnection.onopen = function() { - console.log('AutoTask WebSocket connected'); - }; + AutoTaskState.wsConnection.onopen = function () { + console.log("AutoTask WebSocket connected"); + }; - AutoTaskState.wsConnection.onmessage = function(event) { - handleWebSocketMessage(JSON.parse(event.data)); - }; + AutoTaskState.wsConnection.onmessage = function (event) { + handleWebSocketMessage(JSON.parse(event.data)); + }; - AutoTaskState.wsConnection.onclose = function() { - console.log('AutoTask WebSocket disconnected, reconnecting...'); - setTimeout(initWebSocket, 5000); - }; + AutoTaskState.wsConnection.onclose = function () { + console.log("AutoTask WebSocket disconnected, reconnecting..."); + setTimeout(initWebSocket, 5000); + }; - AutoTaskState.wsConnection.onerror = function(error) { - console.error('AutoTask WebSocket error:', error); - }; - } catch (e) { - console.warn('WebSocket not available, using polling'); - } + AutoTaskState.wsConnection.onerror = function (error) { + console.error("AutoTask WebSocket error:", error); + }; + } catch (e) { + console.warn("WebSocket not available, using polling"); + } } function handleWebSocketMessage(data) { - switch (data.type) { - case 'task_update': - updateTaskInList(data.task); - break; - case 'step_progress': - updateStepProgress(data.taskId, data.step, data.progress); - break; - case 'decision_required': - showDecisionNotification(data.decision); - break; - case 'approval_required': - showApprovalNotification(data.approval); - break; - case 'task_completed': - onTaskCompleted(data.task); - break; - case 'task_failed': - onTaskFailed(data.task, data.error); - break; - case 'stats_update': - updateStatsFromData(data.stats); - break; - } + switch (data.type) { + case "task_update": + updateTaskInList(data.task); + break; + case "step_progress": + updateStepProgress(data.taskId, data.step, data.progress); + break; + case "decision_required": + showDecisionNotification(data.decision); + break; + case "approval_required": + showApprovalNotification(data.approval); + break; + case "task_completed": + onTaskCompleted(data.task); + break; + case "task_failed": + onTaskFailed(data.task, data.error); + break; + case "stats_update": + updateStatsFromData(data.stats); + break; + } } // ============================================================================= @@ -107,66 +107,73 @@ function handleWebSocketMessage(data) { // ============================================================================= function setupEventListeners() { - // Intent form submission - const intentForm = document.getElementById('intent-form'); - if (intentForm) { - intentForm.addEventListener('htmx:afterSwap', function(event) { - if (event.detail.target.id === 'compilation-result') { - onCompilationComplete(event); - } - }); - } + // Intent form submission + const intentForm = document.getElementById("intent-form"); + if (intentForm) { + intentForm.addEventListener("htmx:afterSwap", function (event) { + if (event.detail.target.id === "compilation-result") { + onCompilationComplete(event); + } + }); + } - // Task list updates - const taskList = document.getElementById('task-list'); - if (taskList) { - taskList.addEventListener('htmx:afterSwap', function() { - updateStats(); - highlightPendingItems(); - }); - } + // Task list updates + const taskList = document.getElementById("task-list"); + if (taskList) { + taskList.addEventListener("htmx:afterSwap", function () { + updateStats(); + highlightPendingItems(); + }); + } - // Expand log entries on details open - document.addEventListener('toggle', function(event) { - if (event.target.classList.contains('execution-log') && event.target.open) { - const taskId = event.target.closest('.autotask-item')?.dataset.taskId; - if (taskId) { - loadExecutionLogs(taskId); - } + // Expand log entries on details open + document.addEventListener( + "toggle", + function (event) { + if ( + event.target.classList.contains("execution-log") && + event.target.open + ) { + const taskId = event.target.closest(".autotask-item")?.dataset.taskId; + if (taskId) { + loadExecutionLogs(taskId); } - }, true); + } + }, + true, + ); } function setupKeyboardShortcuts() { - document.addEventListener('keydown', function(e) { - // Alt + N: Focus on intent input - if (e.altKey && e.key === 'n') { - e.preventDefault(); - document.getElementById('intent-input')?.focus(); - } + document.addEventListener("keydown", function (e) { + // Alt + N: Focus on intent input + if (e.altKey && e.key === "n") { + e.preventDefault(); + document.getElementById("intent-input")?.focus(); + } - // Alt + R: Refresh tasks - if (e.altKey && e.key === 'r') { - e.preventDefault(); - refreshTasks(); - } + // Alt + R: Refresh tasks + if (e.altKey && e.key === "r") { + e.preventDefault(); + refreshTasks(); + } - // Escape: Close any open modal - if (e.key === 'Escape') { - closeAllModals(); - } + // Escape: Close any open modal + if (e.key === "Escape") { + closeAllModals(); + } - // Alt + 1-4: Switch filters - if (e.altKey && e.key >= '1' && e.key <= '4') { - e.preventDefault(); - const filters = ['all', 'running', 'approval', 'decision']; - const index = parseInt(e.key) - 1; - const tabs = document.querySelectorAll('.filter-tab'); - if (tabs[index]) { - tabs[index].click(); - } - } - }); + // Alt + 1-4: Switch filters + if (e.altKey && e.key >= "1" && e.key <= "4") { + e.preventDefault(); + const filters = ["all", "running", "approval", "decision"]; + const index = parseInt(e.key) - 1; + const tabs = document.querySelectorAll(".filter-tab"); + if (tabs[index]) { + tabs[index].click(); + } + } + }); } // ============================================================================= @@ -174,19 +181,19 @@ function setupKeyboardShortcuts() { // ============================================================================= function startAutoRefresh() { - // Refresh every 5 seconds - AutoTaskState.refreshInterval = setInterval(function() { - if (!document.hidden) { - updateStats(); - } - }, 5000); + // Refresh every 5 seconds + AutoTaskState.refreshInterval = setInterval(function () { + if (!document.hidden) { + updateStats(); + } + }, 5000); } function stopAutoRefresh() { - if (AutoTaskState.refreshInterval) { - clearInterval(AutoTaskState.refreshInterval); - AutoTaskState.refreshInterval = null; - } + if (AutoTaskState.refreshInterval) { + clearInterval(AutoTaskState.refreshInterval); + AutoTaskState.refreshInterval = null; + } } // ============================================================================= @@ -194,36 +201,39 @@ function stopAutoRefresh() { // ============================================================================= function updateStats() { - fetch('/api/autotask/stats') - .then(response => response.json()) - .then(stats => { - updateStatsFromData(stats); - }) - .catch(error => { - console.error('Failed to fetch stats:', error); - }); + fetch("/api/autotask/stats") + .then((response) => response.json()) + .then((stats) => { + updateStatsFromData(stats); + }) + .catch((error) => { + console.error("Failed to fetch stats:", error); + }); } function updateStatsFromData(stats) { - // Header stats - document.getElementById('stat-running').textContent = stats.running || 0; - document.getElementById('stat-pending').textContent = stats.pending || 0; - document.getElementById('stat-completed').textContent = stats.completed || 0; - document.getElementById('stat-approval').textContent = stats.pending_approval || 0; + // Header stats + document.getElementById("stat-running").textContent = stats.running || 0; + document.getElementById("stat-pending").textContent = stats.pending || 0; + document.getElementById("stat-completed").textContent = stats.completed || 0; + document.getElementById("stat-approval").textContent = + stats.pending_approval || 0; - // Filter counts - document.getElementById('count-all').textContent = stats.total || 0; - document.getElementById('count-running').textContent = stats.running || 0; - document.getElementById('count-approval').textContent = stats.pending_approval || 0; - document.getElementById('count-decision').textContent = stats.pending_decision || 0; + // Filter counts + document.getElementById("count-all").textContent = stats.total || 0; + document.getElementById("count-running").textContent = stats.running || 0; + document.getElementById("count-approval").textContent = + stats.pending_approval || 0; + document.getElementById("count-decision").textContent = + stats.pending_decision || 0; - // Highlight if approvals needed - const approvalStat = document.querySelector('.stat-item.highlight'); - if (approvalStat && stats.pending_approval > 0) { - approvalStat.classList.add('attention'); - } else if (approvalStat) { - approvalStat.classList.remove('attention'); - } + // Highlight if approvals needed + const approvalStat = document.querySelector(".stat-item.highlight"); + if (approvalStat && stats.pending_approval > 0) { + approvalStat.classList.add("attention"); + } else if (approvalStat) { + approvalStat.classList.remove("attention"); + } } // ============================================================================= @@ -231,28 +241,28 @@ function updateStatsFromData(stats) { // ============================================================================= function filterTasks(filter, button) { - AutoTaskState.currentFilter = filter; + AutoTaskState.currentFilter = filter; - // Update active tab - document.querySelectorAll('.filter-tab').forEach(tab => { - tab.classList.remove('active'); - }); - button.classList.add('active'); + // Update active tab + document.querySelectorAll(".filter-tab").forEach((tab) => { + tab.classList.remove("active"); + }); + button.classList.add("active"); - // Trigger HTMX request - htmx.ajax('GET', `/api/autotask/list?filter=${filter}`, { - target: '#task-list', - swap: 'innerHTML' - }); + // Trigger HTMX request + htmx.ajax("GET", `/api/autotask/list?filter=${filter}`, { + target: "#task-list", + swap: "innerHTML", + }); } function refreshTasks() { - const filter = AutoTaskState.currentFilter; - htmx.ajax('GET', `/api/autotask/list?filter=${filter}`, { - target: '#task-list', - swap: 'innerHTML' - }); - updateStats(); + const filter = AutoTaskState.currentFilter; + htmx.ajax("GET", `/api/autotask/list?filter=${filter}`, { + target: "#task-list", + swap: "innerHTML", + }); + updateStats(); } // ============================================================================= @@ -260,78 +270,111 @@ function refreshTasks() { // ============================================================================= function onCompilationComplete(event) { - const result = event.detail.target.querySelector('.compiled-plan'); - if (result) { - // Scroll to result - result.scrollIntoView({ behavior: 'smooth', block: 'start' }); + const result = event.detail.target.querySelector(".compiled-plan"); + if (result) { + // Scroll to result + result.scrollIntoView({ behavior: "smooth", block: "start" }); - // Store compiled plan - const planId = result.dataset?.planId; - if (planId) { - AutoTaskState.compiledPlan = planId; - } - - // Syntax highlight the code - highlightBasicCode(); + // Store compiled plan + const planId = result.dataset?.planId; + if (planId) { + AutoTaskState.compiledPlan = planId; } + + // Syntax highlight the code + highlightBasicCode(); + } } function highlightBasicCode() { - const codeBlocks = document.querySelectorAll('.code-preview code'); - codeBlocks.forEach(block => { - // Basic syntax highlighting for BASIC keywords - let html = block.innerHTML; + const codeBlocks = document.querySelectorAll(".code-preview code"); + codeBlocks.forEach((block) => { + // Basic syntax highlighting for BASIC keywords + let html = block.innerHTML; - // Keywords - const keywords = [ - 'PLAN_START', 'PLAN_END', 'STEP', 'SET', 'GET', 'IF', 'THEN', 'ELSE', 'END IF', - 'FOR EACH', 'NEXT', 'WHILE', 'WEND', 'TALK', 'HEAR', 'LLM', 'CREATE_TASK', - 'RUN_PYTHON', 'RUN_JAVASCRIPT', 'RUN_BASH', 'USE_MCP', 'POST', 'GET', 'PUT', - 'PATCH', 'DELETE HTTP', 'REQUIRE_APPROVAL', 'SIMULATE_IMPACT', 'AUDIT_LOG', - 'SEND_MAIL', 'SAVE', 'UPDATE', 'INSERT', 'DELETE', 'FIND' - ]; + // Keywords + const keywords = [ + "PLAN_START", + "PLAN_END", + "STEP", + "SET", + "GET", + "IF", + "THEN", + "ELSE", + "END IF", + "FOR EACH", + "NEXT", + "WHILE", + "WEND", + "TALK", + "HEAR", + "LLM", + "CREATE_TASK", + "RUN_PYTHON", + "RUN_JAVASCRIPT", + "RUN_BASH", + "USE_MCP", + "POST", + "GET", + "PUT", + "PATCH", + "DELETE HTTP", + "REQUIRE_APPROVAL", + "SIMULATE_IMPACT", + "AUDIT_LOG", + "SEND_MAIL", + "SAVE", + "UPDATE", + "INSERT", + "DELETE", + "FIND", + ]; - keywords.forEach(keyword => { - const regex = new RegExp(`\\b${keyword}\\b`, 'g'); - html = html.replace(regex, `${keyword}`); - }); - - // Comments - html = html.replace(/(\'[^\n]*)/g, '$1'); - - // Strings - html = html.replace(/("[^"]*")/g, '$1'); - - // Numbers - html = html.replace(/\b(\d+)\b/g, '$1'); - - block.innerHTML = html; + keywords.forEach((keyword) => { + const regex = new RegExp(`\\b${keyword}\\b`, "g"); + html = html.replace(regex, `${keyword}`); }); + + // Comments + html = html.replace(/(\'[^\n]*)/g, '$1'); + + // Strings + html = html.replace(/("[^"]*")/g, '$1'); + + // Numbers + html = html.replace(/\b(\d+)\b/g, '$1'); + + block.innerHTML = html; + }); } function copyGeneratedCode() { - const code = document.querySelector('.code-preview code')?.textContent; - if (code) { - navigator.clipboard.writeText(code).then(() => { - showToast('Code copied to clipboard', 'success'); - }).catch(() => { - showToast('Failed to copy code', 'error'); - }); - } + const code = document.querySelector(".code-preview code")?.textContent; + if (code) { + navigator.clipboard + .writeText(code) + .then(() => { + showToast("Code copied to clipboard", "success"); + }) + .catch(() => { + showToast("Failed to copy code", "error"); + }); + } } function discardPlan() { - if (confirm('Are you sure you want to discard this plan?')) { - document.getElementById('compilation-result').innerHTML = ''; - AutoTaskState.compiledPlan = null; - document.getElementById('intent-input').value = ''; - document.getElementById('intent-input').focus(); - } + if (confirm("Are you sure you want to discard this plan?")) { + document.getElementById("compilation-result").innerHTML = ""; + AutoTaskState.compiledPlan = null; + document.getElementById("intent-input").value = ""; + document.getElementById("intent-input").focus(); + } } function editPlan() { - // TODO: Implement plan editor - showToast('Plan editor coming soon!', 'info'); + // TODO: Implement plan editor + showToast("Plan editor coming soon!", "info"); } // ============================================================================= @@ -339,17 +382,17 @@ function editPlan() { // ============================================================================= function simulatePlan(planId) { - showSimulationModal(); + showSimulationModal(); - fetch(`/api/autotask/simulate/${planId}`, { - method: 'POST' + fetch(`/api/autotask/simulate/${planId}`, { + method: "POST", + }) + .then((response) => response.json()) + .then((result) => { + renderSimulationResult(result); }) - .then(response => response.json()) - .then(result => { - renderSimulationResult(result); - }) - .catch(error => { - document.getElementById('simulation-content').innerHTML = ` + .catch((error) => { + document.getElementById("simulation-content").innerHTML = `
❌

Failed to simulate plan: ${error.message}

@@ -359,37 +402,40 @@ function simulatePlan(planId) { } function executePlan(planId) { - const executionMode = document.querySelector('[name="execution_mode"]')?.value || 'semi-automatic'; - const priority = document.querySelector('[name="priority"]')?.value || 'medium'; + const executionMode = + document.querySelector('[name="execution_mode"]')?.value || + "semi-automatic"; + const priority = + document.querySelector('[name="priority"]')?.value || "medium"; - if (!confirm('Are you sure you want to execute this plan?')) { - return; - } + if (!confirm("Are you sure you want to execute this plan?")) { + return; + } - fetch('/api/autotask/execute', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - plan_id: planId, - execution_mode: executionMode, - priority: priority - }) + fetch("/api/autotask/execute", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + plan_id: planId, + execution_mode: executionMode, + priority: priority, + }), + }) + .then((response) => response.json()) + .then((result) => { + if (result.success) { + showToast("Task execution started!", "success"); + document.getElementById("compilation-result").innerHTML = ""; + document.getElementById("intent-input").value = ""; + refreshTasks(); + } else { + showToast(`Failed to start execution: ${result.error}`, "error"); + } }) - .then(response => response.json()) - .then(result => { - if (result.success) { - showToast('Task execution started!', 'success'); - document.getElementById('compilation-result').innerHTML = ''; - document.getElementById('intent-input').value = ''; - refreshTasks(); - } else { - showToast(`Failed to start execution: ${result.error}`, 'error'); - } - }) - .catch(error => { - showToast(`Failed to execute plan: ${error.message}`, 'error'); + .catch((error) => { + showToast(`Failed to execute plan: ${error.message}`, "error"); }); } @@ -398,22 +444,22 @@ function executePlan(planId) { // ============================================================================= function viewTaskDetails(taskId) { - window.location.href = `/suite/tasks/detail/${taskId}`; + window.location.href = `/suite/tasks/detail/${taskId}`; } function simulateTask(taskId) { - showSimulationModal(); + showSimulationModal(); - fetch(`/api/autotask/${taskId}/simulate`, { - method: 'POST' + fetch(`/api/autotask/${taskId}/simulate`, { + method: "POST", + }) + .then((response) => response.json()) + .then((result) => { + result.task_id = taskId; + renderSimulationResult(result); }) - .then(response => response.json()) - .then(result => { - result.task_id = taskId; - renderSimulationResult(result); - }) - .catch(error => { - document.getElementById('simulation-content').innerHTML = ` + .catch((error) => { + document.getElementById("simulation-content").innerHTML = `
❌

Failed to simulate task: ${error.message}

@@ -423,86 +469,92 @@ function simulateTask(taskId) { } function pauseTask(taskId) { - fetch(`/api/autotask/${taskId}/pause`, { - method: 'POST' - }) - .then(response => response.json()) - .then(result => { - if (result.success) { - showToast('Task paused', 'success'); - refreshTasks(); - } else { - showToast(`Failed to pause task: ${result.error}`, 'error'); - } + fetch(`/api/autotask/${taskId}/pause`, { + method: "POST", + }) + .then((response) => response.json()) + .then((result) => { + if (result.success) { + showToast("Task paused", "success"); + refreshTasks(); + } else { + showToast(`Failed to pause task: ${result.error}`, "error"); + } }); } function resumeTask(taskId) { - fetch(`/api/autotask/${taskId}/resume`, { - method: 'POST' - }) - .then(response => response.json()) - .then(result => { - if (result.success) { - showToast('Task resumed', 'success'); - refreshTasks(); - } else { - showToast(`Failed to resume task: ${result.error}`, 'error'); - } + fetch(`/api/autotask/${taskId}/resume`, { + method: "POST", + }) + .then((response) => response.json()) + .then((result) => { + if (result.success) { + showToast("Task resumed", "success"); + refreshTasks(); + } else { + showToast(`Failed to resume task: ${result.error}`, "error"); + } }); } function cancelTask(taskId) { - if (!confirm('Are you sure you want to cancel this task? This may not be reversible.')) { - return; - } + if ( + !confirm( + "Are you sure you want to cancel this task? This may not be reversible.", + ) + ) { + return; + } - fetch(`/api/autotask/${taskId}/cancel`, { - method: 'POST' - }) - .then(response => response.json()) - .then(result => { - if (result.success) { - showToast('Task cancelled', 'success'); - refreshTasks(); - } else { - showToast(`Failed to cancel task: ${result.error}`, 'error'); - } + fetch(`/api/autotask/${taskId}/cancel`, { + method: "POST", + }) + .then((response) => response.json()) + .then((result) => { + if (result.success) { + showToast("Task cancelled", "success"); + refreshTasks(); + } else { + showToast(`Failed to cancel task: ${result.error}`, "error"); + } }); } function updateTaskInList(task) { - const taskElement = document.querySelector(`[data-task-id="${task.id}"]`); - if (taskElement) { - // Update status badge - const statusBadge = taskElement.querySelector('.task-status-badge'); - if (statusBadge) { - statusBadge.className = `task-status-badge status-${task.status}`; - statusBadge.textContent = task.status.replace(/-/g, ' '); - } - - // Update progress - const progressFill = taskElement.querySelector('.progress-fill'); - const progressText = taskElement.querySelector('.progress-text'); - if (progressFill && progressText) { - progressFill.style.width = `${task.progress}%`; - progressText.textContent = `${task.current_step}/${task.total_steps} steps (${Math.round(task.progress)}%)`; - } - - // Update data attribute - taskElement.dataset.status = task.status; + const taskElement = document.querySelector(`[data-task-id="${task.id}"]`); + if (taskElement) { + // Update status badge + const statusBadge = taskElement.querySelector(".task-status-badge"); + if (statusBadge) { + statusBadge.className = `task-status-badge status-${task.status}`; + statusBadge.textContent = task.status.replace(/-/g, " "); } + + // Update progress + const progressFill = taskElement.querySelector(".progress-fill"); + const progressText = taskElement.querySelector(".progress-text"); + if (progressFill && progressText) { + progressFill.style.width = `${task.progress}%`; + progressText.textContent = `${task.current_step}/${task.total_steps} steps (${Math.round(task.progress)}%)`; + } + + // Update data attribute + taskElement.dataset.status = task.status; + } } function updateStepProgress(taskId, step, progress) { - const taskElement = document.querySelector(`[data-task-id="${taskId}"]`); - if (taskElement) { - const currentStep = taskElement.querySelector('.current-step'); - if (currentStep) { - currentStep.querySelector('.step-name').textContent = `Step ${step.order}: ${step.name}`; - currentStep.querySelector('.step-status').textContent = `${Math.round(progress)}%`; - } + const taskElement = document.querySelector(`[data-task-id="${taskId}"]`); + if (taskElement) { + const currentStep = taskElement.querySelector(".current-step"); + if (currentStep) { + currentStep.querySelector(".step-name").textContent = + `Step ${step.order}: ${step.name}`; + currentStep.querySelector(".step-status").textContent = + `${Math.round(progress)}%`; } + } } // ============================================================================= @@ -510,57 +562,61 @@ function updateStepProgress(taskId, step, progress) { // ============================================================================= function viewDecisions(taskId) { - showDecisionModal(); + showDecisionModal(); - fetch(`/api/autotask/${taskId}/decisions`) - .then(response => response.json()) - .then(decisions => { - renderDecisions(taskId, decisions); - }) - .catch(error => { - document.getElementById('decision-content').innerHTML = ` + fetch(`/api/autotask/${taskId}/decisions`) + .then((response) => response.json()) + .then((decisions) => { + renderDecisions(taskId, decisions); + }) + .catch((error) => { + document.getElementById("decision-content").innerHTML = `
❌

Failed to load decisions: ${error.message}

`; - }); + }); } function renderDecisions(taskId, decisions) { - const container = document.getElementById('decision-content'); + const container = document.getElementById("decision-content"); - if (!decisions || decisions.length === 0) { - container.innerHTML = '

No pending decisions.

'; - return; - } + if (!decisions || decisions.length === 0) { + container.innerHTML = '

No pending decisions.

'; + return; + } - let html = '
'; + let html = '
'; - decisions.forEach(decision => { - html += ` + decisions.forEach((decision) => { + html += `

${decision.title}

${decision.description}

- ${decision.options.map(opt => ` -
+ ${decision.options + .map( + (opt) => ` +
- +

${opt.description}

- 💰 ${opt.estimated_impact.cost_change >= 0 ? '+' : ''}$${opt.estimated_impact.cost_change} - âąī¸ ${opt.estimated_impact.time_change_minutes >= 0 ? '+' : ''}${opt.estimated_impact.time_change_minutes}m + 💰 ${opt.estimated_impact.cost_change >= 0 ? "+" : ""}$${opt.estimated_impact.cost_change} + âąī¸ ${opt.estimated_impact.time_change_minutes >= 0 ? "+" : ""}${opt.estimated_impact.time_change_minutes}m âš ī¸ ${opt.risk_level}
- `).join('')} + `, + ) + .join("")}
@@ -569,72 +625,78 @@ function renderDecisions(taskId, decisions) {
`; - }); + }); - html += '
'; - container.innerHTML = html; + html += "
"; + container.innerHTML = html; } function submitDecision(taskId, decisionId) { - const selectedOption = document.querySelector(`input[name="decision_${decisionId}"]:checked`)?.value; + const selectedOption = document.querySelector( + `input[name="decision_${decisionId}"]:checked`, + )?.value; - if (!selectedOption) { - showToast('Please select an option', 'warning'); - return; - } + if (!selectedOption) { + showToast("Please select an option", "warning"); + return; + } - fetch(`/api/autotask/${taskId}/decide`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - decision_id: decisionId, - option_id: selectedOption - }) - }) - .then(response => response.json()) - .then(result => { - if (result.success) { - showToast('Decision submitted', 'success'); - closeDecisionModal(); - refreshTasks(); - } else { - showToast(`Failed to submit decision: ${result.error}`, 'error'); - } + fetch(`/api/autotask/${taskId}/decide`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + decision_id: decisionId, + option_id: selectedOption, + }), + }) + .then((response) => response.json()) + .then((result) => { + if (result.success) { + showToast("Decision submitted", "success"); + closeDecisionModal(); + refreshTasks(); + } else { + showToast(`Failed to submit decision: ${result.error}`, "error"); + } }); } function skipDecision(taskId, decisionId) { - if (!confirm('Are you sure you want to skip this decision? The default option will be used.')) { - return; - } + if ( + !confirm( + "Are you sure you want to skip this decision? The default option will be used.", + ) + ) { + return; + } - fetch(`/api/autotask/${taskId}/decide`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - decision_id: decisionId, - skip: true - }) - }) - .then(response => response.json()) - .then(result => { - if (result.success) { - showToast('Decision skipped', 'info'); - closeDecisionModal(); - refreshTasks(); - } else { - showToast(`Failed to skip decision: ${result.error}`, 'error'); - } + fetch(`/api/autotask/${taskId}/decide`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + decision_id: decisionId, + skip: true, + }), + }) + .then((response) => response.json()) + .then((result) => { + if (result.success) { + showToast("Decision skipped", "info"); + closeDecisionModal(); + refreshTasks(); + } else { + showToast(`Failed to skip decision: ${result.error}`, "error"); + } }); } function showDecisionNotification(decision) { - showToast(`Decision required: ${decision.title}`, 'warning', 10000); - updateStats(); + showToast(`Decision required: ${decision.title}`, "warning", 10000); + updateStats(); } // ============================================================================= @@ -642,38 +704,38 @@ function showDecisionNotification(decision) { // ============================================================================= function viewApprovals(taskId) { - showApprovalModal(); + showApprovalModal(); - fetch(`/api/autotask/${taskId}/approvals`) - .then(response => response.json()) - .then(approvals => { - renderApprovals(taskId, approvals); - }) - .catch(error => { - document.getElementById('approval-content').innerHTML = ` + fetch(`/api/autotask/${taskId}/approvals`) + .then((response) => response.json()) + .then((approvals) => { + renderApprovals(taskId, approvals); + }) + .catch((error) => { + document.getElementById("approval-content").innerHTML = `
❌

Failed to load approvals: ${error.message}

`; - }); + }); } function renderApprovals(taskId, approvals) { - const container = document.getElementById('approval-content'); + const container = document.getElementById("approval-content"); - if (!approvals || approvals.length === 0) { - container.innerHTML = '

No pending approvals.

'; - return; - } + if (!approvals || approvals.length === 0) { + container.innerHTML = '

No pending approvals.

'; + return; + } - let html = '
'; + let html = '
'; - approvals.forEach(approval => { - html += ` + approvals.forEach((approval) => { + html += `
- ${approval.approval_type.replace(/_/g, ' ')} + ${approval.approval_type.replace(/_/g, " ")} ${approval.risk_level} Risk
@@ -685,7 +747,9 @@ function renderApprovals(taskId, approvals) {

${approval.impact_summary}

- ${approval.simulation_result ? ` + ${ + approval.simulation_result + ? `
Simulation Result
@@ -693,10 +757,12 @@ function renderApprovals(taskId, approvals) { Confidence: ${Math.round(approval.simulation_result.confidence * 100)}%
- ` : ''} + ` + : "" + }
- Step: ${approval.step_name || 'N/A'} + Step: ${approval.step_name || "N/A"} Expires: ${formatRelativeTime(approval.expires_at)} Default: ${approval.default_action}
@@ -714,58 +780,58 @@ function renderApprovals(taskId, approvals) {
`; - }); + }); - html += '
'; - container.innerHTML = html; + html += "
"; + container.innerHTML = html; } function approveApproval(taskId, approvalId) { - submitApprovalDecision(taskId, approvalId, 'approve'); + submitApprovalDecision(taskId, approvalId, "approve"); } function rejectApproval(taskId, approvalId) { - if (!confirm('Are you sure you want to reject this action?')) { - return; - } - submitApprovalDecision(taskId, approvalId, 'reject'); + if (!confirm("Are you sure you want to reject this action?")) { + return; + } + submitApprovalDecision(taskId, approvalId, "reject"); } function deferApproval(taskId, approvalId) { - submitApprovalDecision(taskId, approvalId, 'defer'); + submitApprovalDecision(taskId, approvalId, "defer"); } function submitApprovalDecision(taskId, approvalId, action) { - fetch(`/api/autotask/${taskId}/approve`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - approval_id: approvalId, - action: action - }) - }) - .then(response => response.json()) - .then(result => { - if (result.success) { - const messages = { - 'approve': 'Approval granted', - 'reject': 'Approval rejected', - 'defer': 'Approval deferred' - }; - showToast(messages[action], 'success'); - closeApprovalModal(); - refreshTasks(); - } else { - showToast(`Failed to ${action}: ${result.error}`, 'error'); - } + fetch(`/api/autotask/${taskId}/approve`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + approval_id: approvalId, + action: action, + }), + }) + .then((response) => response.json()) + .then((result) => { + if (result.success) { + const messages = { + approve: "Approval granted", + reject: "Approval rejected", + defer: "Approval deferred", + }; + showToast(messages[action], "success"); + closeApprovalModal(); + refreshTasks(); + } else { + showToast(`Failed to ${action}: ${result.error}`, "error"); + } }); } function showApprovalNotification(approval) { - showToast(`Approval required: ${approval.title}`, 'warning', 10000); - updateStats(); + showToast(`Approval required: ${approval.title}`, "warning", 10000); + updateStats(); } // ============================================================================= @@ -773,12 +839,14 @@ function showApprovalNotification(approval) { // ============================================================================= function renderSimulationResult(result) { - const container = document.getElementById('simulation-content'); + const container = document.getElementById("simulation-content"); - const statusIcon = result.success ? '✅' : 'âš ī¸'; - const statusText = result.success ? 'Simulation Successful' : 'Simulation Found Issues'; + const statusIcon = result.success ? "✅" : "âš ī¸"; + const statusText = result.success + ? "Simulation Successful" + : "Simulation Found Issues"; - let html = ` + let html = `
@@ -819,56 +887,377 @@ function renderSimulationResult(result) {

Step-by-Step Predictions

- ${result.step_outcomes.map(step => ` -
- ${step.would_succeed ? '✅' : 'âš ī¸'} + ${result.step_outcomes + .map( + (step) => ` +
+ ${step.would_succeed ? "✅" : "âš ī¸"} ${step.step_name} ${Math.round(step.success_probability * 100)}% success
- `).join('')} + `, + ) + .join("")}
- ${result.side_effects.length > 0 ? ` + ${ + result.side_effects.length > 0 + ? `

âš ī¸ Potential Side Effects

- ${result.side_effects.map(effect => ` + ${result.side_effects + .map( + (effect) => `
${effect.description} - ${effect.mitigation ? `Mitigation: ${effect.mitigation}` : ''} + ${effect.mitigation ? `Mitigation: ${effect.mitigation}` : ""}
- `).join('')} + `, + ) + .join("")}
- ` : ''} + ` + : "" + } - ${result.recommendations.length > 0 ? ` + ${ + result.recommendations.length > 0 + ? `

💡 Recommendations

- ${result.recommendations.map(rec => ` + ${result.recommendations + .map( + (rec) => `
${rec.description} - ${rec.action ? `` : ''} + ${rec.action ? `` : ""}
- `).join('')} + `, + ) + .join("")}
- ` : ''} + ` + : "" + }
-
`; - container.innerHTML = html; + container.innerHTML = html; } -function proc +// ============================================================================= +// MODAL FUNCTIONS +// ============================================================================= + +function showSimulationModal() { + const modal = document.getElementById("simulation-modal"); + if (modal) { + modal.style.display = "flex"; + document.body.classList.add("modal-open"); + // Show loading state + document.getElementById("simulation-content").innerHTML = ` +
+
+

Running impact simulation...

+
+ `; + } +} + +function closeSimulationModal() { + const modal = document.getElementById("simulation-modal"); + if (modal) { + modal.style.display = "none"; + document.body.classList.remove("modal-open"); + } +} + +function showDecisionModal() { + const modal = document.getElementById("decision-modal"); + if (modal) { + modal.style.display = "flex"; + document.body.classList.add("modal-open"); + // Show loading state + document.getElementById("decision-content").innerHTML = ` +
+
+

Loading decisions...

+
+ `; + } +} + +function closeDecisionModal() { + const modal = document.getElementById("decision-modal"); + if (modal) { + modal.style.display = "none"; + document.body.classList.remove("modal-open"); + } +} + +function showApprovalModal() { + const modal = document.getElementById("approval-modal"); + if (modal) { + modal.style.display = "flex"; + document.body.classList.add("modal-open"); + // Show loading state + document.getElementById("approval-content").innerHTML = ` +
+
+

Loading approvals...

+
+ `; + } +} + +function closeApprovalModal() { + const modal = document.getElementById("approval-modal"); + if (modal) { + modal.style.display = "none"; + document.body.classList.remove("modal-open"); + } +} + +function closeAllModals() { + closeSimulationModal(); + closeDecisionModal(); + closeApprovalModal(); +} + +// ============================================================================= +// SIMULATION ACTIONS +// ============================================================================= + +function proceedAfterSimulation(taskId) { + closeSimulationModal(); + + if (!taskId) { + showToast("No task ID provided", "error"); + return; + } + + fetch(`/api/autotask/${taskId}/execute`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + confirmed: true, + }), + }) + .then((response) => response.json()) + .then((result) => { + if (result.success) { + showToast("Task execution started!", "success"); + refreshTasks(); + } else { + showToast(result.error || "Failed to start execution", "error"); + } + }) + .catch((error) => { + console.error("Failed to proceed after simulation:", error); + showToast("Failed to start execution", "error"); + }); +} + +function applyRecommendation(recId) { + fetch(`/api/autotask/recommendations/${recId}/apply`, { + method: "POST", + }) + .then((response) => response.json()) + .then((result) => { + if (result.success) { + showToast("Recommendation applied", "success"); + // Re-run simulation to show updated results + const taskId = + document.querySelector(".simulation-result")?.dataset?.taskId; + if (taskId) { + simulateTask(taskId); + } + } else { + showToast(result.error || "Failed to apply recommendation", "error"); + } + }) + .catch((error) => { + console.error("Failed to apply recommendation:", error); + showToast("Failed to apply recommendation", "error"); + }); +} + +// ============================================================================= +// TOAST NOTIFICATIONS +// ============================================================================= + +function showToast(message, type = "info") { + // Get or create toast container + let container = document.getElementById("toast-container"); + if (!container) { + container = document.createElement("div"); + container.id = "toast-container"; + container.className = "toast-container"; + document.body.appendChild(container); + } + + // Create toast element + const toast = document.createElement("div"); + toast.className = `toast toast-${type}`; + + const icons = { + success: "✅", + error: "❌", + warning: "âš ī¸", + info: "â„šī¸", + }; + + toast.innerHTML = ` + ${icons[type] || icons.info} + ${message} + + `; + + container.appendChild(toast); + + // Auto-remove after 5 seconds + setTimeout(() => { + toast.classList.add("toast-fade-out"); + setTimeout(() => { + toast.remove(); + }, 300); + }, 5000); +} + +// ============================================================================= +// UTILITY FUNCTIONS +// ============================================================================= + +function formatDuration(seconds) { + if (seconds < 60) { + return `${Math.round(seconds)}s`; + } else if (seconds < 3600) { + const minutes = Math.floor(seconds / 60); + const secs = Math.round(seconds % 60); + return `${minutes}m ${secs}s`; + } else { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + return `${hours}h ${minutes}m`; + } +} + +function formatRelativeTime(dateString) { + if (!dateString) return "N/A"; + + const date = new Date(dateString); + const now = new Date(); + const diffMs = date - now; + const diffSeconds = Math.floor(diffMs / 1000); + const diffMinutes = Math.floor(diffSeconds / 60); + const diffHours = Math.floor(diffMinutes / 60); + const diffDays = Math.floor(diffHours / 24); + + if (diffMs < 0) { + // Past + const absDays = Math.abs(diffDays); + const absHours = Math.abs(diffHours); + const absMinutes = Math.abs(diffMinutes); + + if (absDays > 0) return `${absDays}d ago`; + if (absHours > 0) return `${absHours}h ago`; + if (absMinutes > 0) return `${absMinutes}m ago`; + return "just now"; + } else { + // Future + if (diffDays > 0) return `in ${diffDays}d`; + if (diffHours > 0) return `in ${diffHours}h`; + if (diffMinutes > 0) return `in ${diffMinutes}m`; + return "soon"; + } +} + +// ============================================================================= +// TASK LIFECYCLE HANDLERS +// ============================================================================= + +function onTaskCompleted(task) { + showToast(`Task completed: ${task.title || task.id}`, "success"); + updateTaskInList(task); + updateStats(); +} + +function onTaskFailed(task, error) { + showToast(`Task failed: ${task.title || task.id} - ${error}`, "error"); + updateTaskInList(task); + updateStats(); +} + +function highlightPendingItems() { + // Highlight tasks requiring attention + document.querySelectorAll(".autotask-item").forEach((item) => { + const status = item.dataset.status; + if (status === "pending-approval" || status === "pending-decision") { + item.classList.add("attention-required"); + } else { + item.classList.remove("attention-required"); + } + }); +} + +function loadExecutionLogs(taskId) { + const logContainer = document.querySelector( + `[data-task-id="${taskId}"] .log-entries`, + ); + if (!logContainer || logContainer.dataset.loaded === "true") { + return; + } + + logContainer.innerHTML = ` +
+
+

Loading logs...

+
+ `; + + fetch(`/api/autotask/${taskId}/logs`) + .then((response) => response.json()) + .then((logs) => { + if (!logs || logs.length === 0) { + logContainer.innerHTML = + "

No execution logs yet.

"; + } else { + logContainer.innerHTML = logs + .map( + (log) => ` +
+ ${new Date(log.timestamp).toLocaleTimeString()} + ${log.level} + ${log.message} +
+ `, + ) + .join(""); + } + logContainer.dataset.loaded = "true"; + }) + .catch((error) => { + logContainer.innerHTML = ` +
+ ❌ +

Failed to load logs: ${error.message}

+
+ `; + }); +}