botui/ui/suite/tasks/tasks.html

614 lines
23 KiB
HTML
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 - 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/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/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/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/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/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/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/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/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/tasks?filter=all"
hx-trigger="load, taskCreated from:body throttle:2s"
hx-swap="innerHTML transition:false"
>
<!-- 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>
<!-- 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="normal">Normal</option>
<option value="high">High</option>
<option value="low">Low</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));
main.style.gridTemplateColumns = `${newWidth}px 6px 1fr`;
});
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>