botui/ui/suite/tasks/tasks.js

660 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* =============================================================================
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,
};
}
// =============================================================================
// INITIALIZATION
// =============================================================================
document.addEventListener("DOMContentLoaded", function () {
initTasksApp();
});
function initTasksApp() {
// Initialize WebSocket for real-time updates
initWebSocket();
// Setup event listeners
setupEventListeners();
// Setup keyboard shortcuts
setupKeyboardShortcuts();
// Auto-scroll agent log to bottom
scrollAgentLogToBottom();
console.log("[Tasks] Initialized");
}
// =============================================================================
// WEBSOCKET CONNECTION
// =============================================================================
function initWebSocket() {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/ws/tasks`;
try {
TasksState.wsConnection = new WebSocket(wsUrl);
TasksState.wsConnection.onopen = function () {
console.log("[Sentient Tasks] WebSocket connected");
addAgentLog("info", "[SYSTEM] Connected to task orchestrator");
};
TasksState.wsConnection.onmessage = function (event) {
handleWebSocketMessage(JSON.parse(event.data));
};
TasksState.wsConnection.onclose = function () {
console.log("[Sentient Tasks] WebSocket disconnected, reconnecting...");
setTimeout(initWebSocket, 5000);
};
TasksState.wsConnection.onerror = function (error) {
console.error("[Sentient Tasks] WebSocket error:", error);
};
} catch (e) {
console.warn("[Sentient Tasks] WebSocket not available");
}
}
function handleWebSocketMessage(data) {
switch (data.type) {
case "task_update":
updateTaskCard(data.task);
if (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 "task_completed":
onTaskCompleted(data.task);
break;
case "task_failed":
onTaskFailed(data.task, data.error);
break;
}
}
// =============================================================================
// 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) {
// In real app, fetch from API
// htmx.ajax('GET', `/api/tasks/${taskId}`, {target: '#task-detail-panel', swap: 'innerHTML'});
addAgentLog("info", `[LOAD] Loading task #${taskId} details`);
}
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 = `
<span class="activity-timestamp">${timestamp}</span>
<span class="activity-message">${message}</span>
`;
// 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 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 = `
<span style="font-size: 16px;">${icons[type] || icons.info}</span>
<span>${message}</span>
`;
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);
// =============================================================================
// DEMO: Simulate real-time activity
// =============================================================================
// Simulate agent activity for demo
setInterval(() => {
if (Math.random() > 0.7) {
const messages = [
{ level: "info", msg: "[SCAN] Monitoring task queues..." },
{ level: "info", msg: "[AGENT] Processing next batch..." },
{ level: "success", msg: "[OK] Checkpoint saved" },
{ level: "info", msg: "[SYNC] Synchronizing state..." },
];
const { level, msg } =
messages[Math.floor(Math.random() * messages.length)];
addAgentLog(level, msg);
}
}, 5000);