/* ============================================================================= TASKS APP JAVASCRIPT Automated Intelligent Task Management Interface ============================================================================= */ // ============================================================================= // STATE MANAGEMENT // ============================================================================= // Prevent duplicate declaration when script is reloaded via HTMX if (typeof TasksState === "undefined") { var TasksState = { selectedTaskId: 2, // Default selected task currentFilter: "complete", tasks: [], wsConnection: null, agentLogPaused: false, selectedItemType: "task", // task, goal, pending, scheduler, monitor }; } // ============================================================================= // INITIALIZATION // ============================================================================= document.addEventListener("DOMContentLoaded", function () { // Only init if tasks app is visible if (document.querySelector(".tasks-app")) { initTasksApp(); } }); // Reinitialize when tasks page is loaded via HTMX document.body.addEventListener("htmx:afterSwap", function (evt) { // Check if tasks app was just loaded if (evt.detail.target && evt.detail.target.id === "main-content") { if (document.querySelector(".tasks-app")) { console.log( "[Tasks] Detected tasks app loaded via HTMX, initializing...", ); initTasksApp(); } } }); function initTasksApp() { // Only init WebSocket if not already connected if ( !TasksState.wsConnection || TasksState.wsConnection.readyState !== WebSocket.OPEN ) { initWebSocket(); } else { console.log("[Tasks] WebSocket already connected, skipping init"); } setupEventListeners(); setupKeyboardShortcuts(); setupIntentInputHandlers(); scrollAgentLogToBottom(); console.log("[Tasks] Initialized"); } function setupIntentInputHandlers() { const input = document.getElementById("quick-intent-input"); const btn = document.getElementById("quick-intent-btn"); if (input && btn) { input.addEventListener("keypress", function (e) { if (e.key === "Enter" && input.value.trim()) { e.preventDefault(); htmx.trigger(btn, "click"); } }); } document.body.addEventListener("htmx:beforeRequest", function (e) { if (e.detail.elt.id === "quick-intent-btn") { const resultDiv = document.getElementById("intent-result"); resultDiv.innerHTML = `
Processing your request...
`; } }); document.body.addEventListener("htmx:afterRequest", function (e) { if (e.detail.elt.id === "quick-intent-btn") { const resultDiv = document.getElementById("intent-result"); try { const response = JSON.parse(e.detail.xhr.responseText); // Handle async task creation (status 202 Accepted) if (response.status === "running" && response.task_id) { // Clear input immediately document.getElementById("quick-intent-input").value = ""; // Show floating progress panel const intentText = document .getElementById("quick-intent-input") .getAttribute("data-last-intent") || "Processing..."; showFloatingProgress(intentText); // Clear result div - progress is shown in floating panel resultDiv.innerHTML = ""; // Trigger task list refresh to show new task htmx.trigger(document.body, "taskCreated"); // Start polling for task status startTaskPolling(response.task_id); return; } // Handle completed task (legacy sync response) if (response.success) { let html = `
✓ ${response.message || "Done!"}
`; if (response.app_url) { html += ` Open App → `; } if (response.task_id) { html += `
Task ID: ${response.task_id}
`; } html += `
`; resultDiv.innerHTML = html; document.getElementById("quick-intent-input").value = ""; htmx.trigger(document.body, "taskCreated"); } else { resultDiv.innerHTML = `
✗ ${response.error || response.message || "Something went wrong"}
`; } } catch (err) { resultDiv.innerHTML = `
✗ Failed to process response
`; } } }); // Save intent text before submit for progress display if (input) { input.addEventListener("input", function () { input.setAttribute("data-last-intent", input.value); }); } } // Task polling for async task creation let activePollingTaskId = null; let pollingInterval = null; function startTaskPolling(taskId) { // Stop any existing polling stopTaskPolling(); activePollingTaskId = taskId; let pollCount = 0; const maxPolls = 180; // 3 minutes at 1 second intervals console.log(`[POLL] Starting polling for task ${taskId}`); pollingInterval = setInterval(async () => { pollCount++; if (pollCount > maxPolls) { console.log(`[POLL] Max polls reached for task ${taskId}`); stopTaskPolling(); errorFloatingProgress("Task timed out"); return; } try { const response = await fetch(`/api/autotask/tasks/${taskId}`); if (!response.ok) { console.error(`[POLL] Failed to fetch task status: ${response.status}`); return; } const task = await response.json(); console.log( `[POLL] Task ${taskId} status: ${task.status}, progress: ${task.progress || 0}%`, ); // Update progress const progress = task.progress || 0; const message = task.current_step || task.status || "Processing..."; updateFloatingProgressBar(message, progress, task); // Check if task is complete if (task.status === "completed" || task.status === "complete") { stopTaskPolling(); completeFloatingProgress(task); htmx.trigger(document.body, "taskCreated"); // Refresh task list showToast("Task completed successfully!", "success"); } else if (task.status === "failed" || task.status === "error") { stopTaskPolling(); errorFloatingProgress(task.error || "Task failed"); htmx.trigger(document.body, "taskCreated"); // Refresh task list showToast(task.error || "Task failed", "error"); } } catch (err) { console.error(`[POLL] Error polling task ${taskId}:`, err); } }, 1000); // Poll every 1 second } function stopTaskPolling() { if (pollingInterval) { clearInterval(pollingInterval); pollingInterval = null; } activePollingTaskId = null; } // ============================================================================= // WEBSOCKET CONNECTION // ============================================================================= function initWebSocket() { // Don't create new connection if one already exists and is open/connecting if (TasksState.wsConnection) { const state = TasksState.wsConnection.readyState; if (state === WebSocket.OPEN || state === WebSocket.CONNECTING) { console.log( "[Tasks WS] WebSocket already connected/connecting, skipping", ); return; } } const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const wsUrl = `${protocol}//${window.location.host}/ws/task-progress`; console.log("[Tasks WS] Attempting connection to:", wsUrl); try { TasksState.wsConnection = new WebSocket(wsUrl); TasksState.wsConnection.onopen = function () { console.log("[Tasks WS] WebSocket connected successfully"); addAgentLog("info", "[SYSTEM] Connected to task orchestrator"); }; TasksState.wsConnection.onmessage = function (event) { console.log("[Tasks WS] Raw message received:", event.data); try { const data = JSON.parse(event.data); console.log("[Tasks WS] Parsed message:", data.type, data); handleWebSocketMessage(data); } catch (e) { console.error("[Tasks WS] Failed to parse message:", e, event.data); } }; TasksState.wsConnection.onclose = function (event) { console.log( "[Tasks WS] WebSocket disconnected, code:", event.code, "reason:", event.reason, ); setTimeout(initWebSocket, 5000); }; TasksState.wsConnection.onerror = function (error) { console.error("[Tasks WS] WebSocket error:", error); }; } catch (e) { console.error("[Tasks WS] Failed to create WebSocket:", e); } } function handleWebSocketMessage(data) { console.log("[Tasks WS] handleWebSocketMessage called with type:", data.type); switch (data.type) { case "connected": console.log("[Tasks WS] Connected to task progress stream"); addAgentLog("info", "[SYSTEM] Task progress stream connected"); break; case "task_started": console.log( "[Tasks WS] TASK_STARTED - showing floating progress:", data.message, ); addAgentLog("accent", `[TASK] Started: ${data.message}`); showFloatingProgress(data.message); updateFloatingProgressBar(0, data.total_steps, data.message); updateActivityMetrics(data.activity); updateProgressUI(data); break; case "task_progress": console.log( "[Tasks WS] TASK_PROGRESS - step:", data.step, "message:", data.message, ); addAgentLog("info", `[${data.step}] ${data.message}`); if (data.activity) { updateActivityMetrics(data.activity); if (data.activity.current_item) { addAgentLog("info", ` → Processing: ${data.activity.current_item}`); } } else if (data.details) { addAgentLog("info", ` → ${data.details}`); } updateFloatingProgressBar( data.current_step, data.total_steps, data.message, data.step, data.details, data.activity, ); updateProgressUI(data); break; case "task_completed": console.log("[Tasks WS] TASK_COMPLETED:", data.message); addAgentLog("success", `[COMPLETE] ${data.message}`); if (data.activity) { updateActivityMetrics(data.activity); logFinalStats(data.activity); } completeFloatingProgress(data.message, data.activity); updateProgressUI(data); onTaskCompleted(data); if (typeof htmx !== "undefined") { htmx.trigger(document.body, "taskCreated"); } break; case "task_error": console.log("[Tasks WS] TASK_ERROR:", data.error || data.message); addAgentLog("error", `[ERROR] ${data.error || data.message}`); errorFloatingProgress(data.error || data.message); onTaskFailed(data, data.error); break; case "task_update": updateTaskCard(data.task); if (data.task && data.task.id === TasksState.selectedTaskId) { updateTaskDetail(data.task); } break; case "step_progress": updateStepProgress(data.taskId, data.step); break; case "agent_log": addAgentLog(data.level, data.message); break; case "decision_required": showDecisionRequired(data.decision); break; case "llm_stream": // Stream LLM output to terminal in real-time if (data.text) { addAgentLog("accent", data.text); addLLMStreamOutput(data.text); } break; } } function updateActivityMetrics(activity) { if (!activity) return; const metricsEl = document.getElementById("floating-activity-metrics"); if (!metricsEl) return; let html = ""; if (activity.phase) { html += `
Phase: ${activity.phase.toUpperCase()}
`; } if (activity.items_processed !== undefined) { const total = activity.items_total ? `/${activity.items_total}` : ""; html += `
Processed: ${activity.items_processed}${total} items
`; } if (activity.speed_per_min) { html += `
Speed: ~${activity.speed_per_min.toFixed(1)} items/min
`; } if (activity.eta_seconds) { const mins = Math.floor(activity.eta_seconds / 60); const secs = activity.eta_seconds % 60; const eta = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`; html += `
ETA: ${eta}
`; } if (activity.bytes_processed) { const kb = (activity.bytes_processed / 1024).toFixed(1); html += `
Generated: ${kb} KB
`; } if (activity.tokens_used) { html += `
Tokens: ${activity.tokens_used.toLocaleString()}
`; } if (activity.files_created && activity.files_created.length > 0) { html += `
Files: ${activity.files_created.length} created
`; } if (activity.tables_created && activity.tables_created.length > 0) { html += `
Tables: ${activity.tables_created.length} synced
`; } if (activity.current_item) { html += `
Current: ${activity.current_item}
`; } metricsEl.innerHTML = html; } function logFinalStats(activity) { if (!activity) return; addAgentLog("info", "─────────────────────────────────"); addAgentLog("info", "GENERATION COMPLETE"); if (activity.files_created && activity.files_created.length > 0) { addAgentLog("success", `Files created: ${activity.files_created.length}`); activity.files_created.forEach((f) => addAgentLog("info", ` • ${f}`)); } if (activity.tables_created && activity.tables_created.length > 0) { addAgentLog("success", `Tables synced: ${activity.tables_created.length}`); activity.tables_created.forEach((t) => addAgentLog("info", ` • ${t}`)); } if (activity.bytes_processed) { const kb = (activity.bytes_processed / 1024).toFixed(1); addAgentLog("info", `Total size: ${kb} KB`); } addAgentLog("info", "─────────────────────────────────"); } // ============================================================================= // FLOATING PROGRESS PANEL // ============================================================================= function showFloatingProgress(taskName) { let panel = document.getElementById("floating-progress"); if (!panel) { panel = document.createElement("div"); panel.id = "floating-progress"; panel.className = "floating-progress-panel"; panel.innerHTML = `
Processing...
Starting... 0%
LIVE AGENT ACTIVITY
`; document.body.appendChild(panel); } const taskNameEl = document.getElementById("floating-task-name"); if (taskNameEl) taskNameEl.textContent = taskName || "Processing..."; const fillEl = document.getElementById("floating-progress-fill"); if (fillEl) fillEl.style.width = "0%"; const stepEl = document.getElementById("floating-progress-step"); if (stepEl) stepEl.textContent = "Starting..."; const percentEl = document.getElementById("floating-progress-percent"); if (percentEl) percentEl.textContent = "0%"; const logEl = document.getElementById("floating-progress-log"); if (logEl) logEl.innerHTML = ""; const dotEl = panel.querySelector(".progress-dot"); if (dotEl) { dotEl.classList.remove("completed", "error"); } panel.style.display = "block"; panel.classList.remove("minimized"); } function updateFloatingProgressBar( current, total, message, step, details, activity, ) { const panel = document.getElementById("floating-progress"); if (!panel || panel.style.display === "none") { showFloatingProgress(message); } const percent = total > 0 ? Math.round((current / total) * 100) : 0; const fillEl = document.getElementById("floating-progress-fill"); if (fillEl) fillEl.style.width = percent + "%"; const stepEl = document.getElementById("floating-progress-step"); if (stepEl) stepEl.textContent = message; const percentEl = document.getElementById("floating-progress-percent"); if (percentEl) percentEl.textContent = percent + "%"; const statusEl = document.getElementById("terminal-status"); if (statusEl) statusEl.classList.add("active"); if (step) { const logEl = document.getElementById("floating-progress-log"); if (logEl) { const entry = document.createElement("div"); entry.className = "log-entry"; const timestamp = new Date().toLocaleTimeString("en-US", { hour12: false, }); let logContent = `${timestamp} [${step.toUpperCase()}] ${message}`; if (activity) { if (activity.current_item) { logContent += ` → ${activity.current_item}`; } if (activity.items_processed !== undefined && activity.items_total) { logContent += ` (${activity.items_processed}/${activity.items_total})`; } if (activity.bytes_processed) { const kb = (activity.bytes_processed / 1024).toFixed(1); logContent += ` [${kb} KB]`; } } else if (details) { logContent += ` → ${details}`; } entry.innerHTML = logContent; logEl.appendChild(entry); logEl.scrollTop = logEl.scrollHeight; } } if (activity) { updateActivityMetrics(activity); } } function completeFloatingProgress(message, activity) { const fillEl = document.getElementById("floating-progress-fill"); if (fillEl) fillEl.style.width = "100%"; const stepEl = document.getElementById("floating-progress-step"); if (stepEl) stepEl.textContent = message || "Completed!"; const percentEl = document.getElementById("floating-progress-percent"); if (percentEl) percentEl.textContent = "100%"; const panel = document.getElementById("floating-progress"); if (panel) { const dotEl = panel.querySelector(".progress-dot"); if (dotEl) dotEl.classList.add("completed"); } const statusEl = document.getElementById("terminal-status"); if (statusEl) { statusEl.classList.remove("active"); statusEl.classList.add("completed"); } if (activity) { const logEl = document.getElementById("floating-progress-log"); if (logEl) { const summary = document.createElement("div"); summary.className = "log-entry log-summary"; let summaryText = "═══════════════════════════════════\n"; summaryText += "✓ GENERATION COMPLETE\n"; if (activity.files_created) { summaryText += ` Files: ${activity.files_created.length} created\n`; } if (activity.tables_created) { summaryText += ` Tables: ${activity.tables_created.length} synced\n`; } if (activity.bytes_processed) { summaryText += ` Size: ${(activity.bytes_processed / 1024).toFixed(1)} KB\n`; } summaryText += "═══════════════════════════════════"; summary.innerHTML = `
${summaryText}
`; logEl.appendChild(summary); logEl.scrollTop = logEl.scrollHeight; } } setTimeout(closeFloatingProgress, 8000); } function errorFloatingProgress(errorMessage) { const stepEl = document.getElementById("floating-progress-step"); if (stepEl) stepEl.textContent = "Error: " + errorMessage; const panel = document.getElementById("floating-progress"); if (panel) { const dotEl = panel.querySelector(".progress-dot"); if (dotEl) dotEl.classList.add("error"); } } function minimizeFloatingProgress() { const panel = document.getElementById("floating-progress"); if (panel) panel.classList.toggle("minimized"); } function closeFloatingProgress() { const panel = document.getElementById("floating-progress"); if (panel) { panel.style.display = "none"; const dotEl = panel.querySelector(".progress-dot"); if (dotEl) dotEl.classList.remove("completed", "error"); } } function addLLMStreamOutput(text) { // Add LLM streaming output to the floating terminal const terminal = document.getElementById("floating-llm-terminal"); if (!terminal) return; const line = document.createElement("div"); line.className = "llm-output"; line.textContent = text; terminal.appendChild(line); terminal.scrollTop = terminal.scrollHeight; // Keep only last 100 lines to prevent memory issues while (terminal.children.length > 100) { terminal.removeChild(terminal.firstChild); } } function updateProgressUI(data) { const progressBar = document.querySelector(".result-progress-bar"); const resultDiv = document.getElementById("intent-result"); if (data.total_steps && data.current_step) { const percent = Math.round((data.current_step / data.total_steps) * 100); if (progressBar) { progressBar.style.width = `${percent}%`; } if (resultDiv && data.message) { resultDiv.innerHTML = `
${data.message}
Step ${data.current_step}/${data.total_steps} (${percent}%)
`; } } } // ============================================================================= // EVENT LISTENERS // ============================================================================= function setupEventListeners() { // Filter pills document.querySelectorAll(".status-pill").forEach((pill) => { pill.addEventListener("click", function (e) { e.preventDefault(); const filter = this.dataset.filter; setActiveFilter(filter, this); }); }); // Search input const searchInput = document.querySelector(".topbar-search-input"); if (searchInput) { searchInput.addEventListener( "input", debounce(function (e) { searchTasks(e.target.value); }, 300), ); } // Nav items document.querySelectorAll(".topbar-nav-item").forEach((item) => { item.addEventListener("click", function () { document .querySelectorAll(".topbar-nav-item") .forEach((i) => i.classList.remove("active")); this.classList.add("active"); }); }); // Progress log toggle const logToggle = document.querySelector(".progress-log-toggle"); if (logToggle) { logToggle.addEventListener("click", toggleProgressLog); } } function setupKeyboardShortcuts() { document.addEventListener("keydown", function (e) { // Escape: Deselect task if (e.key === "Escape") { deselectTask(); } // Cmd/Ctrl + K: Focus search if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault(); document.querySelector(".topbar-search-input")?.focus(); } // Arrow keys: Navigate tasks if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault(); navigateTasks(e.key === "ArrowDown" ? 1 : -1); } // Enter: Submit decision if in decision mode if ( e.key === "Enter" && document.querySelector(".decision-option.selected") ) { submitDecision(); } // 1-5: Quick filter if (e.key >= "1" && e.key <= "5" && !e.target.matches("input, textarea")) { const pills = document.querySelectorAll(".status-pill"); const index = parseInt(e.key) - 1; if (pills[index]) { pills[index].click(); } } }); } // ============================================================================= // TASK SELECTION & FILTERING // ============================================================================= function selectTask(taskId) { TasksState.selectedTaskId = taskId; // Update selected state in list document.querySelectorAll(".task-card").forEach((card) => { card.classList.toggle("selected", card.dataset.taskId == taskId); }); // Load task details (in real app, this would fetch from API) loadTaskDetails(taskId); } function deselectTask() { TasksState.selectedTaskId = null; document.querySelectorAll(".task-card").forEach((card) => { card.classList.remove("selected"); }); } function navigateTasks(direction) { const cards = Array.from(document.querySelectorAll(".task-card")); if (cards.length === 0) return; const currentIndex = cards.findIndex((c) => c.classList.contains("selected")); let newIndex; if (currentIndex === -1) { newIndex = direction === 1 ? 0 : cards.length - 1; } else { newIndex = currentIndex + direction; if (newIndex < 0) newIndex = cards.length - 1; if (newIndex >= cards.length) newIndex = 0; } const taskId = cards[newIndex].dataset.taskId; selectTask(taskId); cards[newIndex].scrollIntoView({ behavior: "smooth", block: "nearest" }); } function setActiveFilter(filter, button) { TasksState.currentFilter = filter; // Update active pill document.querySelectorAll(".status-pill").forEach((pill) => { pill.classList.remove("active"); }); button.classList.add("active"); // Filter will be handled by HTMX, but we track state addAgentLog("info", `[FILTER] Showing ${filter} tasks`); } function searchTasks(query) { if (query.length > 0) { addAgentLog("info", `[SEARCH] Searching: "${query}"`); } // In real app, this would filter via API // For demo, we'll do client-side filtering const cards = document.querySelectorAll(".task-card"); cards.forEach((card) => { const title = card.querySelector(".task-card-title")?.textContent.toLowerCase() || ""; const subtitle = card.querySelector(".task-card-subtitle")?.textContent.toLowerCase() || ""; const matches = title.includes(query.toLowerCase()) || subtitle.includes(query.toLowerCase()); card.style.display = matches || query === "" ? "block" : "none"; }); } // ============================================================================= // TASK DETAILS // ============================================================================= function loadTaskDetails(taskId) { addAgentLog("info", `[LOAD] Loading task #${taskId} details`); // Show detail panel and hide empty state const emptyState = document.getElementById("detail-empty"); const detailContent = document.getElementById("task-detail-content"); if (emptyState) emptyState.style.display = "none"; if (detailContent) detailContent.style.display = "block"; // Fetch task details from API htmx.ajax("GET", `/api/tasks/${taskId}`, { target: "#task-detail-content", swap: "innerHTML", }); } function updateTaskCard(task) { const card = document.querySelector(`[data-task-id="${task.id}"]`); if (!card) return; // Update progress const progressFill = card.querySelector(".task-progress-fill"); const progressPercent = card.querySelector(".task-progress-percent"); const progressSteps = card.querySelector(".task-progress-steps"); if (progressFill) progressFill.style.width = `${task.progress}%`; if (progressPercent) progressPercent.textContent = `${task.progress}%`; if (progressSteps) progressSteps.textContent = `${task.currentStep}/${task.totalSteps} steps`; // Update status badge const statusBadge = card.querySelector(".task-card-status"); if (statusBadge) { statusBadge.className = `task-card-status ${task.status}`; statusBadge.textContent = formatStatus(task.status); } } function updateTaskDetail(task) { // Update detail panel with task data const detailTitle = document.querySelector(".task-detail-title"); if (detailTitle) detailTitle.textContent = task.title; } // ============================================================================= // DECISION HANDLING // ============================================================================= function selectDecision(element, value) { // Remove selected from all options document.querySelectorAll(".decision-option").forEach((opt) => { opt.classList.remove("selected"); }); // Add selected to clicked option element.classList.add("selected"); // Store selected value TasksState.selectedDecision = value; addAgentLog("info", `[DECISION] Selected: ${value}`); } function submitDecision() { const selectedOption = document.querySelector(".decision-option.selected"); if (!selectedOption) { showToast("Please select an option", "warning"); return; } const value = TasksState.selectedDecision; const taskId = TasksState.selectedTaskId; addAgentLog("accent", `[AGENT] Applying decision: ${value}`); addAgentLog("info", `[TASK] Resuming task #${taskId}...`); // In real app, send to API fetch(`/api/tasks/${taskId}/decide`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ decision: value }), }) .then((response) => response.json()) .then((result) => { if (result.success) { showToast("Decision applied successfully", "success"); addAgentLog("success", `[OK] Decision applied, task resuming`); // Hide decision section (in real app, would update via HTMX) const decisionSection = document.querySelector( ".decision-required-section", ); if (decisionSection) { decisionSection.style.display = "none"; } } else { showToast("Failed to apply decision", "error"); addAgentLog( "error", `[ERROR] Failed to apply decision: ${result.error}`, ); } }) .catch((error) => { // For demo, simulate success showToast("Decision applied successfully", "success"); addAgentLog("success", `[OK] Decision applied, task resuming`); const decisionSection = document.querySelector( ".decision-required-section", ); if (decisionSection) { decisionSection.style.opacity = "0.5"; setTimeout(() => { decisionSection.style.display = "none"; }, 500); } // Update step status const activeStep = document.querySelector(".step-item.active"); if (activeStep) { activeStep.classList.remove("active"); activeStep.classList.add("completed"); activeStep.querySelector(".step-icon").textContent = "✓"; activeStep.querySelector(".step-detail").textContent = "Completed with merge strategy"; const nextStep = activeStep.nextElementSibling; if (nextStep && nextStep.classList.contains("pending")) { nextStep.classList.remove("pending"); nextStep.classList.add("active"); nextStep.querySelector(".step-icon").textContent = "●"; nextStep.querySelector(".step-time").textContent = "Now"; } } }); } function showDecisionRequired(decision) { addAgentLog("warning", `[ALERT] Decision required: ${decision.title}`); showToast(`Decision required: ${decision.title}`, "warning"); } // ============================================================================= // PROGRESS LOG // ============================================================================= function toggleProgressLog() { const stepList = document.querySelector(".step-list"); const toggle = document.querySelector(".progress-log-toggle"); if (stepList.style.display === "none") { stepList.style.display = "flex"; toggle.textContent = "Collapse"; } else { stepList.style.display = "none"; toggle.textContent = "Expand"; } } function updateStepProgress(taskId, step) { if (taskId !== TasksState.selectedTaskId) return; const stepItems = document.querySelectorAll(".step-item"); stepItems.forEach((item, index) => { if (index < step.index) { item.classList.remove("active", "pending"); item.classList.add("completed"); item.querySelector(".step-icon").textContent = "✓"; } else if (index === step.index) { item.classList.remove("completed", "pending"); item.classList.add("active"); item.querySelector(".step-icon").textContent = "●"; item.querySelector(".step-name").textContent = step.name; item.querySelector(".step-detail").textContent = step.detail; item.querySelector(".step-time").textContent = "Now"; } else { item.classList.remove("completed", "active"); item.classList.add("pending"); item.querySelector(".step-icon").textContent = "○"; } }); } // ============================================================================= // AGENT ACTIVITY LOG // ============================================================================= function addAgentLog(level, message) { if (TasksState.agentLogPaused) return; const log = document.getElementById("agent-log"); if (!log) return; const now = new Date(); const timestamp = now.toTimeString().split(" ")[0].substring(0, 8); const line = document.createElement("div"); line.className = `activity-line ${level}`; line.innerHTML = ` ${timestamp} ${message} `; // Insert at the top log.insertBefore(line, log.firstChild); // Limit log entries while (log.children.length > 100) { log.removeChild(log.lastChild); } } function scrollAgentLogToBottom() { const log = document.getElementById("agent-log"); if (log) { log.scrollTop = 0; // Since newest is at top } } function clearAgentLog() { const log = document.getElementById("agent-log"); if (log) { log.innerHTML = ""; addAgentLog("info", "[SYSTEM] Log cleared"); } } function toggleAgentLogPause() { TasksState.agentLogPaused = !TasksState.agentLogPaused; const pauseBtn = document.querySelector(".agent-activity-btn:last-child"); if (pauseBtn) { pauseBtn.textContent = TasksState.agentLogPaused ? "Resume" : "Pause"; } addAgentLog( "info", TasksState.agentLogPaused ? "[SYSTEM] Log paused" : "[SYSTEM] Log resumed", ); } // ============================================================================= // TASK ACTIONS // ============================================================================= function pauseTask(taskId) { addAgentLog("info", `[TASK] Pausing task #${taskId}...`); fetch(`/api/autotask/${taskId}/pause`, { method: "POST", headers: { "Content-Type": "application/json" }, }) .then((response) => response.json()) .then((result) => { if (result.success) { showToast("Task paused", "success"); addAgentLog("success", `[OK] Task #${taskId} paused`); htmx.trigger(document.body, "taskCreated"); if (TasksState.selectedTaskId === taskId) { loadTaskDetails(taskId); } } else { showToast("Failed to pause task", "error"); addAgentLog( "error", `[ERROR] Failed to pause task: ${result.error || result.message}`, ); } }) .catch((error) => { showToast("Failed to pause task", "error"); addAgentLog("error", `[ERROR] Failed to pause task: ${error}`); }); } function cancelTask(taskId) { if (!confirm("Are you sure you want to cancel this task?")) { return; } addAgentLog("info", `[TASK] Cancelling task #${taskId}...`); fetch(`/api/autotask/${taskId}/cancel`, { method: "POST", headers: { "Content-Type": "application/json" }, }) .then((response) => response.json()) .then((result) => { if (result.success) { showToast("Task cancelled", "success"); addAgentLog("success", `[OK] Task #${taskId} cancelled`); htmx.trigger(document.body, "taskCreated"); if (TasksState.selectedTaskId === taskId) { loadTaskDetails(taskId); } } else { showToast("Failed to cancel task", "error"); addAgentLog( "error", `[ERROR] Failed to cancel task: ${result.error || result.message}`, ); } }) .catch((error) => { showToast("Failed to cancel task", "error"); addAgentLog("error", `[ERROR] Failed to cancel task: ${error}`); }); } function showDetailedView(taskId) { addAgentLog("info", `[TASK] Opening detailed view for task #${taskId}...`); // For now, just reload the task details // In the future, this could open a modal or new page with more details loadTaskDetails(taskId); showToast("Detailed view loaded", "info"); } // ============================================================================= // TASK LIFECYCLE // ============================================================================= function onTaskCompleted(task) { showToast(`Task completed: ${task.title}`, "success"); addAgentLog("success", `[COMPLETE] Task #${task.id}: ${task.title}`); updateTaskCard(task); } function onTaskFailed(task, error) { showToast(`Task failed: ${task.title}`, "error"); addAgentLog("error", `[FAILED] Task #${task.id}: ${error}`); updateTaskCard(task); } // ============================================================================= // TOAST NOTIFICATIONS // ============================================================================= function showToast(message, type = "info") { let container = document.getElementById("toast-container"); if (!container) { container = document.createElement("div"); container.id = "toast-container"; container.style.cssText = ` position: fixed; bottom: 24px; right: 24px; z-index: 10000; display: flex; flex-direction: column; gap: 8px; `; document.body.appendChild(container); } const toast = document.createElement("div"); const bgColors = { success: "rgba(34, 197, 94, 0.95)", error: "rgba(239, 68, 68, 0.95)", warning: "rgba(245, 158, 11, 0.95)", info: "rgba(59, 130, 246, 0.95)", }; const icons = { success: "✓", error: "✕", warning: "⚠", info: "ℹ", }; toast.style.cssText = ` display: flex; align-items: center; gap: 10px; padding: 12px 16px; background: ${bgColors[type] || bgColors.info}; border-radius: 10px; color: white; font-size: 14px; font-weight: 500; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); animation: slideIn 0.3s ease; `; toast.innerHTML = ` ${icons[type] || icons.info} ${message} `; container.appendChild(toast); setTimeout(() => { toast.style.animation = "fadeOut 0.3s ease forwards"; setTimeout(() => toast.remove(), 300); }, 4000); } // ============================================================================= // UTILITY FUNCTIONS // ============================================================================= function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } function formatStatus(status) { const statusMap = { complete: "Complete", running: "Running", awaiting: "Awaiting", paused: "Paused", blocked: "Blocked", }; return statusMap[status] || status; } function formatTime(seconds) { if (seconds < 60) return `${seconds}s`; if (seconds < 3600) { const mins = Math.floor(seconds / 60); return `${mins}m`; } const hours = Math.floor(seconds / 3600); const mins = Math.floor((seconds % 3600) / 60); return `${hours}h ${mins}m`; } // ============================================================================= // GLOBAL STYLES FOR TOAST ANIMATIONS // ============================================================================= const style = document.createElement("style"); style.textContent = ` @keyframes slideIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } } @keyframes fadeOut { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(20px); } } `; document.head.appendChild(style); // ============================================================================= // GOALS, PENDING INFO, SCHEDULERS, MONITORS // ============================================================================= // Select a goal and show its details window.selectGoal = function (goalId) { TasksState.selectedItemType = "goal"; window.selectedTaskId = goalId; document.querySelectorAll(".task-item, .task-card").forEach((el) => { el.classList.remove("selected"); }); const selectedEl = document.querySelector(`[data-goal-id="${goalId}"]`); if (selectedEl) { selectedEl.classList.add("selected"); } document.getElementById("task-detail-empty").style.display = "none"; document.getElementById("task-detail-content").style.display = "block"; // Hide other sections, show goal section hideAllDetailSections(); document.getElementById("goal-progress-section").style.display = "block"; fetch(`/api/goals/${goalId}`) .then((response) => response.json()) .then((goal) => { document.getElementById("detail-title").textContent = goal.goal_text || "Untitled Goal"; document.getElementById("detail-status-text").textContent = goal.status || "active"; document.getElementById("detail-priority-text").textContent = "Goal"; document.getElementById("detail-description").textContent = goal.goal_text || ""; const percent = goal.target_value > 0 ? Math.round((goal.current_value / goal.target_value) * 100) : 0; document.getElementById("goal-progress-fill").style.width = `${percent}%`; document.getElementById("goal-current-value").textContent = goal.current_value || 0; document.getElementById("goal-target-value").textContent = goal.target_value || 0; document.getElementById("goal-percent").textContent = percent; document.getElementById("goal-last-action").textContent = goal.last_action ? `Last action: ${goal.last_action}` : "No actions yet"; }) .catch((err) => console.error("Failed to load goal:", err)); }; // Select a pending info item window.selectPendingInfo = function (pendingId) { TasksState.selectedItemType = "pending"; window.selectedTaskId = pendingId; document.querySelectorAll(".task-item, .task-card").forEach((el) => { el.classList.remove("selected"); }); const selectedEl = document.querySelector(`[data-pending-id="${pendingId}"]`); if (selectedEl) { selectedEl.classList.add("selected"); } document.getElementById("task-detail-empty").style.display = "none"; document.getElementById("task-detail-content").style.display = "block"; hideAllDetailSections(); document.getElementById("pending-fill-section").style.display = "block"; fetch(`/api/pending-info/${pendingId}`) .then((response) => response.json()) .then((pending) => { document.getElementById("detail-title").textContent = pending.field_label || "Pending Info"; document.getElementById("detail-status-text").textContent = "Pending"; document.getElementById("detail-priority-text").textContent = pending.app_name || ""; document.getElementById("detail-description").textContent = pending.reason || ""; document.getElementById("pending-reason").textContent = pending.reason || "Required for app functionality"; document.getElementById("pending-fill-id").value = pending.id; document.getElementById("pending-fill-label").textContent = pending.field_label; document.getElementById("pending-fill-value").type = pending.field_type === "secret" ? "password" : "text"; }) .catch((err) => console.error("Failed to load pending info:", err)); }; // Select a scheduler window.selectScheduler = function (schedulerName) { TasksState.selectedItemType = "scheduler"; window.selectedTaskId = schedulerName; document.querySelectorAll(".task-item, .task-card").forEach((el) => { el.classList.remove("selected"); }); const selectedEl = document.querySelector( `[data-scheduler-name="${schedulerName}"]`, ); if (selectedEl) { selectedEl.classList.add("selected"); } document.getElementById("task-detail-empty").style.display = "none"; document.getElementById("task-detail-content").style.display = "block"; hideAllDetailSections(); document.getElementById("scheduler-info-section").style.display = "block"; fetch(`/api/schedulers/${encodeURIComponent(schedulerName)}`) .then((response) => response.json()) .then((scheduler) => { document.getElementById("detail-title").textContent = scheduler.name || schedulerName; document.getElementById("detail-status-text").textContent = scheduler.status || "active"; document.getElementById("detail-priority-text").textContent = "Scheduler"; document.getElementById("detail-description").textContent = scheduler.description || ""; document.getElementById("scheduler-cron").textContent = scheduler.cron || "-"; document.getElementById("scheduler-next").textContent = scheduler.next_run ? `Next run: ${new Date(scheduler.next_run).toLocaleString()}` : "Next run: -"; document.getElementById("scheduler-file").textContent = scheduler.file ? `File: ${scheduler.file}` : "File: -"; }) .catch((err) => console.error("Failed to load scheduler:", err)); }; // Select a monitor window.selectMonitor = function (monitorName) { TasksState.selectedItemType = "monitor"; window.selectedTaskId = monitorName; document.querySelectorAll(".task-item, .task-card").forEach((el) => { el.classList.remove("selected"); }); const selectedEl = document.querySelector( `[data-monitor-name="${monitorName}"]`, ); if (selectedEl) { selectedEl.classList.add("selected"); } document.getElementById("task-detail-empty").style.display = "none"; document.getElementById("task-detail-content").style.display = "block"; hideAllDetailSections(); document.getElementById("monitor-info-section").style.display = "block"; fetch(`/api/monitors/${encodeURIComponent(monitorName)}`) .then((response) => response.json()) .then((monitor) => { document.getElementById("detail-title").textContent = monitor.name || monitorName; document.getElementById("detail-status-text").textContent = monitor.status || "active"; document.getElementById("detail-priority-text").textContent = "Monitor"; document.getElementById("detail-description").textContent = monitor.description || ""; document.getElementById("monitor-target").textContent = monitor.target ? `Target: ${monitor.target}` : "Target: -"; document.getElementById("monitor-interval").textContent = monitor.interval ? `Interval: ${monitor.interval}` : "Interval: -"; document.getElementById("monitor-last-check").textContent = monitor.last_check ? `Last check: ${new Date(monitor.last_check).toLocaleString()}` : "Last check: -"; document.getElementById("monitor-last-value").textContent = monitor.last_value ? `Last value: ${monitor.last_value}` : "Last value: -"; }) .catch((err) => console.error("Failed to load monitor:", err)); }; // Hide all detail sections function hideAllDetailSections() { document.getElementById("goal-progress-section").style.display = "none"; document.getElementById("pending-fill-section").style.display = "none"; document.getElementById("scheduler-info-section").style.display = "none"; document.getElementById("monitor-info-section").style.display = "none"; } // Fill pending info form submission document.addEventListener("htmx:afterRequest", function (event) { if (event.detail.elt.id === "pending-fill-form" && event.detail.successful) { htmx.trigger(document.body, "taskCreated"); document.getElementById("pending-fill-value").value = ""; addAgentLog("success", "[OK] Pending info filled successfully"); } }); // Update counts for new filters function updateFilterCounts() { fetch("/api/tasks/stats/json") .then((response) => response.json()) .then((stats) => { if (stats.total !== undefined) { const el = document.getElementById("count-all"); if (el) el.textContent = stats.total; } if (stats.completed !== undefined) { const el = document.getElementById("count-complete"); if (el) el.textContent = stats.completed; } if (stats.active !== undefined) { const el = document.getElementById("count-active"); if (el) el.textContent = stats.active; } if (stats.awaiting !== undefined) { const el = document.getElementById("count-awaiting"); if (el) el.textContent = stats.awaiting; } if (stats.paused !== undefined) { const el = document.getElementById("count-paused"); if (el) el.textContent = stats.paused; } if (stats.blocked !== undefined) { const el = document.getElementById("count-blocked"); if (el) el.textContent = stats.blocked; } if (stats.time_saved !== undefined) { const el = document.getElementById("time-saved-value"); if (el) el.textContent = stats.time_saved; } }) .catch((e) => console.warn("Failed to load task stats:", e)); } // Call updateFilterCounts on load document.addEventListener("DOMContentLoaded", updateFilterCounts); document.body.addEventListener("taskCreated", updateFilterCounts);