botui/ui/suite/tasks/tasks.html

617 lines
23 KiB
HTML
Raw Normal View History

<!-- =============================================================================
TASKS APP - Autonomous Task Management
Respects Theme Manager - No hardcoded theme
============================================================================= -->
<div class="tasks-app">
<!-- Hidden element to load stats on page load -->
<div
hx-get="/api/ui/tasks/stats"
hx-trigger="load, taskCreated from:body"
hx-swap="innerHTML"
style="display: none"
></div>
<!-- Status Filter Pills Row -->
<div class="status-filter-row">
<button
class="filter-pill"
data-filter="complete"
hx-get="/api/ui/tasks?filter=complete"
hx-target="#task-list"
hx-swap="innerHTML"
>
<span class="pill-icon"></span>
<span class="pill-label">Complete</span>
<span class="pill-count" id="count-complete">-</span>
</button>
<button
class="filter-pill active"
data-filter="all"
hx-get="/api/ui/tasks?filter=all"
hx-target="#task-list"
hx-swap="innerHTML"
>
<span class="pill-icon">📋</span>
<span class="pill-label">All Tasks</span>
<span class="pill-count" id="count-all">-</span>
</button>
<button
class="filter-pill"
data-filter="active"
hx-get="/api/ui/tasks?filter=active"
hx-target="#task-list"
hx-swap="innerHTML"
>
<span class="pill-icon"></span>
<span class="pill-label">Active Intents</span>
<span class="pill-count" id="count-active">-</span>
</button>
<button
class="filter-pill"
data-filter="awaiting"
hx-get="/api/ui/tasks?filter=awaiting"
hx-target="#task-list"
hx-swap="innerHTML"
>
<span class="pill-icon"></span>
<span class="pill-label">Awaiting Decision</span>
<span class="pill-count" id="count-awaiting">-</span>
</button>
<button
class="filter-pill"
data-filter="paused"
hx-get="/api/ui/tasks?filter=paused"
hx-target="#task-list"
hx-swap="innerHTML"
>
<span class="pill-icon"></span>
<span class="pill-label">Paused</span>
<span class="pill-count" id="count-paused">-</span>
</button>
<button
class="filter-pill"
data-filter="blocked"
hx-get="/api/ui/tasks?filter=blocked"
hx-target="#task-list"
hx-swap="innerHTML"
>
<span class="pill-icon"></span>
<span class="pill-label">Blocked/Issues</span>
<span class="pill-count" id="count-blocked">-</span>
</button>
<div class="time-saved-badge">
<span class="time-label">Active Time Saved:</span>
<span class="time-value" id="time-saved-value">-</span>
</div>
</div>
<!-- Quick Intent Input -->
<div class="quick-intent-bar">
<div class="intent-input-wrapper">
<input
type="text"
id="quick-intent-input"
name="intent"
class="quick-intent-input"
placeholder="What would you like to do? e.g., 'create a CRM app' or 'remind me to call John tomorrow'"
autocomplete="off"
/>
<button
id="quick-intent-btn"
class="btn-create-run"
hx-post="/api/ui/autotask/create"
hx-ext="json-enc"
hx-include="#quick-intent-input"
hx-target="#intent-result-hidden"
hx-swap="none"
hx-indicator="#intent-spinner"
hx-timeout="300000"
>
<span class="btn-text">Create & Run</span>
<span class="spinner" id="intent-spinner"></span>
</button>
</div>
<div id="intent-result" class="intent-result"></div>
<div id="intent-result-hidden" style="display: none"></div>
</div>
<!-- Main Two-Column Layout with Splitter -->
<main class="tasks-main">
<!-- Left Panel: Task Cards List -->
<section class="tasks-list-panel">
<div
class="tasks-list-scroll"
id="task-list"
hx-get="/api/ui/tasks?filter=all"
hx-trigger="load, taskCreated from:body throttle:2s"
hx-swap="innerHTML transition:false"
2025-12-03 18:42:22 -03:00
>
<!-- Loading state - replaced by HTMX -->
<div class="loading-state">
<div class="loading-spinner"></div>
<p>Loading tasks...</p>
</div>
</div>
</section>
<!-- Splitter -->
<div class="tasks-splitter" id="tasks-splitter"></div>
<!-- Right Panel: Task Detail -->
<aside class="task-detail-panel" id="task-detail-panel">
<!-- Detail content loaded dynamically -->
<div class="detail-empty" id="detail-empty">
<div class="empty-icon">📋</div>
<h3 class="empty-title">Select a task</h3>
<p class="empty-description">
Click on a task from the list to view details
</p>
<div class="empty-info">
<p>
<strong>Bot Database:</strong> All apps share the same
database tables.
</p>
<p>
<strong>Shared Resources:</strong> Schedulers, tools,
and monitors work across all apps.
</p>
</div>
</div>
<!-- Dynamic detail content -->
<div
id="task-detail-content"
style="display: none"
hx-get=""
hx-trigger="taskSelected from:body"
hx-swap="innerHTML"
>
<!-- Loaded via HTMX when task selected -->
</div>
</aside>
</main>
</div>
2025-12-03 18:42:22 -03:00
<!-- Floating Progress Panel - Shows live task generation progress -->
<div
class="floating-progress-panel"
id="floating-progress"
style="display: none"
>
<div class="floating-progress-header">
<div class="floating-progress-title">
<span class="progress-dot"></span>
<span id="floating-task-name">Processing...</span>
</div>
<div class="floating-progress-actions">
<button class="btn-minimize" onclick="minimizeFloatingProgress()">
</button>
<button class="btn-close-float" onclick="closeFloatingProgress()">
×
</button>
</div>
</div>
<div class="floating-progress-body">
<div class="floating-progress-bar">
<div
class="floating-progress-fill"
id="floating-progress-fill"
style="width: 0%"
></div>
</div>
<div class="floating-progress-info">
<span id="floating-progress-step">Starting...</span>
<span id="floating-progress-percent">0%</span>
</div>
<div class="floating-progress-log" id="floating-progress-log">
<!-- Live log entries appear here -->
</div>
<div class="floating-llm-terminal" id="floating-llm-terminal">
<!-- LLM streaming output appears here -->
</div>
</div>
</div>
<!-- Toast Container -->
<div class="toast-container" id="toast-container"></div>
<!-- New Intent Modal -->
<div class="modal" id="new-intent-modal" style="display: none">
<div class="modal-backdrop" onclick="closeNewIntentModal()"></div>
<div class="modal-content">
<div class="modal-header">
<h3>Create New Intent</h3>
<button class="btn-close" onclick="closeNewIntentModal()">×</button>
</div>
<div class="modal-body">
<form id="new-intent-form">
<div class="form-group">
<label for="intent-text"
>What would you like to accomplish?</label
>
<textarea
id="intent-text"
name="intent"
rows="4"
placeholder="Describe what you want to create or automate..."
></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="intent-priority">Priority</label>
<select id="intent-priority" name="priority">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
</div>
<div class="form-group">
<label for="intent-mode">Execution Mode</label>
<select id="intent-mode" name="mode">
<option value="auto">Automatic</option>
<option value="supervised">Supervised</option>
<option value="manual">Manual Approval</option>
</select>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeNewIntentModal()">
Cancel
</button>
<button class="btn-primary" onclick="submitNewIntent()">
Create & Run
</button>
</div>
</div>
</div>
<!-- Decision Modal -->
<div class="modal" id="decision-modal" style="display: none">
<div class="modal-backdrop" onclick="closeDecisionModal()"></div>
<div class="modal-content modal-lg">
<div class="modal-header">
<h3>Make Decision</h3>
<button class="btn-close" onclick="closeDecisionModal()">×</button>
</div>
<div class="modal-body">
<div class="decision-question" id="decision-question">
<h4>Decision Required</h4>
<p>Loading decision details...</p>
</div>
<div class="decision-options" id="decision-options">
<!-- Options loaded dynamically -->
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeDecisionModal()">
Cancel
</button>
<button class="btn-secondary" onclick="skipDecision()">
Skip for Now
</button>
<button class="btn-primary" onclick="submitDecision()">
Confirm Decision
</button>
</div>
</div>
</div>
<script>
// Initialize on load
document.addEventListener("DOMContentLoaded", function () {
if (typeof initTasksApp === "function") {
initTasksApp();
}
// Load stats
loadTaskStats();
// Handle create task response - auto-select new task and show progress
document.body.addEventListener("htmx:afterRequest", function (evt) {
// Check if this is the create task response
if (
evt.detail.pathInfo &&
evt.detail.pathInfo.requestPath === "/api/autotask/create"
) {
const xhr = evt.detail.xhr;
const intentResult = document.getElementById("intent-result");
if (xhr && xhr.status === 202) {
try {
const response = JSON.parse(xhr.responseText);
if (response.success && response.task_id) {
console.log(
"[TASK] Created task:",
response.task_id,
);
// Show success feedback
intentResult.innerHTML = `<span class="intent-success">✓ Task created - running...</span>`;
intentResult.style.display = "block";
// Clear the input
document.getElementById(
"quick-intent-input",
).value = "";
// Select the task and let tasks.js handle polling
selectTask(response.task_id);
// Hide success message after task is selected
setTimeout(function () {
intentResult.style.display = "none";
}, 2000);
} else {
intentResult.innerHTML = `<span class="intent-error">✗ ${response.message || "Failed to create task"}</span>`;
intentResult.style.display = "block";
}
} catch (e) {
console.warn("Failed to parse create response:", e);
intentResult.innerHTML = `<span class="intent-error">✗ Failed to parse response</span>`;
intentResult.style.display = "block";
}
} else if (xhr && xhr.status >= 400) {
try {
const response = JSON.parse(xhr.responseText);
intentResult.innerHTML = `<span class="intent-error">✗ ${response.error || response.message || "Error creating task"}</span>`;
} catch (e) {
intentResult.innerHTML = `<span class="intent-error">✗ Error: ${xhr.status}</span>`;
}
intentResult.style.display = "block";
}
}
});
});
// Load task statistics
function loadTaskStats() {
fetch("/api/tasks/stats/json")
.then((r) => r.json())
.then((stats) => {
if (stats.complete !== undefined)
document.getElementById("count-complete").textContent =
stats.complete;
if (stats.active !== undefined)
document.getElementById("count-active").textContent =
stats.active;
if (stats.awaiting !== undefined)
document.getElementById("count-awaiting").textContent =
stats.awaiting;
if (stats.paused !== undefined)
document.getElementById("count-paused").textContent =
stats.paused;
if (stats.blocked !== undefined)
document.getElementById("count-blocked").textContent =
stats.blocked;
if (stats.time_saved !== undefined)
document.getElementById("time-saved-value").textContent =
stats.time_saved;
})
.catch((e) => console.warn("Failed to load stats:", e));
}
// Filter pill click handler
document.querySelectorAll(".filter-pill").forEach((pill) => {
pill.addEventListener("click", function () {
document
.querySelectorAll(".filter-pill")
.forEach((p) => p.classList.remove("active"));
this.classList.add("active");
});
});
// Modal functions
function showNewIntentModal() {
document.getElementById("new-intent-modal").style.display = "flex";
}
function closeNewIntentModal() {
document.getElementById("new-intent-modal").style.display = "none";
}
function showDecisionModal(decision) {
if (decision) {
document.getElementById("decision-question").innerHTML = `
<h4>${decision.title || "Decision Required"}</h4>
<p>${decision.description || ""}</p>
`;
}
document.getElementById("decision-modal").style.display = "flex";
}
function closeDecisionModal() {
document.getElementById("decision-modal").style.display = "none";
}
function submitNewIntent() {
const form = document.getElementById("new-intent-form");
const intent = form.querySelector('[name="intent"]').value;
if (intent.trim()) {
document.getElementById("quick-intent-input").value = intent;
htmx.trigger(document.getElementById("quick-intent-btn"), "click");
closeNewIntentModal();
}
}
function submitDecision() {
// Implementation for submitting decision
closeDecisionModal();
htmx.trigger(document.body, "taskCreated");
}
function skipDecision() {
closeDecisionModal();
}
// Task selection
function selectTask(taskId) {
// Update selection UI
document.querySelectorAll(".task-card").forEach((card) => {
card.classList.remove("selected");
});
const selectedCard = document.querySelector(
`[data-task-id="${taskId}"]`,
);
if (selectedCard) {
selectedCard.classList.add("selected");
}
// Show detail panel
document.getElementById("detail-empty").style.display = "none";
const detailContent = document.getElementById("task-detail-content");
detailContent.style.display = "block";
// Load task details
htmx.ajax("GET", `/api/tasks/${taskId}`, {
target: "#task-detail-content",
swap: "innerHTML",
});
}
function deselectTask() {
document.querySelectorAll(".task-card").forEach((card) => {
card.classList.remove("selected");
});
document.getElementById("detail-empty").style.display = "flex";
document.getElementById("task-detail-content").style.display = "none";
}
// Floating Progress Panel Functions
function showFloatingProgress(taskName) {
let panel = document.getElementById("floating-progress");
document.getElementById("floating-task-name").textContent =
taskName || "Processing...";
document.getElementById("floating-progress-fill").style.width = "0%";
document.getElementById("floating-progress-step").textContent =
"Starting...";
document.getElementById("floating-progress-percent").textContent = "0%";
document.getElementById("floating-progress-log").innerHTML = "";
const dot = panel.querySelector(".progress-dot");
if (dot) dot.classList.remove("completed", "error");
panel.style.display = "block";
panel.classList.remove("minimized");
}
function updateFloatingProgress(step, message, current, total, details) {
const panel = document.getElementById("floating-progress");
if (panel.style.display === "none") {
showFloatingProgress(message);
}
const percent = total > 0 ? Math.round((current / total) * 100) : 0;
document.getElementById("floating-progress-fill").style.width =
percent + "%";
document.getElementById("floating-progress-step").textContent = message;
document.getElementById("floating-progress-percent").textContent =
percent + "%";
// Add log entry
if (step) {
const log = document.getElementById("floating-progress-log");
const entry = document.createElement("div");
entry.className = "log-entry";
const time = new Date().toLocaleTimeString();
entry.innerHTML = `<span class="log-time">${time}</span> <span class="log-step">[${step}]</span> ${message}`;
if (details) {
entry.innerHTML += `<br><span class="log-details">→ ${details}</span>`;
}
log.appendChild(entry);
log.scrollTop = log.scrollHeight;
}
}
function completeFloatingProgress(message) {
document.getElementById("floating-progress-fill").style.width = "100%";
document.getElementById("floating-progress-step").textContent =
message || "Completed!";
document.getElementById("floating-progress-percent").textContent =
"100%";
const panel = document.getElementById("floating-progress");
const dot = panel.querySelector(".progress-dot");
if (dot) dot.classList.add("completed");
// Refresh task list
htmx.trigger(document.body, "taskCreated");
loadTaskStats();
// Auto-hide after 5 seconds
setTimeout(closeFloatingProgress, 5000);
}
function errorFloatingProgress(errorMessage) {
document.getElementById("floating-progress-step").textContent =
"Error: " + errorMessage;
const panel = document.getElementById("floating-progress");
const dot = panel.querySelector(".progress-dot");
if (dot) dot.classList.add("error");
}
function minimizeFloatingProgress() {
document
.getElementById("floating-progress")
.classList.toggle("minimized");
}
function closeFloatingProgress() {
const panel = document.getElementById("floating-progress");
panel.style.display = "none";
const dot = panel.querySelector(".progress-dot");
if (dot) dot.classList.remove("completed", "error");
}
// Splitter drag functionality
(function initSplitter() {
const splitter = document.getElementById("tasks-splitter");
const main = document.querySelector(".tasks-main");
const leftPanel = document.querySelector(".tasks-list-panel");
if (!splitter || !main || !leftPanel) return;
let isDragging = false;
let startX, startWidth;
splitter.addEventListener("mousedown", function (e) {
isDragging = true;
startX = e.clientX;
startWidth = leftPanel.offsetWidth;
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
e.preventDefault();
});
document.addEventListener("mousemove", function (e) {
if (!isDragging) return;
const diff = e.clientX - startX;
const newWidth = Math.max(200, Math.min(600, startWidth + diff));
leftPanel.style.flex = `0 0 ${newWidth}px`;
leftPanel.style.width = `${newWidth}px`;
});
document.addEventListener("mouseup", function () {
if (isDragging) {
isDragging = false;
document.body.style.cursor = "";
document.body.style.userSelect = "";
}
});
})();
// Listen for HTMX events to refresh stats
document.body.addEventListener("htmx:afterSwap", function (e) {
if (e.detail.target.id === "task-list") {
loadTaskStats();
// Show empty state if no tasks
const taskList = document.getElementById("task-list");
const emptyState = document.getElementById("empty-state");
if (taskList && emptyState) {
const hasTasks = taskList.querySelector(".task-card");
emptyState.style.display = hasTasks ? "none" : "flex";
}
}
});
</script>