Fix tasks UI and add WebSocket proxy for task progress

Tasks UI:
- Add proper CSS for task cards with status-based colors
- Add task detail panel CSS for header, progress, sections
- Remove duplicate empty state div
- Add All Tasks filter pill
- Fix updateFilterCounts to use /api/tasks/stats/json
- Remove fake demo agent activity simulation
- Add detailed console logging for WebSocket debugging
- Update floating progress panel with terminal-style metrics

WebSocket proxy:
- Add /ws/task-progress proxy route without required params
- Add handle_task_progress_ws_proxy for task progress WebSocket
- Support optional task_id filtering
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-12-30 22:42:54 -03:00
parent 3f95c4645d
commit 4a3eb0cc4f
12 changed files with 7482 additions and 3842 deletions

View file

@ -244,6 +244,13 @@ struct WsQuery {
user_id: String, user_id: String,
} }
#[derive(Debug, Default, Deserialize)]
struct OptionalWsQuery {
session_id: Option<String>,
user_id: Option<String>,
task_id: Option<String>,
}
async fn ws_proxy( async fn ws_proxy(
ws: WebSocketUpgrade, ws: WebSocketUpgrade,
State(state): State<AppState>, State(state): State<AppState>,
@ -252,6 +259,140 @@ async fn ws_proxy(
ws.on_upgrade(move |socket| handle_ws_proxy(socket, state, params)) ws.on_upgrade(move |socket| handle_ws_proxy(socket, state, params))
} }
async fn ws_task_progress_proxy(
ws: WebSocketUpgrade,
State(state): State<AppState>,
Query(params): Query<OptionalWsQuery>,
) -> impl IntoResponse {
ws.on_upgrade(move |socket| handle_task_progress_ws_proxy(socket, state, params))
}
async fn handle_task_progress_ws_proxy(
client_socket: WebSocket,
state: AppState,
params: OptionalWsQuery,
) {
let mut backend_url = format!(
"{}/ws/task-progress",
state
.client
.base_url()
.replace("https://", "wss://")
.replace("http://", "ws://"),
);
if let Some(task_id) = &params.task_id {
backend_url = format!("{}/{}", backend_url, task_id);
}
info!("Proxying task-progress WebSocket to: {backend_url}");
let Ok(tls_connector) = native_tls::TlsConnector::builder()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true)
.build()
else {
error!("Failed to build TLS connector for task-progress");
return;
};
let connector = tokio_tungstenite::Connector::NativeTls(tls_connector);
let backend_result =
connect_async_tls_with_config(&backend_url, None, false, Some(connector)).await;
let backend_socket = match backend_result {
Ok((socket, _)) => socket,
Err(e) => {
error!("Failed to connect to backend task-progress WebSocket: {e}");
return;
}
};
info!("Connected to backend task-progress WebSocket");
let (mut client_tx, mut client_rx) = client_socket.split();
let (mut backend_tx, mut backend_rx) = backend_socket.split();
let client_to_backend = async {
while let Some(msg) = client_rx.next().await {
match msg {
Ok(AxumMessage::Text(text)) => {
if backend_tx
.send(TungsteniteMessage::Text(text))
.await
.is_err()
{
break;
}
}
Ok(AxumMessage::Binary(data)) => {
if backend_tx
.send(TungsteniteMessage::Binary(data))
.await
.is_err()
{
break;
}
}
Ok(AxumMessage::Ping(data)) => {
if backend_tx
.send(TungsteniteMessage::Ping(data))
.await
.is_err()
{
break;
}
}
Ok(AxumMessage::Pong(data)) => {
if backend_tx
.send(TungsteniteMessage::Pong(data))
.await
.is_err()
{
break;
}
}
Ok(AxumMessage::Close(_)) | Err(_) => break,
}
}
};
let backend_to_client = async {
while let Some(msg) = backend_rx.next().await {
match msg {
Ok(TungsteniteMessage::Text(text)) => {
if client_tx.send(AxumMessage::Text(text)).await.is_err() {
break;
}
}
Ok(TungsteniteMessage::Binary(data)) => {
if client_tx.send(AxumMessage::Binary(data)).await.is_err() {
break;
}
}
Ok(TungsteniteMessage::Ping(data)) => {
if client_tx.send(AxumMessage::Ping(data)).await.is_err() {
break;
}
}
Ok(TungsteniteMessage::Pong(data)) => {
if client_tx.send(AxumMessage::Pong(data)).await.is_err() {
break;
}
}
Ok(TungsteniteMessage::Close(_)) | Err(_) => break,
Ok(_) => {}
}
}
};
tokio::select! {
() = client_to_backend => info!("Task-progress client connection closed"),
() = backend_to_client => info!("Task-progress backend connection closed"),
}
}
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQuery) { async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQuery) {
let backend_url = format!( let backend_url = format!(
@ -374,7 +515,10 @@ async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQu
} }
fn create_ws_router() -> Router<AppState> { fn create_ws_router() -> Router<AppState> {
Router::new().fallback(any(ws_proxy)) Router::new()
.route("/task-progress", get(ws_task_progress_proxy))
.route("/task-progress/{task_id}", get(ws_task_progress_proxy))
.fallback(any(ws_proxy))
} }
fn create_apps_router() -> Router<AppState> { fn create_apps_router() -> Router<AppState> {

View file

@ -1,4 +1,4 @@
/* Drive Module Styles */ /* Drive Module Styles v1.0 */
.drive-container { .drive-container {
display: flex; display: flex;
@ -75,6 +75,12 @@
color: var(--text-secondary); color: var(--text-secondary);
} }
.storage-detail {
font-size: 11px;
color: var(--text-secondary);
margin-top: 4px;
}
/* Drive Main */ /* Drive Main */
.drive-main { .drive-main {
flex: 1; flex: 1;
@ -295,7 +301,7 @@
.file-list-header { .file-list-header {
display: grid; display: grid;
grid-template-columns: 1fr 120px 120px 80px; grid-template-columns: 1fr 120px 100px 80px;
gap: 16px; gap: 16px;
padding: 12px 20px; padding: 12px 20px;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
@ -305,31 +311,66 @@
text-transform: uppercase; text-transform: uppercase;
} }
.file-list-item { .file-list-header .file-col {
display: flex;
align-items: center;
}
.file-list-item,
.drive-file-item {
display: grid; display: grid;
grid-template-columns: 1fr 120px 120px 80px; grid-template-columns: 1fr 120px 100px 80px;
gap: 16px; gap: 16px;
padding: 12px 20px; padding: 12px 20px;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
cursor: pointer; cursor: pointer;
transition: background 0.15s; transition: background 0.15s;
align-items: center;
} }
.file-list-item:hover { .file-list-item:hover,
.drive-file-item:hover {
background: var(--surface-hover); background: var(--surface-hover);
} }
.file-list-name { .file-list-item.selected,
.drive-file-item.selected {
background: var(--primary-light, rgba(99, 102, 241, 0.1));
}
.file-list-name,
.file-col.file-name-col {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
font-size: 14px; font-size: 14px;
min-width: 0;
} }
.file-list-name svg { .file-list-name svg,
.file-col.file-name-col svg {
flex-shrink: 0; flex-shrink: 0;
} }
.file-col.file-name-col span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-col.file-modified-col,
.file-col.file-size-col {
font-size: 13px;
color: var(--text-secondary);
}
.file-col.file-actions-col {
display: flex;
align-items: center;
gap: 4px;
justify-content: flex-end;
}
/* Upload Zone */ /* Upload Zone */
.upload-zone { .upload-zone {
border: 2px dashed var(--border); border: 2px dashed var(--border);
@ -361,39 +402,579 @@
color: var(--text-secondary); color: var(--text-secondary);
} }
/* Search Box */
.drive-toolbar-center {
flex: 1;
display: flex;
justify-content: center;
padding: 0 20px;
}
.search-box {
position: relative;
width: 100%;
max-width: 400px;
}
.search-box svg {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--text-secondary);
}
.search-box input {
width: 100%;
padding: 8px 12px 8px 40px;
border: 1px solid var(--border, #2a2a3a);
border-radius: 8px;
background: var(--surface, #12121a);
color: var(--text, #ffffff);
font-size: 14px;
}
.search-box input:focus {
outline: none;
border-color: var(--primary, #3b82f6);
}
.search-box input::placeholder {
color: var(--text-secondary, #6b6b80);
}
/* Selection Bar */
.selection-bar {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 16px;
padding: 12px 20px;
background: var(--surface, #12121a);
border: 1px solid var(--border, #2a2a3a);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
z-index: 100;
}
.selection-count {
font-size: 14px;
font-weight: 500;
color: var(--text, #ffffff);
}
.selection-actions {
display: flex;
align-items: center;
gap: 8px;
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border: none;
border-radius: 8px;
background: transparent;
color: var(--text-secondary, #6b6b80);
cursor: pointer;
transition: all 0.15s;
}
.action-btn:hover {
background: var(--surface-hover, rgba(255, 255, 255, 0.1));
color: var(--text, #ffffff);
}
.action-btn.danger:hover {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.close-selection {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
border-radius: 6px;
background: transparent;
color: var(--text-secondary, #6b6b80);
cursor: pointer;
margin-left: 8px;
}
.close-selection:hover {
background: var(--surface-hover);
color: var(--text);
}
/* Loading State */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: var(--text-secondary, #6b6b80);
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid var(--border, #2a2a3a);
border-top-color: var(--primary, #3b82f6);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Empty State */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
color: var(--text-secondary, #6b6b80);
}
.empty-state svg {
margin-bottom: 20px;
opacity: 0.5;
}
.empty-state h3 {
font-size: 18px;
font-weight: 600;
color: var(--text, #ffffff);
margin-bottom: 8px;
}
.empty-state p {
font-size: 14px;
margin-bottom: 20px;
}
.empty-state .btn-primary {
padding: 10px 20px;
border: none;
border-radius: 8px;
background: var(--primary, #3b82f6);
color: white;
font-size: 14px;
cursor: pointer;
}
.empty-state .btn-primary:hover {
opacity: 0.9;
}
/* Drop Overlay */
.drop-overlay {
position: absolute;
inset: 0;
display: none;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.7);
z-index: 200;
}
.drop-overlay.visible {
display: flex;
}
.drop-zone {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 60px;
border: 3px dashed var(--primary, #3b82f6);
border-radius: 16px;
background: rgba(59, 130, 246, 0.1);
color: var(--text, #ffffff);
}
.drop-zone svg {
margin-bottom: 16px;
color: var(--primary, #3b82f6);
}
.drop-zone p {
font-size: 16px;
font-weight: 500;
}
/* File Checkbox */
.file-checkbox {
width: 16px;
height: 16px;
margin-right: 8px;
cursor: pointer;
accent-color: var(--primary, #3b82f6);
}
/* Button Icon Small */
.btn-icon-sm {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
border-radius: 6px;
background: transparent;
color: var(--text-secondary, #6b6b80);
cursor: pointer;
font-size: 14px;
transition: all 0.15s;
}
.btn-icon-sm:hover {
background: var(--surface-hover, rgba(255, 255, 255, 0.1));
color: var(--text, #ffffff);
}
/* Drive Container Position Relative for overlay */
.drive-container {
position: relative;
}
/* Notification Animation */
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Sidebar Actions */
.drive-sidebar-actions {
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.btn-primary-full,
.btn-secondary-full {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 16px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
}
.btn-primary-full {
background: var(--primary, #3b82f6);
color: white;
}
.btn-primary-full:hover {
opacity: 0.9;
}
.btn-secondary-full {
background: var(--surface-hover, rgba(255, 255, 255, 0.1));
color: var(--text, #ffffff);
border: 1px solid var(--border, #2a2a3a);
}
.btn-secondary-full:hover {
background: rgba(255, 255, 255, 0.15);
}
/* File Card Info */
.file-card-info {
text-align: center;
}
.file-card-meta {
font-size: 11px;
color: var(--text-secondary, #6b6b80);
margin-top: 4px;
color: var(--text-secondary);
}
/* Context Menu */ /* Context Menu */
.context-menu { .context-menu {
position: fixed; position: fixed;
background: var(--surface); background: var(--surface);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 8px; border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); box-shadow:
min-width: 180px; 0 12px 40px rgba(0, 0, 0, 0.2),
0 4px 12px rgba(0, 0, 0, 0.1);
min-width: 200px;
z-index: 1000; z-index: 1000;
padding: 6px;
backdrop-filter: blur(12px);
animation: contextMenuIn 0.15s ease-out;
transform-origin: top left;
}
@keyframes contextMenuIn {
from {
opacity: 0;
transform: scale(0.95) translateY(-4px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.context-menu.hidden {
display: none !important;
} }
.context-menu-item { .context-menu-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
padding: 10px 16px; padding: 10px 14px;
font-size: 14px; font-size: 14px;
font-weight: 500;
cursor: pointer; cursor: pointer;
transition: background 0.15s; transition: all 0.15s ease;
border-radius: 8px;
color: var(--text);
}
.context-menu-item svg {
flex-shrink: 0;
opacity: 0.7;
transition:
opacity 0.15s ease,
transform 0.15s ease;
} }
.context-menu-item:hover { .context-menu-item:hover {
background: var(--surface-hover); background: linear-gradient(
135deg,
var(--primary-light, rgba(99, 102, 241, 0.1)) 0%,
var(--surface-hover, rgba(99, 102, 241, 0.05)) 100%
);
color: var(--primary, #6366f1);
}
.context-menu-item:hover svg {
opacity: 1;
transform: scale(1.1);
stroke: var(--primary, #6366f1);
}
.context-menu-item:active {
transform: scale(0.98);
} }
.context-menu-item.danger { .context-menu-item.danger {
color: var(--error); color: var(--error, #ef4444);
}
.context-menu-item.danger svg {
stroke: var(--error, #ef4444);
}
.context-menu-item.danger:hover {
background: linear-gradient(
135deg,
rgba(239, 68, 68, 0.15) 0%,
rgba(239, 68, 68, 0.05) 100%
);
color: var(--error, #ef4444);
}
.context-menu-item.danger:hover svg {
stroke: var(--error, #ef4444);
} }
.context-menu-divider { .context-menu-divider {
height: 1px; height: 1px;
background: var(--border); background: linear-gradient(
margin: 4px 0; 90deg,
transparent 0%,
var(--border) 20%,
var(--border) 80%,
transparent 100%
);
margin: 6px 8px;
}
/* Inline Editor Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
padding: 20px;
}
.modal-content {
background: var(--surface, #12121a);
border-radius: 12px;
border: 1px solid var(--border, #2a2a3a);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
max-width: 100%;
max-height: 100%;
display: flex;
flex-direction: column;
}
.editor-modal-content {
width: 90vw;
height: 85vh;
max-width: 1200px;
}
.editor-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid var(--border, #2a2a3a);
background: var(--bg-secondary, #0d0d12);
border-radius: 12px 12px 0 0;
}
.editor-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
font-weight: 500;
}
.editor-icon {
font-size: 18px;
}
.editor-filename {
color: var(--text, #ffffff);
}
.editor-status {
color: var(--warning, #f59e0b);
font-size: 12px;
margin-left: 8px;
}
.editor-actions {
display: flex;
gap: 8px;
}
.editor-body {
flex: 1;
display: flex;
overflow: hidden;
min-height: 0;
}
.editor-textarea {
flex: 1;
width: 100%;
height: 100%;
padding: 16px;
border: none;
background: var(--bg, #0a0a0f);
color: var(--text, #ffffff);
font-family: "JetBrains Mono", "Fira Code", "Monaco", "Consolas", monospace;
font-size: 14px;
line-height: 1.6;
resize: none;
outline: none;
tab-size: 2;
}
.editor-textarea:focus {
outline: none;
}
.editor-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
border-top: 1px solid var(--border, #2a2a3a);
background: var(--bg-secondary, #0d0d12);
border-radius: 0 0 12px 12px;
font-size: 12px;
color: var(--text-secondary, #6b6b80);
}
.editor-info {
display: flex;
gap: 16px;
}
/* Button styles for editor */
.btn-primary {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: var(--primary, #3b82f6);
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.15s;
}
.btn-primary:hover {
background: var(--primary-hover, #2563eb);
}
.btn-secondary {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: transparent;
color: var(--text-secondary, #6b6b80);
border: 1px solid var(--border, #2a2a3a);
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.15s;
}
.btn-secondary:hover {
background: var(--surface-hover, #1a1a24);
color: var(--text, #ffffff);
} }
/* Responsive */ /* Responsive */
@ -407,4 +988,15 @@
padding: 12px; padding: 12px;
gap: 12px; gap: 12px;
} }
.editor-modal-content {
width: 100vw;
height: 100vh;
border-radius: 0;
}
.editor-header,
.editor-footer {
border-radius: 0;
}
} }

View file

@ -1,4 +1,4 @@
<!-- Drive - File Management --> <!-- Drive - File Management v1.0 -->
<link rel="stylesheet" href="drive/drive.css" /> <link rel="stylesheet" href="drive/drive.css" />
<div class="drive-container" id="drive-app"> <div class="drive-container" id="drive-app">
@ -22,11 +22,7 @@
<!-- Quick Actions --> <!-- Quick Actions -->
<div class="drive-sidebar-actions"> <div class="drive-sidebar-actions">
<button <button class="btn-primary-full" id="upload-btn">
class="btn-primary-full"
id="upload-btn"
onclick="uploadFile()"
>
<svg <svg
width="16" width="16"
height="16" height="16"
@ -41,11 +37,7 @@
</svg> </svg>
<span>Upload</span> <span>Upload</span>
</button> </button>
<button <button class="btn-secondary-full" id="new-folder-btn">
class="btn-secondary-full"
id="new-folder-btn"
onclick="createFolder()"
>
<svg <svg
width="16" width="16"
height="16" height="16"
@ -147,9 +139,14 @@
<!-- Storage --> <!-- Storage -->
<div class="drive-storage"> <div class="drive-storage">
<div class="storage-bar"> <div class="storage-bar">
<div class="storage-fill" style="width: 62%"></div> <div
class="storage-fill"
id="storage-fill"
style="width: 0%"
></div>
</div> </div>
<div class="storage-text">12.4 GB of 20 GB used</div> <div class="storage-text" id="storage-used">Loading storage...</div>
<div class="storage-detail" id="storage-detail"></div>
</div> </div>
</aside> </aside>
@ -158,15 +155,40 @@
<!-- Toolbar --> <!-- Toolbar -->
<div class="drive-toolbar"> <div class="drive-toolbar">
<div class="drive-toolbar-left"> <div class="drive-toolbar-left">
<select
id="bucket-selector"
class="bucket-selector"
style="display: none"
>
<!-- Populated by JavaScript -->
</select>
<div class="drive-breadcrumb"> <div class="drive-breadcrumb">
<button <button
class="breadcrumb-item" class="breadcrumb-item"
onclick="navigateTo('root')" onclick="DriveModule.loadFiles('')"
> >
My Drive My Drive
</button> </button>
<span class="breadcrumb-sep">/</span> </div>
<span class="breadcrumb-current">Projects</span> </div>
<div class="drive-toolbar-center">
<div class="search-box">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.3-4.3"></path>
</svg>
<input
type="text"
id="search-input"
placeholder="Search in Drive"
/>
</div> </div>
</div> </div>
<div class="drive-toolbar-right"> <div class="drive-toolbar-right">
@ -213,262 +235,130 @@
</div> </div>
</div> </div>
<!-- File List --> <!-- File Content Area - Populated by JavaScript -->
<div class="drive-content" id="drive-content"> <div class="drive-content" id="drive-content">
<!-- List Header --> <div class="loading-state">
<div class="drive-list-header"> <div class="spinner"></div>
<div class="file-col file-name-col">Name</div> <p>Loading files...</p>
<div class="file-col file-modified-col">Modified</div>
<div class="file-col file-size-col">Size</div>
<div class="file-col file-actions-col"></div>
</div>
<!-- Folders -->
<div
class="drive-file-item folder"
data-id="1"
onclick="openFolder(this)"
hx-get="/api/drive/folder/1"
hx-target="#drive-content"
hx-swap="innerHTML"
>
<div class="file-col file-name-col">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="#5f6368"
stroke="none"
>
<path
d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"
/>
</svg>
<span>Documents</span>
</div>
<div class="file-col file-modified-col">Dec 15, 2024</div>
<div class="file-col file-size-col"></div>
<div class="file-col file-actions-col">
<button class="btn-icon-sm" title="More options"></button>
</div> </div>
</div> </div>
<div <!-- Selection Bar -->
class="drive-file-item folder" <div class="selection-bar" id="selection-bar" style="display: none">
data-id="2" <span class="selection-count"
onclick="openFolder(this)" ><span id="selected-count">0</span> selected</span
>
<div class="selection-actions">
<button
class="action-btn"
onclick="DriveModule.copySelected()"
title="Copy"
> >
<div class="file-col file-name-col">
<svg <svg
width="20" width="16"
height="20" height="16"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="#5f6368" fill="none"
stroke="none" stroke="currentColor"
stroke-width="2"
> >
<rect
x="9"
y="9"
width="13"
height="13"
rx="2"
ry="2"
></rect>
<path <path
d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
/> ></path>
</svg> </svg>
<span>Images</span> </button>
</div> <button
<div class="file-col file-modified-col">Dec 14, 2024</div> class="action-btn"
<div class="file-col file-size-col"></div> onclick="DriveModule.cutSelected()"
<div class="file-col file-actions-col"> title="Cut"
<button class="btn-icon-sm" title="More options"></button>
</div>
</div>
<div
class="drive-file-item folder"
data-id="3"
onclick="openFolder(this)"
> >
<div class="file-col file-name-col">
<svg <svg
width="20" width="16"
height="20" height="16"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="#5f6368" fill="none"
stroke="none" stroke="currentColor"
stroke-width="2"
> >
<path <circle cx="6" cy="6" r="3"></circle>
d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" <circle cx="6" cy="18" r="3"></circle>
/> <line x1="20" y1="4" x2="8.12" y2="15.88"></line>
<line x1="14.47" y1="14.48" x2="20" y2="20"></line>
<line x1="8.12" y1="8.12" x2="12" y2="12"></line>
</svg> </svg>
<span>Source Code</span> </button>
</div> <button
<div class="file-col file-modified-col">Dec 12, 2024</div> class="action-btn danger"
<div class="file-col file-size-col"></div> onclick="DriveModule.deleteSelected()"
<div class="file-col file-actions-col"> title="Delete"
<button class="btn-icon-sm" title="More options"></button>
</div>
</div>
<!-- Files -->
<div
class="drive-file-item"
data-id="10"
onclick="selectFile(this)"
> >
<div class="file-col file-name-col">
<svg <svg
width="20" width="16"
height="20" height="16"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="#ea4335" fill="none"
stroke="none" stroke="currentColor"
stroke-width="2"
> >
<polyline points="3 6 5 6 21 6"></polyline>
<path <path
d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z" d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"
/> ></path>
</svg> </svg>
<span>project-report.pdf</span> </button>
</div> <button
<div class="file-col file-modified-col">Dec 15, 2024</div> class="close-selection"
<div class="file-col file-size-col">2.4 MB</div> onclick="DriveModule.clearSelection()"
<div class="file-col file-actions-col"> title="Clear selection"
<button class="btn-icon-sm" title="Download"></button>
<button class="btn-icon-sm" title="More options"></button>
</div>
</div>
<div
class="drive-file-item"
data-id="11"
onclick="selectFile(this)"
> >
<div class="file-col file-name-col">
<svg <svg
width="20" width="16"
height="20" height="16"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="#4285f4" fill="none"
stroke="none" stroke="currentColor"
stroke-width="2"
> >
<path <line x1="18" y1="6" x2="6" y2="18"></line>
d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z" <line x1="6" y1="6" x2="18" y2="18"></line>
/>
</svg> </svg>
<span>meeting-notes.docx</span> </button>
</div>
<div class="file-col file-modified-col">Dec 14, 2024</div>
<div class="file-col file-size-col">156 KB</div>
<div class="file-col file-actions-col">
<button class="btn-icon-sm" title="Download"></button>
<button class="btn-icon-sm" title="More options"></button>
</div>
</div>
<div
class="drive-file-item"
data-id="12"
onclick="selectFile(this)"
>
<div class="file-col file-name-col">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="#0f9d58"
stroke="none"
>
<path
d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"
/>
</svg>
<span>analytics-2025.xlsx</span>
</div>
<div class="file-col file-modified-col">Dec 13, 2024</div>
<div class="file-col file-size-col">890 KB</div>
<div class="file-col file-actions-col">
<button class="btn-icon-sm" title="Download"></button>
<button class="btn-icon-sm" title="More options"></button>
</div>
</div>
<div
class="drive-file-item"
data-id="13"
onclick="selectFile(this)"
>
<div class="file-col file-name-col">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="#fbbc04"
stroke="none"
>
<path
d="M8.5,13.5L11,16.5L14.5,12L19,18H5M21,19V5C21,3.89 20.1,3 19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19Z"
/>
</svg>
<span>dashboard-mockup.png</span>
</div>
<div class="file-col file-modified-col">Dec 12, 2024</div>
<div class="file-col file-size-col">1.8 MB</div>
<div class="file-col file-actions-col">
<button class="btn-icon-sm" title="Download"></button>
<button class="btn-icon-sm" title="More options"></button>
</div>
</div>
<div
class="drive-file-item"
data-id="14"
onclick="selectFile(this)"
>
<div class="file-col file-name-col">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="#ea4335"
stroke="none"
>
<path
d="M18,4L20,8H17L15,4H13L15,8H12L10,4H8L10,8H7L5,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V4H18Z"
/>
</svg>
<span>demo-video.mp4</span>
</div>
<div class="file-col file-modified-col">Dec 10, 2024</div>
<div class="file-col file-size-col">125 MB</div>
<div class="file-col file-actions-col">
<button class="btn-icon-sm" title="Download"></button>
<button class="btn-icon-sm" title="More options"></button>
</div>
</div>
<div
class="drive-file-item"
data-id="15"
onclick="selectFile(this)"
>
<div class="file-col file-name-col">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="#5f6368"
stroke="none"
>
<path
d="M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-6 10H6v-2h8v2zm4-4H6v-2h12v2z"
/>
</svg>
<span>release-v2.0.zip</span>
</div>
<div class="file-col file-modified-col">Dec 8, 2024</div>
<div class="file-col file-size-col">45 MB</div>
<div class="file-col file-actions-col">
<button class="btn-icon-sm" title="Download"></button>
<button class="btn-icon-sm" title="More options"></button>
</div>
</div> </div>
</div> </div>
</main> </main>
<!-- Drop Overlay for Drag & Drop -->
<div class="drop-overlay" id="drop-overlay">
<div class="drop-zone">
<svg
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
<p>Drop files here to upload</p>
</div>
</div>
</div> </div>
<!-- Hidden file input for uploads -->
<input type="file" id="file-input" multiple style="display: none" />
<!-- Context Menu (dynamically populated by JS) -->
<div id="context-menu" class="context-menu hidden"></div>
<script src="drive/drive.js"></script> <script src="drive/drive.js"></script>

File diff suppressed because it is too large Load diff

View file

@ -718,6 +718,13 @@
</svg> </svg>
Open Open
</button> </button>
<button class="context-item" data-action="edit" onclick="document.getElementById('context-menu').style.display='none'; if(window.contextTarget) DriveModule.openInlineEditor(window.contextTarget.dataset.path);">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
Edit
</button>
<button class="context-item" data-action="preview"> <button class="context-item" data-action="preview">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path> <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
@ -1857,13 +1864,13 @@
} }
// Context menu handling // Context menu handling
let contextTarget = null; window.contextTarget = null;
document.addEventListener('contextmenu', function(e) { document.addEventListener('contextmenu', function(e) {
const fileCard = e.target.closest('.file-card'); const fileCard = e.target.closest('.file-card');
if (fileCard) { if (fileCard) {
e.preventDefault(); e.preventDefault();
contextTarget = fileCard; window.contextTarget = fileCard;
const menu = document.getElementById('context-menu'); const menu = document.getElementById('context-menu');
menu.style.display = 'block'; menu.style.display = 'block';
menu.style.left = e.pageX + 'px'; menu.style.left = e.pageX + 'px';

View file

@ -25,7 +25,7 @@
<link rel="stylesheet" href="meet/meet.css" /> <link rel="stylesheet" href="meet/meet.css" />
<link rel="stylesheet" href="paper/paper.css" /> <link rel="stylesheet" href="paper/paper.css" />
<link rel="stylesheet" href="research/research.css" /> <link rel="stylesheet" href="research/research.css" />
<link rel="stylesheet" href="tasks/tasks.css" /> <link rel="stylesheet" href="tasks/tasks.css?v=20251230" />
<link rel="stylesheet" href="analytics/analytics.css" /> <link rel="stylesheet" href="analytics/analytics.css" />
<link rel="stylesheet" href="monitoring/monitoring.css" /> <link rel="stylesheet" href="monitoring/monitoring.css" />

File diff suppressed because it is too large Load diff

View file

@ -1,492 +1,479 @@
<div class="autotask-container"> <div class="autotask-container" data-theme="sentient">
<!-- Header --> <!-- Top Navigation Bar -->
<div class="autotask-header"> <div class="autotask-topbar">
<div class="header-content"> <div class="topbar-left">
<h1 class="autotask-title"> <div class="nav-tabs">
<span class="autotask-icon"></span> <button class="nav-tab active">Dashboard</button>
Auto Tasks <button class="nav-tab">Analytics</button>
<span class="beta-badge">PREMIUM</span> </div>
</h1> </div>
<p class="autotask-subtitle">Intelligent self-executing tasks powered by AI</p> <div class="topbar-center">
<div class="search-container">
<span class="search-icon">🔍</span>
<input
type="text"
class="search-input"
placeholder="Search Intents"
id="intent-search"
/>
</div>
</div>
<div class="topbar-right">
<button class="btn-new-intent" onclick="showNewIntentModal()">
<span>New Intent</span>
</button>
<button class="btn-settings">Settings</button>
<div class="user-profile">
<div class="user-avatar">👤</div>
<span class="user-name">Profile 1</span>
<span class="dropdown-arrow"></span>
</div>
<div class="notification-bell">
<span class="bell-icon">🔔</span>
<span class="notification-count" id="notification-count"
>3</span
>
</div> </div>
<div class="header-stats" id="autotask-stats">
<span class="stat-item">
<span class="stat-value" id="stat-running">0</span>
<span class="stat-label">Running</span>
</span>
<span class="stat-item">
<span class="stat-value" id="stat-pending">0</span>
<span class="stat-label">Pending</span>
</span>
<span class="stat-item">
<span class="stat-value" id="stat-completed">0</span>
<span class="stat-label">Completed</span>
</span>
<span class="stat-item highlight">
<span class="stat-value" id="stat-approval">0</span>
<span class="stat-label">Need Approval</span>
</span>
</div> </div>
</div> </div>
<!-- Intent Input Section --> <!-- Status Filter Pills -->
<div class="intent-section"> <div class="status-filters">
<div class="intent-header"> <button
<span class="section-icon">💡</span> class="status-filter"
<h2>What would you like to accomplish?</h2> data-filter="complete"
onclick="filterByStatus('complete', this)"
>
<span class="filter-icon"></span>
<span class="filter-label">Complete</span>
<span class="filter-count" id="count-complete">8</span>
</button>
<button
class="status-filter active"
data-filter="active"
onclick="filterByStatus('active', this)"
>
<span class="filter-icon"></span>
<span class="filter-label">Active Intents</span>
<span class="filter-count" id="count-active">12</span>
</button>
<button
class="status-filter"
data-filter="awaiting"
onclick="filterByStatus('awaiting', this)"
>
<span class="filter-icon"></span>
<span class="filter-label">Awaiting Decision</span>
<span class="filter-count" id="count-awaiting">5</span>
</button>
<button
class="status-filter"
data-filter="paused"
onclick="filterByStatus('paused', this)"
>
<span class="filter-icon"></span>
<span class="filter-label">Paused</span>
<span class="filter-count" id="count-paused">2</span>
</button>
<button
class="status-filter"
data-filter="blocked"
onclick="filterByStatus('blocked', this)"
>
<span class="filter-icon"></span>
<span class="filter-label">Blocked/Issues</span>
<span class="filter-count" id="count-blocked">1</span>
</button>
<div class="time-saved">
<span class="time-label">Active Time Saved:</span>
<span class="time-value" id="time-saved">23.5 hrs this week</span>
</div> </div>
<form class="intent-form" id="intent-form" hx-post="/api/autotask/create" hx-ext="json-enc" hx-target="#compilation-result" hx-indicator="#compile-spinner"> </div>
<div class="intent-input-wrapper">
<!-- Main Content Area -->
<div class="main-content">
<!-- Left Panel: Intent List -->
<div class="intent-list-panel">
<div
class="intent-list"
id="intent-list"
hx-get="/api/autotask/list"
hx-trigger="load, every 10s"
hx-swap="innerHTML"
>
<!-- Intent cards will be loaded here -->
<div class="loading-state">
<div class="spinner"></div>
<span>Loading intents...</span>
</div>
</div>
</div>
<!-- Right Panel: Intent Detail -->
<div class="intent-detail-panel" id="intent-detail-panel">
<div class="detail-placeholder">
<span class="placeholder-icon">📋</span>
<p>Select an intent to view details</p>
</div>
</div>
</div>
<!-- New Intent Modal -->
<div class="modal-overlay" id="new-intent-modal" style="display: none">
<div class="modal-container">
<div class="modal-header">
<h2>New Intent</h2>
<button class="modal-close" onclick="closeNewIntentModal()">
×
</button>
</div>
<div class="modal-body">
<form
id="new-intent-form"
hx-post="/api/autotask/create"
hx-ext="json-enc"
hx-target="#intent-list"
hx-swap="afterbegin"
>
<div class="form-group">
<label for="intent-input"
>What would you like to accomplish?</label
>
<textarea <textarea
name="intent"
id="intent-input" id="intent-input"
class="intent-input" name="intent"
placeholder="Describe your task in natural language...&#10;&#10;Examples:&#10;• Make a financial CRM for Deloitte with client management and reporting&#10;• Create a website that collects leads and sends them to Salesforce&#10;• Build an automated email campaign for product launch&#10;• Analyze sales data and generate weekly reports" class="intent-textarea"
rows="4" placeholder="Describe your task in natural language...
Examples:
• Make a financial CRM for Deloitte
• Create a website that collects leads
• Build an automated email campaign
• Analyze sales data and generate reports"
rows="6"
required required
></textarea> ></textarea>
<div class="intent-actions"> </div>
<select name="execution_mode" class="execution-mode-select"> <div class="form-row">
<option value="semi-automatic">Semi-Automatic (Recommended)</option> <div class="form-group">
<option value="supervised">Supervised (Step-by-step)</option> <label>Execution Mode</label>
<option value="fully-automatic">Fully Automatic</option> <select name="execution_mode" class="form-select">
<option value="dry-run">Dry Run (Simulate Only)</option> <option value="semi-automatic">
Semi-Automatic
</option>
<option value="supervised">Supervised</option>
<option value="fully-automatic">
Fully Automatic
</option>
</select> </select>
<select name="priority" class="priority-select"> </div>
<option value="medium">Medium Priority</option> <div class="form-group">
<option value="critical">Critical</option> <label>Priority</label>
<select name="priority" class="form-select">
<option value="medium">Medium</option>
<option value="high">High</option> <option value="high">High</option>
<option value="low">Low</option> <option value="low">Low</option>
<option value="background">Background</option>
</select> </select>
<button type="submit" class="btn-create">
<span class="btn-icon">🚀</span>
Create & Execute
<span class="spinner" id="compile-spinner"></span>
</button>
</div> </div>
</div> </div>
<div class="form-actions">
<button
type="button"
class="btn-secondary"
onclick="closeNewIntentModal()"
>
Cancel
</button>
<button type="submit" class="btn-primary">
<span>Create Intent</span>
<span class="spinner" id="create-spinner"></span>
</button>
</div>
</form> </form>
<!-- Compilation Result -->
<div id="compilation-result" class="compilation-result"></div>
</div>
<!-- Active Tasks Section -->
<div class="tasks-section">
<div class="section-header">
<div class="section-title">
<span class="section-icon">📋</span>
<h2>Active Tasks</h2>
</div>
<div class="section-actions">
<div class="filter-tabs">
<button class="filter-tab active" data-filter="all" onclick="filterTasks('all', this)">
All <span class="count" id="count-all">0</span>
</button>
<button class="filter-tab" data-filter="running" onclick="filterTasks('running', this)">
Running <span class="count" id="count-running">0</span>
</button>
<button class="filter-tab" data-filter="approval" onclick="filterTasks('approval', this)">
Need Approval <span class="count" id="count-approval">0</span>
</button>
<button class="filter-tab" data-filter="decision" onclick="filterTasks('decision', this)">
Decisions <span class="count" id="count-decision">0</span>
</button>
</div>
<button class="btn-refresh" onclick="refreshTasks()" title="Refresh">
<span>🔄</span>
</button>
</div>
</div>
<!-- Task List -->
<div class="task-list" id="task-list" hx-get="/api/autotask/list" hx-trigger="load, every 5s" hx-swap="innerHTML">
<div class="loading-placeholder">
<div class="spinner-large"></div>
<p>Loading tasks...</p>
</div> </div>
</div> </div>
</div> </div>
<!-- Pending Decisions Modal --> <!-- Decision Modal -->
<div class="modal" id="decision-modal" style="display: none;"> <div class="modal-overlay" id="decision-modal" style="display: none">
<div class="modal-backdrop" onclick="closeDecisionModal()"></div> <div class="modal-container modal-lg">
<div class="modal-content decision-modal-content">
<div class="modal-header"> <div class="modal-header">
<h3><span class="modal-icon">🤔</span> Decision Required</h3> <h2>Decision Required</h2>
<button class="modal-close" onclick="closeDecisionModal()">×</button> <button class="modal-close" onclick="closeDecisionModal()">
×
</button>
</div> </div>
<div class="modal-body" id="decision-content"> <div class="modal-body" id="decision-content">
<!-- Decision content loaded dynamically --> <!-- Decision content loaded dynamically -->
</div> </div>
</div> </div>
</div> </div>
<!-- Approval Modal -->
<div class="modal" id="approval-modal" style="display: none;">
<div class="modal-backdrop" onclick="closeApprovalModal()"></div>
<div class="modal-content approval-modal-content">
<div class="modal-header">
<h3><span class="modal-icon"></span> Approval Required</h3>
<button class="modal-close" onclick="closeApprovalModal()">×</button>
</div>
<div class="modal-body" id="approval-content">
<!-- Approval content loaded dynamically -->
</div>
</div>
</div> </div>
<!-- Simulation Preview Modal --> <!-- Intent Card Template -->
<div class="modal" id="simulation-modal" style="display: none;"> <template id="intent-card-template">
<div class="modal-backdrop" onclick="closeSimulationModal()"></div> <div
<div class="modal-content simulation-modal-content"> class="intent-card"
<div class="modal-header"> data-intent-id="${id}"
<h3><span class="modal-icon">🔮</span> Impact Simulation</h3> data-status="${status}"
<button class="modal-close" onclick="closeSimulationModal()">×</button> onclick="selectIntent('${id}')"
</div> >
<div class="modal-body" id="simulation-content"> <div class="intent-card-header">
<!-- Simulation content loaded dynamically --> <h3 class="intent-title">${title}</h3>
</div> <div class="intent-status-indicator ${status_class}"></div>
</div>
</div>
</div> </div>
<!-- Task Item Template (for reference, actual rendering done server-side) --> <div class="intent-progress">
<template id="task-item-template">
<div class="autotask-item" data-task-id="${id}" data-status="${status}">
<div class="task-header">
<div class="task-info">
<div class="task-status-badge status-${status}">${status}</div>
<h4 class="task-title">${title}</h4>
<span class="task-priority priority-${priority}">${priority}</span>
</div>
<div class="task-meta">
<span class="task-time" title="Created">${created_at}</span>
<span class="task-mode">${execution_mode}</span>
</div>
</div>
<div class="task-progress">
<div class="progress-bar"> <div class="progress-bar">
<div class="progress-fill" style="width: ${progress}%"></div> <div class="progress-fill" style="width: ${progress}%"></div>
</div> </div>
<span class="progress-text">${current_step}/${total_steps} steps (${progress}%)</span> <div class="progress-info">
</div> <span class="progress-steps"
>${current_step}/${total_steps} Steps</span
<div class="task-details"> >
<p class="task-intent">${intent}</p> <span class="progress-percent">${progress}%</span>
<!-- Current Step Info -->
<div class="current-step" style="display: ${show_current_step}">
<div class="step-indicator">
<span class="step-icon">▶️</span>
<span class="step-name">Step ${current_step}: ${current_step_name}</span>
</div>
<div class="step-status">${current_step_status}</div>
</div>
<!-- Pending Decision Alert -->
<div class="pending-decision-alert" style="display: ${show_decision}">
<span class="alert-icon">🤔</span>
<span class="alert-text">${decision_count} decision(s) pending</span>
<button class="btn-view-decisions" onclick="viewDecisions('${id}')">View & Decide</button>
</div>
<!-- Pending Approval Alert -->
<div class="pending-approval-alert" style="display: ${show_approval}">
<span class="alert-icon">⚠️</span>
<span class="alert-text">${approval_count} approval(s) required</span>
<button class="btn-view-approvals" onclick="viewApprovals('${id}')">Review & Approve</button>
</div>
<!-- Risk Summary -->
<div class="risk-summary" style="display: ${show_risk}">
<span class="risk-icon risk-${risk_level}"></span>
<span class="risk-text">Risk Level: ${risk_level}</span>
</div> </div>
</div> </div>
<div class="task-actions"> <div class="intent-status-section">
<button class="btn-action btn-view" onclick="viewTaskDetails('${id}')" title="View Details"> <div class="status-label">STATUS</div>
<span>👁️</span> Details <div class="status-row">
</button>
<button class="btn-action btn-simulate" onclick="simulateTask('${id}')" title="Simulate Impact" style="display: ${show_simulate}">
<span>🔮</span> Simulate
</button>
<button class="btn-action btn-pause" onclick="pauseTask('${id}')" title="Pause" style="display: ${show_pause}">
<span>⏸️</span> Pause
</button>
<button class="btn-action btn-resume" onclick="resumeTask('${id}')" title="Resume" style="display: ${show_resume}">
<span>▶️</span> Resume
</button>
<button class="btn-action btn-cancel" onclick="cancelTask('${id}')" title="Cancel">
<span></span> Cancel
</button>
</div>
<!-- Execution Log (expandable) -->
<details class="execution-log">
<summary>Execution Log (${log_count} entries)</summary>
<div class="log-entries" id="log-${id}">
<!-- Log entries loaded on expand -->
</div>
</details>
</div>
</template>
<!-- Compilation Result Template -->
<template id="compilation-result-template">
<div class="compiled-plan">
<div class="plan-header">
<div class="plan-info">
<h3>${plan_name}</h3>
<p>${plan_description}</p>
</div>
<div class="plan-meta">
<span class="confidence-badge confidence-${confidence_level}">
Confidence: ${confidence}%
</span>
<span class="risk-badge risk-${risk_level}">
Risk: ${risk_level}
</span>
<span class="duration-badge">
Est. Duration: ${estimated_duration}
</span>
<span class="cost-badge">
Est. Cost: $${estimated_cost}
</span>
</div>
</div>
<!-- Alternatives (if ambiguous) -->
<div class="alternatives-section" style="display: ${show_alternatives}">
<h4>🔀 Alternative Approaches</h4>
<p class="alternatives-note</h4>">The intent was ambiguous. Please choose your preferred approach:</p>
<div class="alternatives-list">
${alternatives_html}
</div>
</div>
<!-- Execution Plan Steps -->
<div class="plan-steps">
<h4>📋 Execution Plan (${step_count} steps)</h4>
<div class="steps-list">
${steps_html}
</div>
</div>
<!-- Resource Estimates -->
<div class="resource-estimates">
<h4>📊 Resource Estimates</h4>
<div class="estimates-grid">
<div class="estimate-item">
<span class="estimate-icon">💻</span>
<span class="estimate-label">Compute</span>
<span class="estimate-value">${compute_hours} hours</span>
</div>
<div class="estimate-item">
<span class="estimate-icon">💾</span>
<span class="estimate-label">Storage</span>
<span class="estimate-value">${storage_gb} GB</span>
</div>
<div class="estimate-item">
<span class="estimate-icon">🌐</span>
<span class="estimate-label">API Calls</span>
<span class="estimate-value">${api_calls}</span>
</div>
<div class="estimate-item">
<span class="estimate-icon">🤖</span>
<span class="estimate-label">LLM Tokens</span>
<span class="estimate-value">${llm_tokens}</span>
</div>
</div>
</div>
<!-- MCP Servers & APIs -->
<div class="integrations-section" style="display: ${show_integrations}">
<h4></h4>🔌 Integrations Required</h4>
<div class="integrations-list">
<div class="mcp-servers" style="display: ${show_mcp}">
<h5>MCP Servers</h5>
<div class="integration-tags">${mcp_servers_html}</div>
</div>
<div class="external-apis" style="display: ${show_apis}">
<h5>External APIs</h5>
<div class="integration-tags">${external_apis_html}</div>
</div>
</div>
</div>
<!-- Risk Assessment -->
<div class="risk-assessment" style="display: ${show_risks}">
<h4>⚠️ Risk Assessment</h4>
<div class="risks-list">
${risks_html}
</div>
</div>
<!-- Generated BASIC Code (collapsible) -->
<details class="generated-code">
<summary>📝 Generated BASIC Program (${code_lines} lines)</summary>
<pre class="code-preview"><code>${basic_program}</code></pre>
<button class="btn-copy-code" onclick="copyGeneratedCode()">📋 Copy Code</button>
</details>
<!-- Action Buttons -->
<div class="plan-actions">
<button class="btn-secondary" onclick="discardPlan()">
<span>🗑️</span> Discard
</button>
<button class="btn-secondary" onclick="editPlan()">
<span>✏️</span> Edit Plan
</button>
<button class="btn-secondary" onclick="simulatePlan('${plan_id}')">
<span>🔮</span> Simulate First
</button>
<button class="btn-primary" onclick="executePlan('${plan_id}')" ${execute_disabled}>
<span>🚀</span> Execute Plan
</button>
</div>
</div>
</template>
<!-- Decision Option Template -->
<template id="decision-option-template">
<div class="decision-option" data-option-id="${id}">
<div class="option-header">
<input type="radio" name="decision_choice" value="${id}" id="option-${id}">
<label for="option-${id}">
<span class="option-label">${label}</span>
<span class="recommended-badge" style="display: ${show_recommended}">Recommended</span>
</label>
</div>
<p class="option-description">${description}</p>
<div class="option-details">
<div class="pros">
<h5>✅ Pros</h5>
<ul>${pros_html}</ul>
</div>
<div class="cons">
<h5>❌ Cons</h5>
<ul>${cons_html}</ul>
</div>
</div>
<div class="option-impact">
<span class="impact-item</h5>">💰 Cost: ${cost_change}</span>
<span class="impact-item">⏱️ Time: ${time_change}</span>
<span class="impact-item risk-${risk_level}">⚠️ Risk: ${risk_level}</span>
</div>
</div>
</template>
<!-- Approval Request Template -->
<template id="approval-request-template">
<div class="approval-request" data-approval-id="${id}">
<div class="approval-header">
<span class="approval-type type-${approval_type}">${approval_type}</span>
<span class="approval-risk risk-${risk_level}">${risk_level} Risk</span>
</div>
<h4>${title}</h4>
<p class="approval-description">${description}</p>
<div class="approval-impact">
<h5>Impact Summary</h5>
<p>${impact_summary}</p>
</div>
<div class="simulation-preview" style="display: ${show_simulation}">
<h5>Simulation Result</h5>
<div class="simulation-summary">${simulation_summary}</div>
</div>
<div class="approval-meta">
<span class="meta-item">Step: ${step_name}</span>
<span class="meta-item">Expires: ${expires_at}</span>
<span class="meta-item">Default: ${default_action}</span>
</div>
<div class="approval-actions">
<button class="btn-reject" onclick="rejectApproval('${id}')">
<span></span> Reject
</button>
<button class="btn-defer" onclick="deferApproval('${id}')">
<span>⏸️</span> Defer
</button>
<button class="btn-approve" onclick="approveApproval('${id}')">
<span></span> Approve
</button>
</div>
</div>
</template>
<!-- Simulation Result Template -->
<template id="simulation-result-template">
<div class="simulation-result">
<div class="simulation-header">
<div class="simulation-status status-${success}">
<span class="status-icon">${status_icon}</span> <span class="status-icon">${status_icon}</span>
<span class="status-text">${status_text}</span> <span class="status-text">${status_display}</span>
</div>
<div class="simulation-confidence">
Confidence: ${confidence}%
</div> </div>
</div> </div>
<div class="impact-overview"> <div class="intent-current-task">
<h4>Impact Assessment</h4> <div class="current-task-label">${current_task_name}</div>
<div class="impact-grid"> <div class="current-task-desc">${current_task_description}</div>
<div class="impact-card risk-${data_risk}">
<span class="impact-icon">💾</span>
<span class="impact-label">Data Impact</span>
<span class="impact-value">${data_impact_summary}</span>
</div>
<div class="impact-card risk-${cost_risk}">
<span class="impact-icon">💰</span>
<span class="impact-label">Cost Impact</span>
<span class="impact-value">${cost_impact_summary}</span>
</div>
<div class="impact-card risk-${time_risk}">
<span class="impact-icon">⏱️</span>
<span class="impact-label">Time Impact</span>
<span class="impact-value">${time_impact_summary}</span>
</div>
<div class="impact-card risk-${security_risk}">
<span class="impact-icon">🔒</span>
<span class="impact-label">Security Impact</span>
<span class="impact-value">${security_impact_summary}</span>
</div> </div>
<div class="intent-integrity">
<div class="integrity-label">INTEGRITY</div>
<div class="integrity-items">
<span class="integrity-item">
<span class="item-icon">🕐</span>
Started ${started_ago}
</span>
<span class="integrity-item">
<span class="item-icon">📅</span>
Due ${due_date}
</span>
<span class="integrity-badge health-${health_level}">
Intent Health
<span class="health-value">${health_percent}%</span>
</span>
</div> </div>
</div> </div>
<div class="step-outcomes"> <button
<h4>Step-by-Step Predictions</h4> class="btn-detail-view"
<div class="outcomes-list"> onclick="event.stopPropagation(); viewDetailedIntent('${id}')"
${step_outcomes_html} >
</div> ${action_button_text}
</div>
<div class="side-effects" style="display: ${show_side_effects}">
<h4>⚠️ Potential Side Effects</h4>
<div class="side-effects-list">
${side_effects_html}
</div>
</div>
<div class="recommendations" style="display: ${show_recommendations}">
<h4>💡 Recommendations</h4>
<div class="recommendations-list">
${recommendations_html}
</div>
</div>
<div class="simulation-actions">
<button class="btn-secondary" onclick="closeSimulationModal()">
<span>↩️</span> Back
</button> </button>
<button class="btn-primary" onclick="proceedAfterSimulation('${task_id}')" ${proceed_disabled}> </div>
<span>🚀</span> Proceed with Execution </template>
<!-- Intent Detail Template -->
<template id="intent-detail-template">
<div class="detail-view">
<!-- Header -->
<div class="detail-header">
<button class="btn-back" onclick="closeDetailPanel()">
<span></span>
</button> </button>
<h2 class="detail-title">${title}</h2>
<div class="detail-status">
<span class="status-icon">${status_icon}</span>
<span class="status-label">${status_display}</span>
</div>
<button class="btn-pause" onclick="togglePause('${id}')">
<span class="pause-icon"></span>
</button>
</div>
<!-- Progress Bar -->
<div class="detail-progress">
<div class="progress-bar large">
<div class="progress-fill" style="width: ${progress}%"></div>
</div>
<div class="progress-stats">
<span class="progress-steps"
>${current_step}/${total_steps} Steps</span
>
<span class="progress-percent">${progress}%</span>
</div>
</div>
<!-- Decision Required Panel -->
<div
class="decision-panel"
id="decision-panel-${id}"
style="display: ${show_decision}"
>
<div class="decision-header">
<span class="decision-badge">DECISION REQUIRED</span>
<span class="decision-title">${decision_title}</span>
</div>
<div class="decision-body">
<div class="decision-actions">
<button
class="btn-decision-primary"
onclick="openDecisionModal('${id}')"
>
Make Decision
</button>
<button
class="btn-decision-secondary"
onclick="viewDecisionContext('${id}')"
>
View Context Details
</button>
</div>
<div class="decision-context">
<div class="context-label">Context:</div>
<p class="context-text">${decision_context}</p>
</div>
</div>
</div>
<!-- Status Section -->
<div class="detail-section">
<div class="section-header">
<span class="section-title">STATUS</span>
<span class="section-runtime">Runtime: ${runtime}</span>
</div>
<div class="status-current-task">
<span class="task-name">${current_task_name}</span>
<span class="task-estimate">Estimated: ${estimated_time}</span>
</div>
<div class="status-steps-preview">
<div class="step-item active">
<span class="step-dot"></span>
<span class="step-name">${next_step_name}</span>
</div>
<div class="step-item pending">
<span class="step-dot"></span>
<span class="step-name"
>Decision Point Coming (Step ${decision_step})</span
>
<span class="step-note">${decision_note}</span>
</div>
</div>
</div>
<!-- Progress Log Section -->
<div class="detail-section">
<div class="section-header">
<span class="section-title">PROGRESS LOG</span>
</div>
<div class="progress-log" id="progress-log-${id}">
<!-- Log entries will be populated here -->
</div>
</div>
<!-- Terminal Activity Section -->
<div class="detail-section terminal-section">
<div class="section-header">
<span class="section-title"
>TERMINAL (LIVE AGENT ACTIVITY)</span
>
<span class="terminal-stats">
Processed: <strong>${processed_count}</strong> data points /
Processing speed: <strong>${processing_speed}</strong> /
Estimated completion:
<strong>${estimated_completion}</strong>
</span>
</div>
<div class="terminal-output" id="terminal-${id}">
<div class="terminal-line">
Synthesizing competitive insights...
</div>
<div class="terminal-line">
Cross-referencing pricing data...
</div>
<!-- More terminal lines will stream here -->
</div>
<div class="terminal-cursor"></div>
</div> </div>
</div> </div>
</template> </template>
<!-- Progress Log Entry Template -->
<template id="progress-log-entry-template">
<div class="log-entry ${expanded_class}">
<div class="log-entry-header" onclick="toggleLogEntry(this)">
<span class="log-icon ${status_class}"></span>
<span class="log-title">${title}</span>
<span class="log-expand"></span>
<span class="log-step-badge">${step_label}</span>
</div>
<div class="log-entry-body">
<div class="log-sub-entries">
<!-- Sub entries -->
</div>
</div>
</div>
</template>
<!-- Progress Log Sub-Entry Template -->
<template id="progress-log-subentry-template">
<div class="log-sub-entry">
<span class="sub-icon ${status_class}"></span>
<span class="sub-text">${text}</span>
<span class="sub-duration">Duration: ${duration}</span>
<span class="sub-check">${check_icon}</span>
</div>
</template>
<!-- Floating Progress Panel (for active task) -->
<div
class="floating-progress-panel"
id="floating-progress"
style="display: none"
>
<div class="floating-header">
<div class="floating-title">
<span class="floating-icon"></span>
<span class="floating-name" id="floating-task-name"
>Processing...</span
>
</div>
<div class="floating-controls">
<button class="btn-minimize" onclick="minimizeFloatingPanel()">
</button>
<button class="btn-close-floating" onclick="closeFloatingPanel()">
×
</button>
</div>
</div>
<div class="floating-body">
<div class="floating-status">
<span class="status-text" id="floating-status-text"
>Initializing...</span
>
<span class="status-steps" id="floating-status-steps">0/0</span>
</div>
<div class="floating-progress-bar">
<div
class="progress-fill"
id="floating-progress-fill"
style="width: 0%"
></div>
</div>
<div class="floating-percentage" id="floating-percentage">0%</div>
<div class="floating-log" id="floating-log">
<!-- Real-time log entries -->
</div>
<div class="floating-terminal" id="floating-terminal">
<!-- LLM output streaming -->
</div>
</div>
</div>
<!-- Toast Container -->
<div class="toast-container" id="toast-container"></div>
<script src="autotask.js"></script>

View file

@ -1,6 +1,6 @@
/* ============================================================================= /* =============================================================================
AUTO TASK JAVASCRIPT - Intelligent Self-Executing Task Interface AUTO TASK JAVASCRIPT - Sentient Theme
Premium VIP Mode Functionality Intelligent Self-Executing Task Interface
============================================================================= */ ============================================================================= */
// ============================================================================= // =============================================================================
@ -8,13 +8,17 @@
// ============================================================================= // =============================================================================
const AutoTaskState = { const AutoTaskState = {
currentFilter: "all", currentFilter: "active",
tasks: [], selectedIntentId: null,
intents: [],
compiledPlan: null, compiledPlan: null,
pendingDecisions: [], pendingDecisions: [],
pendingApprovals: [], pendingApprovals: [],
refreshInterval: null, refreshInterval: null,
wsConnection: null, wsConnection: null,
progressWsConnection: null,
activeTaskProgress: {},
llmOutputStream: [],
}; };
// ============================================================================= // =============================================================================
@ -29,6 +33,9 @@ function initAutoTask() {
// Initialize WebSocket for real-time updates // Initialize WebSocket for real-time updates
initWebSocket(); initWebSocket();
// Initialize task progress WebSocket
initTaskProgressWebSocket();
// Start auto-refresh // Start auto-refresh
startAutoRefresh(); startAutoRefresh();
@ -41,7 +48,101 @@ function initAutoTask() {
// Setup keyboard shortcuts // Setup keyboard shortcuts
setupKeyboardShortcuts(); setupKeyboardShortcuts();
console.log("AutoTask initialized"); // Initialize floating panel (hidden by default)
initFloatingPanel();
console.log("AutoTask Sentient initialized");
}
// =============================================================================
// FLOATING PANEL
// =============================================================================
function initFloatingPanel() {
const panel = document.getElementById("floating-progress");
if (panel) {
panel.style.display = "none";
}
}
function showFloatingPanel(taskId, taskName) {
const panel = document.getElementById("floating-progress");
if (!panel) return;
panel.style.display = "block";
panel.dataset.taskId = taskId;
const nameEl = document.getElementById("floating-task-name");
if (nameEl) nameEl.textContent = taskName || "Processing...";
updateFloatingProgress(0, 0, 0, "Initializing...");
}
function updateFloatingProgress(progress, current, total, statusText) {
const fill = document.getElementById("floating-progress-fill");
const pct = document.getElementById("floating-percentage");
const steps = document.getElementById("floating-status-steps");
const status = document.getElementById("floating-status-text");
if (fill) fill.style.width = `${progress}%`;
if (pct) pct.textContent = `${progress}%`;
if (steps) steps.textContent = `${current}/${total}`;
if (status) status.textContent = statusText;
}
function addFloatingLog(icon, message, type = "info") {
const log = document.getElementById("floating-log");
if (!log) return;
const entry = document.createElement("div");
entry.className = `floating-log-entry ${type}`;
entry.innerHTML = `
<span class="log-icon">${icon}</span>
<span class="log-message">${escapeHtml(message)}</span>
<span class="log-time">${formatTime(new Date())}</span>
`;
log.appendChild(entry);
log.scrollTop = log.scrollHeight;
}
function addFloatingTerminalOutput(text) {
const terminal = document.getElementById("floating-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 50 lines
while (terminal.children.length > 50) {
terminal.removeChild(terminal.firstChild);
}
}
function minimizeFloatingPanel() {
const panel = document.getElementById("floating-progress");
if (panel) {
panel.classList.toggle("minimized");
}
}
function closeFloatingPanel() {
const panel = document.getElementById("floating-progress");
if (panel) {
panel.style.display = "none";
}
}
function completeFloatingPanel(message) {
updateFloatingProgress(100, 0, 0, message || "Complete!");
addFloatingLog("✅", message || "Task completed successfully", "success");
// Auto-hide after delay
setTimeout(() => {
closeFloatingPanel();
}, 5000);
} }
// ============================================================================= // =============================================================================
@ -76,10 +177,485 @@ function initWebSocket() {
} }
} }
function initTaskProgressWebSocket() {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/ws/task-progress`;
try {
AutoTaskState.progressWsConnection = new WebSocket(wsUrl);
AutoTaskState.progressWsConnection.onopen = function () {
console.log("Task Progress WebSocket connected");
};
AutoTaskState.progressWsConnection.onmessage = function (event) {
handleTaskProgressMessage(JSON.parse(event.data));
};
AutoTaskState.progressWsConnection.onclose = function () {
console.log("Task Progress WebSocket disconnected, reconnecting...");
setTimeout(initTaskProgressWebSocket, 3000);
};
AutoTaskState.progressWsConnection.onerror = function (error) {
console.error("Task Progress WebSocket error:", error);
};
} catch (e) {
console.warn("Task Progress WebSocket not available");
}
}
function handleTaskProgressMessage(data) {
console.log("Task progress:", data);
switch (data.type) {
case "connected":
console.log("Connected to task progress stream");
break;
case "task_started":
onTaskStarted(data);
break;
case "task_progress":
onTaskProgress(data);
break;
case "task_completed":
onTaskProgressCompleted(data);
break;
case "task_error":
onTaskProgressError(data);
break;
case "llm_stream":
onLLMStream(data);
break;
default:
console.log("Unknown progress event:", data.type);
}
}
function onTaskStarted(data) {
AutoTaskState.activeTaskProgress[data.task_id] = {
started: new Date(),
totalSteps: data.total_steps,
currentStep: 0,
progress: 0,
logs: [],
};
// Show floating panel
showFloatingPanel(data.task_id, data.message);
addFloatingLog("🚀", data.message, "started");
// Also update detail panel if viewing this intent
if (AutoTaskState.selectedIntentId === data.task_id) {
updateDetailPanelProgress(data);
}
}
function onTaskProgress(data) {
const taskProgress = AutoTaskState.activeTaskProgress[data.task_id];
if (taskProgress) {
taskProgress.currentStep = data.current_step;
taskProgress.progress = data.progress;
taskProgress.logs.push({
time: new Date(),
step: data.step,
message: data.message,
details: data.details,
});
}
// Update floating panel
updateFloatingProgress(
data.progress,
data.current_step,
data.total_steps,
data.message,
);
const stepIcons = {
llm_request: "🤖",
llm_response: "✨",
parse_structure: "📋",
create_tables: "🗄️",
tables_synced: "✅",
write_files: "📝",
write_file: "📄",
write_designer: "🎨",
write_tools: "🔧",
sync_site: "🔄",
};
const icon = stepIcons[data.step] || "📌";
addFloatingLog(icon, data.message, "progress");
// Update detail panel if viewing this intent
if (AutoTaskState.selectedIntentId === data.task_id) {
updateDetailPanelProgress(data);
addTerminalLine(data.message);
}
}
function onTaskProgressCompleted(data) {
const taskProgress = AutoTaskState.activeTaskProgress[data.task_id];
if (taskProgress) {
taskProgress.progress = 100;
taskProgress.completed = new Date();
}
completeFloatingPanel(data.message);
// Refresh intent list
setTimeout(() => {
refreshIntents();
updateStats();
}, 1000);
}
function onTaskProgressError(data) {
addFloatingLog("❌", data.error, "error");
updateFloatingProgress(
AutoTaskState.activeTaskProgress[data.task_id]?.progress || 0,
0,
0,
`Error: ${data.error}`,
);
}
function onLLMStream(data) {
// Stream LLM output to terminal
addFloatingTerminalOutput(data.text);
if (AutoTaskState.selectedIntentId === data.task_id) {
addTerminalLine(data.text, true);
}
}
function addTerminalLine(text, isLLM = false) {
const terminal = document.querySelector(
`#terminal-${AutoTaskState.selectedIntentId}`,
);
if (!terminal) return;
const line = document.createElement("div");
line.className = `terminal-line${isLLM ? " highlight" : ""}`;
line.textContent = text;
terminal.appendChild(line);
terminal.scrollTop = terminal.scrollHeight;
}
function updateDetailPanelProgress(data) {
// Update progress bar in detail panel
const progressFill = document.querySelector(
".detail-progress .progress-fill",
);
const progressSteps = document.querySelector(
".detail-progress .progress-steps",
);
const progressPct = document.querySelector(
".detail-progress .progress-percent",
);
if (progressFill) progressFill.style.width = `${data.progress}%`;
if (progressSteps)
progressSteps.textContent = `${data.current_step}/${data.total_steps} Steps`;
if (progressPct) progressPct.textContent = `${data.progress}%`;
}
// =============================================================================
// INTENT SELECTION & DETAIL PANEL
// =============================================================================
function selectIntent(intentId) {
// Update selected state
document.querySelectorAll(".intent-card").forEach((card) => {
card.classList.remove("selected");
});
const selectedCard = document.querySelector(
`.intent-card[data-intent-id="${intentId}"]`,
);
if (selectedCard) {
selectedCard.classList.add("selected");
}
AutoTaskState.selectedIntentId = intentId;
// Load detail panel
loadIntentDetail(intentId);
}
function loadIntentDetail(intentId) {
const panel = document.getElementById("intent-detail-panel");
if (!panel) return;
// Show loading state
panel.innerHTML = `
<div class="loading-state">
<div class="spinner"></div>
<span>Loading intent details...</span>
</div>
`;
// Fetch intent details
fetch(`/api/autotask/${intentId}`)
.then((response) => response.json())
.then((data) => {
renderIntentDetail(data);
})
.catch((error) => {
console.error("Failed to load intent detail:", error);
panel.innerHTML = `
<div class="detail-placeholder">
<span class="placeholder-icon"></span>
<p>Failed to load intent details</p>
</div>
`;
});
}
function renderIntentDetail(intent) {
const panel = document.getElementById("intent-detail-panel");
if (!panel) return;
const statusIcons = {
active: "⚡",
complete: "✓",
paused: "⏸",
blocked: "⚠",
awaiting: "⏳",
};
const healthClass =
intent.health >= 80 ? "good" : intent.health >= 50 ? "warning" : "bad";
panel.innerHTML = `
<div class="detail-view">
<!-- Header -->
<div class="detail-header">
<button class="btn-back" onclick="closeDetailPanel()">
<span></span>
</button>
<h2 class="detail-title">${escapeHtml(intent.title || intent.intent)}</h2>
<div class="detail-status">
<span class="status-icon">${statusIcons[intent.status] || "⚡"}</span>
<span class="status-label">${intent.status_display || intent.status}</span>
</div>
<button class="btn-pause" onclick="togglePause('${intent.id}')">
<span class="pause-icon">${intent.status === "paused" ? "▶" : "⏸"}</span>
</button>
</div>
<!-- Progress Bar -->
<div class="detail-progress">
<div class="progress-bar large">
<div class="progress-fill" style="width: ${intent.progress || 0}%"></div>
</div>
<div class="progress-stats">
<span class="progress-steps">${intent.current_step || 0}/${intent.total_steps || 0} Steps</span>
<span class="progress-percent">${intent.progress || 0}%</span>
</div>
</div>
${
intent.decision_required
? `
<!-- Decision Required Panel -->
<div class="decision-panel">
<div class="decision-header">
<span class="decision-badge">DECISION REQUIRED</span>
<span class="decision-title">${escapeHtml(intent.decision_title || "Decision needed")}</span>
</div>
<div class="decision-body">
<div class="decision-actions">
<button class="btn-decision-primary" onclick="openDecisionModal('${intent.id}')">
Make Decision
</button>
<button class="btn-decision-secondary" onclick="viewDecisionContext('${intent.id}')">
View Context Details
</button>
</div>
<div class="decision-context">
<div class="context-label">Context:</div>
<p class="context-text">${escapeHtml(intent.decision_context || "")}</p>
</div>
</div>
</div>
`
: ""
}
<!-- Status Section -->
<div class="detail-section">
<div class="section-header">
<span class="section-title">STATUS</span>
<span class="section-runtime">Runtime: ${intent.runtime || "0 min"}</span>
</div>
<div class="status-current-task">
<span class="task-name">${escapeHtml(intent.current_task_name || "Processing...")}</span>
<span class="task-estimate">Estimated: ${intent.estimated_time || "calculating..."}</span>
</div>
<div class="status-steps-preview">
${renderStepsPreview(intent.steps || [])}
</div>
</div>
<!-- Progress Log Section -->
<div class="detail-section">
<div class="section-header">
<span class="section-title">PROGRESS LOG</span>
</div>
<div class="progress-log" id="progress-log-${intent.id}">
${renderProgressLog(intent.logs || [])}
</div>
</div>
<!-- Terminal Activity Section -->
<div class="detail-section terminal-section">
<div class="section-header">
<span class="section-title">TERMINAL (LIVE AGENT ACTIVITY)</span>
<span class="terminal-stats">
Processed: <strong>${intent.processed_count || 0}</strong> data points /
Processing speed: <strong>${intent.processing_speed || "~8 sources/s"}</strong> /
Estimated completion: <strong>${intent.estimated_completion || "calculating"}</strong>
</span>
</div>
<div class="terminal-output" id="terminal-${intent.id}">
<div class="terminal-line">Initializing agent...</div>
</div>
<div class="terminal-cursor"></div>
</div>
</div>
`;
}
function renderStepsPreview(steps) {
if (!steps || steps.length === 0) {
return '<div class="step-item pending"><span class="step-dot"></span><span class="step-name">No steps yet</span></div>';
}
return steps
.slice(0, 3)
.map(
(step) => `
<div class="step-item ${step.status || "pending"}">
<span class="step-dot"></span>
<span class="step-name">${escapeHtml(step.name)}</span>
${step.note ? `<span class="step-note">${escapeHtml(step.note)}</span>` : ""}
</div>
`,
)
.join("");
}
function renderProgressLog(logs) {
if (!logs || logs.length === 0) {
return '<div class="log-entry"><div class="log-entry-header"><span class="log-icon pending">●</span><span class="log-title">Waiting for activity...</span></div></div>';
}
return logs
.map(
(log) => `
<div class="log-entry ${log.expanded ? "expanded" : ""}">
<div class="log-entry-header" onclick="toggleLogEntry(this)">
<span class="log-icon ${log.status || "pending"}"></span>
<span class="log-title">${escapeHtml(log.title)}</span>
<span class="log-expand"></span>
${log.step_label ? `<span class="log-step-badge">${escapeHtml(log.step_label)}</span>` : ""}
</div>
<div class="log-entry-body">
<div class="log-sub-entries">
${(log.sub_entries || [])
.map(
(sub) => `
<div class="log-sub-entry">
<span class="sub-icon ${sub.status || "complete"}"></span>
<span class="sub-text">${escapeHtml(sub.text)}</span>
<span class="sub-duration">Duration: ${sub.duration || "< 1 min"}</span>
<span class="sub-check">${sub.status === "complete" ? "✓" : ""}</span>
</div>
`,
)
.join("")}
</div>
</div>
</div>
`,
)
.join("");
}
function toggleLogEntry(header) {
const entry = header.closest(".log-entry");
if (entry) {
entry.classList.toggle("expanded");
}
}
function closeDetailPanel() {
AutoTaskState.selectedIntentId = null;
document.querySelectorAll(".intent-card").forEach((card) => {
card.classList.remove("selected");
});
const panel = document.getElementById("intent-detail-panel");
if (panel) {
panel.innerHTML = `
<div class="detail-placeholder">
<span class="placeholder-icon">📋</span>
<p>Select an intent to view details</p>
</div>
`;
}
}
function viewDetailedIntent(intentId) {
selectIntent(intentId);
}
function togglePause(intentId) {
const intent = AutoTaskState.intents.find((i) => i.id === intentId);
if (!intent) return;
const action = intent.status === "paused" ? "resume" : "pause";
fetch(`/api/autotask/${intentId}/${action}`, { method: "POST" })
.then((response) => response.json())
.then((result) => {
if (result.success) {
showToast(`Intent ${action}d`, "success");
refreshIntents();
if (AutoTaskState.selectedIntentId === intentId) {
loadIntentDetail(intentId);
}
} else {
showToast(`Failed to ${action} intent`, "error");
}
})
.catch((error) => {
console.error(`Failed to ${action} intent:`, error);
showToast(`Failed to ${action} intent`, "error");
});
}
function formatTime(date) {
return date.toLocaleTimeString("en-US", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
});
}
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
function handleWebSocketMessage(data) { function handleWebSocketMessage(data) {
switch (data.type) { switch (data.type) {
case "task_update": case "task_update":
updateTaskInList(data.task); case "intent_update":
updateIntentInList(data.task || data.intent);
break; break;
case "step_progress": case "step_progress":
updateStepProgress(data.taskId, data.step, data.progress); updateStepProgress(data.taskId, data.step, data.progress);
@ -91,10 +667,12 @@ function handleWebSocketMessage(data) {
showApprovalNotification(data.approval); showApprovalNotification(data.approval);
break; break;
case "task_completed": case "task_completed":
onTaskCompleted(data.task); case "intent_completed":
onIntentCompleted(data.task || data.intent);
break; break;
case "task_failed": case "task_failed":
onTaskFailed(data.task, data.error); case "intent_failed":
onIntentFailed(data.task || data.intent, data.error);
break; break;
case "stats_update": case "stats_update":
updateStatsFromData(data.stats); updateStatsFromData(data.stats);
@ -102,6 +680,41 @@ function handleWebSocketMessage(data) {
} }
} }
function updateIntentInList(intent) {
const card = document.querySelector(
`.intent-card[data-intent-id="${intent.id}"]`,
);
if (card) {
// Update progress
const progressFill = card.querySelector(".progress-fill");
const progressSteps = card.querySelector(".progress-steps");
const progressPct = card.querySelector(".progress-percent");
if (progressFill) progressFill.style.width = `${intent.progress}%`;
if (progressSteps)
progressSteps.textContent = `${intent.current_step}/${intent.total_steps} Steps`;
if (progressPct) progressPct.textContent = `${intent.progress}%`;
// Update status indicator
const statusIndicator = card.querySelector(".intent-status-indicator");
if (statusIndicator) {
statusIndicator.className = `intent-status-indicator ${intent.status}`;
}
}
}
function onIntentCompleted(intent) {
showToast(`Intent completed: ${intent.title || intent.id}`, "success");
updateIntentInList(intent);
updateStats();
}
function onIntentFailed(intent, error) {
showToast(`Intent failed: ${intent.title || intent.id} - ${error}`, "error");
updateIntentInList(intent);
updateStats();
}
// ============================================================================= // =============================================================================
// EVENT LISTENERS // EVENT LISTENERS
// ============================================================================= // =============================================================================
@ -212,28 +825,33 @@ function updateStats() {
} }
function updateStatsFromData(stats) { function updateStatsFromData(stats) {
// Header stats // Sentient filter counts
document.getElementById("stat-running").textContent = stats.running || 0; const countComplete = document.getElementById("count-complete");
document.getElementById("stat-pending").textContent = stats.pending || 0; const countActive = document.getElementById("count-active");
document.getElementById("stat-completed").textContent = stats.completed || 0; const countAwaiting = document.getElementById("count-awaiting");
document.getElementById("stat-approval").textContent = const countPaused = document.getElementById("count-paused");
stats.pending_approval || 0; const countBlocked = document.getElementById("count-blocked");
const timeSaved = document.getElementById("time-saved");
// Filter counts if (countComplete) countComplete.textContent = stats.completed || 0;
document.getElementById("count-all").textContent = stats.total || 0; if (countActive) countActive.textContent = stats.running || stats.active || 0;
document.getElementById("count-running").textContent = stats.running || 0; if (countAwaiting)
document.getElementById("count-approval").textContent = countAwaiting.textContent = stats.pending_decision || stats.awaiting || 0;
stats.pending_approval || 0; if (countPaused) countPaused.textContent = stats.paused || 0;
document.getElementById("count-decision").textContent = if (countBlocked)
stats.pending_decision || 0; countBlocked.textContent = stats.blocked || stats.failed || 0;
if (timeSaved) timeSaved.textContent = stats.time_saved || "0 hrs this week";
// Highlight if approvals needed // Legacy support
const approvalStat = document.querySelector(".stat-item.highlight"); const statRunning = document.getElementById("stat-running");
if (approvalStat && stats.pending_approval > 0) { const statPending = document.getElementById("stat-pending");
approvalStat.classList.add("attention"); const statCompleted = document.getElementById("stat-completed");
} else if (approvalStat) { const statApproval = document.getElementById("stat-approval");
approvalStat.classList.remove("attention");
} if (statRunning) statRunning.textContent = stats.running || 0;
if (statPending) statPending.textContent = stats.pending || 0;
if (statCompleted) statCompleted.textContent = stats.completed || 0;
if (statApproval) statApproval.textContent = stats.pending_approval || 0;
} }
// ============================================================================= // =============================================================================
@ -256,6 +874,23 @@ function filterTasks(filter, button) {
}); });
} }
// Sentient status filter
function filterByStatus(status, button) {
AutoTaskState.currentFilter = status;
// Update active filter
document.querySelectorAll(".status-filter").forEach((filter) => {
filter.classList.remove("active");
});
button.classList.add("active");
// Trigger HTMX request for intent list
htmx.ajax("GET", `/api/autotask/list?status=${status}`, {
target: "#intent-list",
swap: "innerHTML",
});
}
function refreshTasks() { function refreshTasks() {
const filter = AutoTaskState.currentFilter; const filter = AutoTaskState.currentFilter;
htmx.ajax("GET", `/api/autotask/list?filter=${filter}`, { htmx.ajax("GET", `/api/autotask/list?filter=${filter}`, {
@ -265,6 +900,176 @@ function refreshTasks() {
updateStats(); updateStats();
} }
function refreshIntents() {
const status = AutoTaskState.currentFilter;
htmx.ajax("GET", `/api/autotask/list?status=${status}`, {
target: "#intent-list",
swap: "innerHTML",
});
updateStats();
}
// =============================================================================
// MODAL FUNCTIONS - SENTIENT
// =============================================================================
function showNewIntentModal() {
const modal = document.getElementById("new-intent-modal");
if (modal) {
modal.style.display = "flex";
document.body.classList.add("modal-open");
setTimeout(() => {
document.getElementById("intent-input")?.focus();
}, 100);
}
}
function closeNewIntentModal() {
const modal = document.getElementById("new-intent-modal");
if (modal) {
modal.style.display = "none";
document.body.classList.remove("modal-open");
}
}
function openDecisionModal(intentId) {
const modal = document.getElementById("decision-modal");
if (!modal) return;
modal.style.display = "flex";
document.body.classList.add("modal-open");
const content = document.getElementById("decision-content");
if (content) {
content.innerHTML = `
<div class="loading-state">
<div class="spinner"></div>
<span>Loading decision options...</span>
</div>
`;
}
// Fetch decision details
fetch(`/api/autotask/${intentId}/decisions`)
.then((response) => response.json())
.then((decisions) => {
renderDecisionContent(intentId, decisions);
})
.catch((error) => {
console.error("Failed to load decisions:", error);
if (content) {
content.innerHTML = `
<div class="detail-placeholder">
<span class="placeholder-icon"></span>
<p>Failed to load decision options</p>
</div>
`;
}
});
}
function renderDecisionContent(intentId, decisions) {
const content = document.getElementById("decision-content");
if (!content || !decisions || decisions.length === 0) {
if (content) {
content.innerHTML = `
<div class="detail-placeholder">
<span class="placeholder-icon"></span>
<p>No pending decisions</p>
</div>
`;
}
return;
}
const decision = decisions[0];
content.innerHTML = `
<div class="decision-detail">
<h3>${escapeHtml(decision.title)}</h3>
<p class="decision-desc">${escapeHtml(decision.description)}</p>
<div class="decision-options-list">
${decision.options
.map(
(opt, idx) => `
<label class="decision-option-item ${opt.recommended ? "recommended" : ""}">
<input type="radio" name="decision_option" value="${opt.id}" ${idx === 0 ? "checked" : ""}>
<div class="option-content">
<span class="option-label">${escapeHtml(opt.label)}</span>
${opt.recommended ? '<span class="recommended-tag">Recommended</span>' : ""}
<p class="option-desc">${escapeHtml(opt.description || "")}</p>
</div>
</label>
`,
)
.join("")}
</div>
<div class="form-actions">
<button class="btn-secondary" onclick="closeDecisionModal()">Cancel</button>
<button class="btn-primary" onclick="submitDecisionFromModal('${intentId}', '${decision.id}')">
Submit Decision
</button>
</div>
</div>
`;
}
function submitDecisionFromModal(intentId, decisionId) {
const selectedOption = document.querySelector(
'input[name="decision_option"]:checked',
)?.value;
if (!selectedOption) {
showToast("Please select an option", "warning");
return;
}
fetch(`/api/autotask/${intentId}/decide`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
decision_id: decisionId,
option_id: selectedOption,
}),
})
.then((response) => response.json())
.then((result) => {
if (result.success) {
showToast("Decision submitted", "success");
closeDecisionModal();
refreshIntents();
if (AutoTaskState.selectedIntentId === intentId) {
loadIntentDetail(intentId);
}
} else {
showToast(`Failed: ${result.error || "Unknown error"}`, "error");
}
})
.catch((error) => {
console.error("Failed to submit decision:", error);
showToast("Failed to submit decision", "error");
});
}
function closeDecisionModal() {
const modal = document.getElementById("decision-modal");
if (modal) {
modal.style.display = "none";
document.body.classList.remove("modal-open");
}
}
function viewDecisionContext(intentId) {
openDecisionModal(intentId);
}
function closeAllModals() {
closeNewIntentModal();
closeDecisionModal();
document.body.classList.remove("modal-open");
}
// ============================================================================= // =============================================================================
// COMPILATION HANDLING // COMPILATION HANDLING
// ============================================================================= // =============================================================================

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -40,10 +40,11 @@ function setupIntentInputHandlers() {
const input = document.getElementById("quick-intent-input"); const input = document.getElementById("quick-intent-input");
const btn = document.getElementById("quick-intent-btn"); const btn = document.getElementById("quick-intent-btn");
if (input) { if (input && btn) {
input.addEventListener("keypress", function (e) { input.addEventListener("keypress", function (e) {
if (e.key === "Enter" && input.value.trim()) { if (e.key === "Enter" && input.value.trim()) {
btn.click(); e.preventDefault();
htmx.trigger(btn, "click");
} }
}); });
} }
@ -106,56 +107,441 @@ function setupIntentInputHandlers() {
function initWebSocket() { function initWebSocket() {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/ws/tasks`; const wsUrl = `${protocol}//${window.location.host}/ws/task-progress`;
console.log("[Tasks WS] Attempting connection to:", wsUrl);
try { try {
TasksState.wsConnection = new WebSocket(wsUrl); TasksState.wsConnection = new WebSocket(wsUrl);
TasksState.wsConnection.onopen = function () { TasksState.wsConnection.onopen = function () {
console.log("[Sentient Tasks] WebSocket connected"); console.log("[Tasks WS] WebSocket connected successfully");
addAgentLog("info", "[SYSTEM] Connected to task orchestrator"); addAgentLog("info", "[SYSTEM] Connected to task orchestrator");
}; };
TasksState.wsConnection.onmessage = function (event) { TasksState.wsConnection.onmessage = function (event) {
handleWebSocketMessage(JSON.parse(event.data)); 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 () { TasksState.wsConnection.onclose = function (event) {
console.log("[Sentient Tasks] WebSocket disconnected, reconnecting..."); console.log(
"[Tasks WS] WebSocket disconnected, code:",
event.code,
"reason:",
event.reason,
);
setTimeout(initWebSocket, 5000); setTimeout(initWebSocket, 5000);
}; };
TasksState.wsConnection.onerror = function (error) { TasksState.wsConnection.onerror = function (error) {
console.error("[Sentient Tasks] WebSocket error:", error); console.error("[Tasks WS] WebSocket error:", error);
}; };
} catch (e) { } catch (e) {
console.warn("[Sentient Tasks] WebSocket not available"); console.error("[Tasks WS] Failed to create WebSocket:", e);
} }
} }
function handleWebSocketMessage(data) { function handleWebSocketMessage(data) {
console.log("[Tasks WS] handleWebSocketMessage called with type:", data.type);
switch (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": case "task_update":
updateTaskCard(data.task); updateTaskCard(data.task);
if (data.task.id === TasksState.selectedTaskId) { if (data.task && data.task.id === TasksState.selectedTaskId) {
updateTaskDetail(data.task); updateTaskDetail(data.task);
} }
break; break;
case "step_progress": case "step_progress":
updateStepProgress(data.taskId, data.step); updateStepProgress(data.taskId, data.step);
break; break;
case "agent_log": case "agent_log":
addAgentLog(data.level, data.message); addAgentLog(data.level, data.message);
break; break;
case "decision_required": case "decision_required":
showDecisionRequired(data.decision); showDecisionRequired(data.decision);
break; break;
case "task_completed": }
onTaskCompleted(data.task); }
break;
case "task_failed": function updateActivityMetrics(activity) {
onTaskFailed(data.task, data.error); if (!activity) return;
break;
const metricsEl = document.getElementById("floating-activity-metrics");
if (!metricsEl) return;
let html = "";
if (activity.phase) {
html += `<div class="metric-row"><span class="metric-label">Phase:</span> <span class="metric-value phase-${activity.phase}">${activity.phase.toUpperCase()}</span></div>`;
}
if (activity.items_processed !== undefined) {
const total = activity.items_total ? `/${activity.items_total}` : "";
html += `<div class="metric-row"><span class="metric-label">Processed:</span> <span class="metric-value">${activity.items_processed}${total} items</span></div>`;
}
if (activity.speed_per_min) {
html += `<div class="metric-row"><span class="metric-label">Speed:</span> <span class="metric-value">~${activity.speed_per_min.toFixed(1)} items/min</span></div>`;
}
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 += `<div class="metric-row"><span class="metric-label">ETA:</span> <span class="metric-value">${eta}</span></div>`;
}
if (activity.bytes_processed) {
const kb = (activity.bytes_processed / 1024).toFixed(1);
html += `<div class="metric-row"><span class="metric-label">Generated:</span> <span class="metric-value">${kb} KB</span></div>`;
}
if (activity.tokens_used) {
html += `<div class="metric-row"><span class="metric-label">Tokens:</span> <span class="metric-value">${activity.tokens_used.toLocaleString()}</span></div>`;
}
if (activity.files_created && activity.files_created.length > 0) {
html += `<div class="metric-row"><span class="metric-label">Files:</span> <span class="metric-value">${activity.files_created.length} created</span></div>`;
}
if (activity.tables_created && activity.tables_created.length > 0) {
html += `<div class="metric-row"><span class="metric-label">Tables:</span> <span class="metric-value">${activity.tables_created.length} synced</span></div>`;
}
if (activity.current_item) {
html += `<div class="metric-row current-item"><span class="metric-label">Current:</span> <span class="metric-value">${activity.current_item}</span></div>`;
}
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 = `
<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-activity-metrics" id="floating-activity-metrics"></div>
<div class="floating-progress-terminal" id="floating-progress-terminal">
<div class="terminal-header">
<span class="terminal-title">LIVE AGENT ACTIVITY</span>
<span class="terminal-status" id="terminal-status"></span>
</div>
<div class="terminal-content" id="floating-progress-log"></div>
</div>
</div>
`;
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 = `<span class="log-time">${timestamp}</span> <span class="log-step">[${step.toUpperCase()}]</span> ${message}`;
if (activity) {
if (activity.current_item) {
logContent += `<span class="log-current"> → ${activity.current_item}</span>`;
}
if (activity.items_processed !== undefined && activity.items_total) {
logContent += `<span class="log-progress"> (${activity.items_processed}/${activity.items_total})</span>`;
}
if (activity.bytes_processed) {
const kb = (activity.bytes_processed / 1024).toFixed(1);
logContent += `<span class="log-bytes"> [${kb} KB]</span>`;
}
} else if (details) {
logContent += `<span class="log-details"> → ${details}</span>`;
}
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 = `<pre class="summary-pre">${summaryText}</pre>`;
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 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 = `
<div class="result-card">
<div class="result-message">${data.message}</div>
<div class="result-progress">
<div class="result-progress-bar" style="width: ${percent}%"></div>
</div>
<div style="margin-top:8px;font-size:12px;color:var(--sentient-text-muted);">
Step ${data.current_step}/${data.total_steps} (${percent}%)
</div>
</div>
`;
}
} }
} }
@ -904,47 +1290,41 @@ document.addEventListener("htmx:afterRequest", function (event) {
// Update counts for new filters // Update counts for new filters
function updateFilterCounts() { function updateFilterCounts() {
fetch("/api/tasks/stats") fetch("/api/tasks/stats/json")
.then((response) => response.json()) .then((response) => response.json())
.then((stats) => { .then((stats) => {
if (stats.pending_count !== undefined) { if (stats.total !== undefined) {
document.getElementById("count-pending").textContent = const el = document.getElementById("count-all");
stats.pending_count; if (el) el.textContent = stats.total;
} }
if (stats.goals_count !== undefined) { if (stats.completed !== undefined) {
document.getElementById("count-goals").textContent = stats.goals_count; const el = document.getElementById("count-complete");
if (el) el.textContent = stats.completed;
} }
if (stats.schedulers_count !== undefined) { if (stats.active !== undefined) {
document.getElementById("count-schedulers").textContent = const el = document.getElementById("count-active");
stats.schedulers_count; if (el) el.textContent = stats.active;
} }
if (stats.monitors_count !== undefined) { if (stats.awaiting !== undefined) {
document.getElementById("count-monitors").textContent = const el = document.getElementById("count-awaiting");
stats.monitors_count; 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(() => {}); .catch((e) => console.warn("Failed to load task stats:", e));
} }
// Call updateFilterCounts on load // Call updateFilterCounts on load
document.addEventListener("DOMContentLoaded", updateFilterCounts); document.addEventListener("DOMContentLoaded", updateFilterCounts);
document.body.addEventListener("taskCreated", updateFilterCounts); document.body.addEventListener("taskCreated", updateFilterCounts);
// =============================================================================
// 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);