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:
parent
3f95c4645d
commit
4a3eb0cc4f
12 changed files with 7482 additions and 3842 deletions
|
|
@ -244,6 +244,13 @@ struct WsQuery {
|
|||
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(
|
||||
ws: WebSocketUpgrade,
|
||||
State(state): State<AppState>,
|
||||
|
|
@ -252,6 +259,140 @@ async fn ws_proxy(
|
|||
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) = ¶ms.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)]
|
||||
async fn handle_ws_proxy(client_socket: WebSocket, state: AppState, params: WsQuery) {
|
||||
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> {
|
||||
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> {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Drive Module Styles */
|
||||
/* Drive Module Styles v1.0 */
|
||||
|
||||
.drive-container {
|
||||
display: flex;
|
||||
|
|
@ -75,6 +75,12 @@
|
|||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.storage-detail {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Drive Main */
|
||||
.drive-main {
|
||||
flex: 1;
|
||||
|
|
@ -295,7 +301,7 @@
|
|||
|
||||
.file-list-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 120px 120px 80px;
|
||||
grid-template-columns: 1fr 120px 100px 80px;
|
||||
gap: 16px;
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
|
|
@ -305,31 +311,66 @@
|
|||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.file-list-item {
|
||||
.file-list-header .file-col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-list-item,
|
||||
.drive-file-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 120px 120px 80px;
|
||||
grid-template-columns: 1fr 120px 100px 80px;
|
||||
gap: 16px;
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-list-item:hover {
|
||||
.file-list-item:hover,
|
||||
.drive-file-item: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;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 14px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-list-name svg {
|
||||
.file-list-name svg,
|
||||
.file-col.file-name-col svg {
|
||||
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 {
|
||||
border: 2px dashed var(--border);
|
||||
|
|
@ -361,39 +402,579 @@
|
|||
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 {
|
||||
position: fixed;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
min-width: 180px;
|
||||
border-radius: 12px;
|
||||
box-shadow:
|
||||
0 12px 40px rgba(0, 0, 0, 0.2),
|
||||
0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
min-width: 200px;
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
margin: 4px 0;
|
||||
background: linear-gradient(
|
||||
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 */
|
||||
|
|
@ -407,4 +988,15 @@
|
|||
padding: 12px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.editor-modal-content {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.editor-header,
|
||||
.editor-footer {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<!-- Drive - File Management -->
|
||||
<!-- Drive - File Management v1.0 -->
|
||||
<link rel="stylesheet" href="drive/drive.css" />
|
||||
|
||||
<div class="drive-container" id="drive-app">
|
||||
|
|
@ -22,11 +22,7 @@
|
|||
|
||||
<!-- Quick Actions -->
|
||||
<div class="drive-sidebar-actions">
|
||||
<button
|
||||
class="btn-primary-full"
|
||||
id="upload-btn"
|
||||
onclick="uploadFile()"
|
||||
>
|
||||
<button class="btn-primary-full" id="upload-btn">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
|
|
@ -41,11 +37,7 @@
|
|||
</svg>
|
||||
<span>Upload</span>
|
||||
</button>
|
||||
<button
|
||||
class="btn-secondary-full"
|
||||
id="new-folder-btn"
|
||||
onclick="createFolder()"
|
||||
>
|
||||
<button class="btn-secondary-full" id="new-folder-btn">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
|
|
@ -147,9 +139,14 @@
|
|||
<!-- Storage -->
|
||||
<div class="drive-storage">
|
||||
<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 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>
|
||||
</aside>
|
||||
|
||||
|
|
@ -158,15 +155,40 @@
|
|||
<!-- Toolbar -->
|
||||
<div class="drive-toolbar">
|
||||
<div class="drive-toolbar-left">
|
||||
<select
|
||||
id="bucket-selector"
|
||||
class="bucket-selector"
|
||||
style="display: none"
|
||||
>
|
||||
<!-- Populated by JavaScript -->
|
||||
</select>
|
||||
<div class="drive-breadcrumb">
|
||||
<button
|
||||
class="breadcrumb-item"
|
||||
onclick="navigateTo('root')"
|
||||
onclick="DriveModule.loadFiles('')"
|
||||
>
|
||||
My Drive
|
||||
</button>
|
||||
<span class="breadcrumb-sep">/</span>
|
||||
<span class="breadcrumb-current">Projects</span>
|
||||
</div>
|
||||
</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 class="drive-toolbar-right">
|
||||
|
|
@ -213,262 +235,130 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File List -->
|
||||
<!-- File Content Area - Populated by JavaScript -->
|
||||
<div class="drive-content" id="drive-content">
|
||||
<!-- List Header -->
|
||||
<div class="drive-list-header">
|
||||
<div class="file-col file-name-col">Name</div>
|
||||
<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 class="loading-state">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading files...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="drive-file-item folder"
|
||||
data-id="2"
|
||||
onclick="openFolder(this)"
|
||||
<!-- Selection Bar -->
|
||||
<div class="selection-bar" id="selection-bar" style="display: none">
|
||||
<span class="selection-count"
|
||||
><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
|
||||
width="20"
|
||||
height="20"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="#5f6368"
|
||||
stroke="none"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect
|
||||
x="9"
|
||||
y="9"
|
||||
width="13"
|
||||
height="13"
|
||||
rx="2"
|
||||
ry="2"
|
||||
></rect>
|
||||
<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>
|
||||
<span>Images</span>
|
||||
</div>
|
||||
<div class="file-col file-modified-col">Dec 14, 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
|
||||
class="drive-file-item folder"
|
||||
data-id="3"
|
||||
onclick="openFolder(this)"
|
||||
</button>
|
||||
<button
|
||||
class="action-btn"
|
||||
onclick="DriveModule.cutSelected()"
|
||||
title="Cut"
|
||||
>
|
||||
<div class="file-col file-name-col">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="#5f6368"
|
||||
stroke="none"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
<circle cx="6" cy="6" r="3"></circle>
|
||||
<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>
|
||||
<span>Source Code</span>
|
||||
</div>
|
||||
<div class="file-col file-modified-col">Dec 12, 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>
|
||||
|
||||
<!-- Files -->
|
||||
<div
|
||||
class="drive-file-item"
|
||||
data-id="10"
|
||||
onclick="selectFile(this)"
|
||||
</button>
|
||||
<button
|
||||
class="action-btn danger"
|
||||
onclick="DriveModule.deleteSelected()"
|
||||
title="Delete"
|
||||
>
|
||||
<div class="file-col file-name-col">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="#ea4335"
|
||||
stroke="none"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<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>
|
||||
<span>project-report.pdf</span>
|
||||
</div>
|
||||
<div class="file-col file-modified-col">Dec 15, 2024</div>
|
||||
<div class="file-col file-size-col">2.4 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="11"
|
||||
onclick="selectFile(this)"
|
||||
</button>
|
||||
<button
|
||||
class="close-selection"
|
||||
onclick="DriveModule.clearSelection()"
|
||||
title="Clear selection"
|
||||
>
|
||||
<div class="file-col file-name-col">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="#4285f4"
|
||||
stroke="none"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
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="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
<span>meeting-notes.docx</span>
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- 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>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -718,6 +718,13 @@
|
|||
</svg>
|
||||
Open
|
||||
</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">
|
||||
<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>
|
||||
|
|
@ -1857,13 +1864,13 @@
|
|||
}
|
||||
|
||||
// Context menu handling
|
||||
let contextTarget = null;
|
||||
window.contextTarget = null;
|
||||
|
||||
document.addEventListener('contextmenu', function(e) {
|
||||
const fileCard = e.target.closest('.file-card');
|
||||
if (fileCard) {
|
||||
e.preventDefault();
|
||||
contextTarget = fileCard;
|
||||
window.contextTarget = fileCard;
|
||||
const menu = document.getElementById('context-menu');
|
||||
menu.style.display = 'block';
|
||||
menu.style.left = e.pageX + 'px';
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<link rel="stylesheet" href="meet/meet.css" />
|
||||
<link rel="stylesheet" href="paper/paper.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="monitoring/monitoring.css" />
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,492 +1,479 @@
|
|||
<div class="autotask-container">
|
||||
<!-- Header -->
|
||||
<div class="autotask-header">
|
||||
<div class="header-content">
|
||||
<h1 class="autotask-title">
|
||||
<span class="autotask-icon">⚡</span>
|
||||
Auto Tasks
|
||||
<span class="beta-badge">PREMIUM</span>
|
||||
</h1>
|
||||
<p class="autotask-subtitle">Intelligent self-executing tasks powered by AI</p>
|
||||
<div class="autotask-container" data-theme="sentient">
|
||||
<!-- Top Navigation Bar -->
|
||||
<div class="autotask-topbar">
|
||||
<div class="topbar-left">
|
||||
<div class="nav-tabs">
|
||||
<button class="nav-tab active">Dashboard</button>
|
||||
<button class="nav-tab">Analytics</button>
|
||||
</div>
|
||||
</div>
|
||||
<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 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>
|
||||
|
||||
<!-- Intent Input Section -->
|
||||
<div class="intent-section">
|
||||
<div class="intent-header">
|
||||
<span class="section-icon">💡</span>
|
||||
<h2>What would you like to accomplish?</h2>
|
||||
<!-- Status Filter Pills -->
|
||||
<div class="status-filters">
|
||||
<button
|
||||
class="status-filter"
|
||||
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>
|
||||
<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 class="intent-input-wrapper">
|
||||
</div>
|
||||
|
||||
<!-- 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
|
||||
name="intent"
|
||||
id="intent-input"
|
||||
class="intent-input"
|
||||
placeholder="Describe your task in natural language... Examples: • Make a financial CRM for Deloitte with client management and reporting • Create a website that collects leads and sends them to Salesforce • Build an automated email campaign for product launch • Analyze sales data and generate weekly reports"
|
||||
rows="4"
|
||||
name="intent"
|
||||
class="intent-textarea"
|
||||
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
|
||||
></textarea>
|
||||
<div class="intent-actions">
|
||||
<select name="execution_mode" class="execution-mode-select">
|
||||
<option value="semi-automatic">Semi-Automatic (Recommended)</option>
|
||||
<option value="supervised">Supervised (Step-by-step)</option>
|
||||
<option value="fully-automatic">Fully Automatic</option>
|
||||
<option value="dry-run">Dry Run (Simulate Only)</option>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Execution Mode</label>
|
||||
<select name="execution_mode" class="form-select">
|
||||
<option value="semi-automatic">
|
||||
Semi-Automatic
|
||||
</option>
|
||||
<option value="supervised">Supervised</option>
|
||||
<option value="fully-automatic">
|
||||
Fully Automatic
|
||||
</option>
|
||||
</select>
|
||||
<select name="priority" class="priority-select">
|
||||
<option value="medium">Medium Priority</option>
|
||||
<option value="critical">Critical</option>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Priority</label>
|
||||
<select name="priority" class="form-select">
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
<option value="low">Low</option>
|
||||
<option value="background">Background</option>
|
||||
</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 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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Pending Decisions Modal -->
|
||||
<div class="modal" id="decision-modal" style="display: none;">
|
||||
<div class="modal-backdrop" onclick="closeDecisionModal()"></div>
|
||||
<div class="modal-content decision-modal-content">
|
||||
<!-- Decision Modal -->
|
||||
<div class="modal-overlay" id="decision-modal" style="display: none">
|
||||
<div class="modal-container modal-lg">
|
||||
<div class="modal-header">
|
||||
<h3><span class="modal-icon">🤔</span> Decision Required</h3>
|
||||
<button class="modal-close" onclick="closeDecisionModal()">×</button>
|
||||
<h2>Decision Required</h2>
|
||||
<button class="modal-close" onclick="closeDecisionModal()">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" id="decision-content">
|
||||
<!-- Decision content loaded dynamically -->
|
||||
</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>
|
||||
<!-- Intent Card Template -->
|
||||
<template id="intent-card-template">
|
||||
<div
|
||||
class="intent-card"
|
||||
data-intent-id="${id}"
|
||||
data-status="${status}"
|
||||
onclick="selectIntent('${id}')"
|
||||
>
|
||||
<div class="intent-card-header">
|
||||
<h3 class="intent-title">${title}</h3>
|
||||
<div class="intent-status-indicator ${status_class}"></div>
|
||||
</div>
|
||||
<div class="modal-body" id="approval-content">
|
||||
<!-- Approval content loaded dynamically -->
|
||||
|
||||
<div class="intent-progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: ${progress}%"></div>
|
||||
</div>
|
||||
<div class="progress-info">
|
||||
<span class="progress-steps"
|
||||
>${current_step}/${total_steps} Steps</span
|
||||
>
|
||||
<span class="progress-percent">${progress}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="intent-status-section">
|
||||
<div class="status-label">STATUS</div>
|
||||
<div class="status-row">
|
||||
<span class="status-icon">${status_icon}</span>
|
||||
<span class="status-text">${status_display}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="intent-current-task">
|
||||
<div class="current-task-label">${current_task_name}</div>
|
||||
<div class="current-task-desc">${current_task_description}</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>
|
||||
|
||||
<button
|
||||
class="btn-detail-view"
|
||||
onclick="event.stopPropagation(); viewDetailedIntent('${id}')"
|
||||
>
|
||||
${action_button_text}
|
||||
</button>
|
||||
</div>
|
||||
</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>
|
||||
<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>
|
||||
|
||||
<!-- Simulation Preview Modal -->
|
||||
<div class="modal" id="simulation-modal" style="display: none;">
|
||||
<div class="modal-backdrop" onclick="closeSimulationModal()"></div>
|
||||
<div class="modal-content simulation-modal-content">
|
||||
<div class="modal-header">
|
||||
<h3><span class="modal-icon">🔮</span> Impact Simulation</h3>
|
||||
<button class="modal-close" onclick="closeSimulationModal()">×</button>
|
||||
<!-- 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="modal-body" id="simulation-content">
|
||||
<!-- Simulation content loaded dynamically -->
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<!-- Task Item Template (for reference, actual rendering done server-side) -->
|
||||
<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>
|
||||
<!-- Toast Container -->
|
||||
<div class="toast-container" id="toast-container"></div>
|
||||
|
||||
<div class="task-progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: ${progress}%"></div>
|
||||
</div>
|
||||
<span class="progress-text">${current_step}/${total_steps} steps (${progress}%)</span>
|
||||
</div>
|
||||
|
||||
<div class="task-details">
|
||||
<p class="task-intent">${intent}</p>
|
||||
|
||||
<!-- 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 class="task-actions">
|
||||
<button class="btn-action btn-view" onclick="viewTaskDetails('${id}')" title="View Details">
|
||||
<span>👁️</span> Details
|
||||
</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-text">${status_text}</span>
|
||||
</div>
|
||||
<div class="simulation-confidence">
|
||||
Confidence: ${confidence}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="impact-overview">
|
||||
<h4>Impact Assessment</h4>
|
||||
<div class="impact-grid">
|
||||
<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>
|
||||
|
||||
<div class="step-outcomes">
|
||||
<h4>Step-by-Step Predictions</h4>
|
||||
<div class="outcomes-list">
|
||||
${step_outcomes_html}
|
||||
</div>
|
||||
</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 class="btn-primary" onclick="proceedAfterSimulation('${task_id}')" ${proceed_disabled}>
|
||||
<span>🚀</span> Proceed with Execution
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script src="autotask.js"></script>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/* =============================================================================
|
||||
AUTO TASK JAVASCRIPT - Intelligent Self-Executing Task Interface
|
||||
Premium VIP Mode Functionality
|
||||
AUTO TASK JAVASCRIPT - Sentient Theme
|
||||
Intelligent Self-Executing Task Interface
|
||||
============================================================================= */
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -8,13 +8,17 @@
|
|||
// =============================================================================
|
||||
|
||||
const AutoTaskState = {
|
||||
currentFilter: "all",
|
||||
tasks: [],
|
||||
currentFilter: "active",
|
||||
selectedIntentId: null,
|
||||
intents: [],
|
||||
compiledPlan: null,
|
||||
pendingDecisions: [],
|
||||
pendingApprovals: [],
|
||||
refreshInterval: null,
|
||||
wsConnection: null,
|
||||
progressWsConnection: null,
|
||||
activeTaskProgress: {},
|
||||
llmOutputStream: [],
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -29,6 +33,9 @@ function initAutoTask() {
|
|||
// Initialize WebSocket for real-time updates
|
||||
initWebSocket();
|
||||
|
||||
// Initialize task progress WebSocket
|
||||
initTaskProgressWebSocket();
|
||||
|
||||
// Start auto-refresh
|
||||
startAutoRefresh();
|
||||
|
||||
|
|
@ -41,7 +48,101 @@ function initAutoTask() {
|
|||
// Setup keyboard shortcuts
|
||||
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) {
|
||||
switch (data.type) {
|
||||
case "task_update":
|
||||
updateTaskInList(data.task);
|
||||
case "intent_update":
|
||||
updateIntentInList(data.task || data.intent);
|
||||
break;
|
||||
case "step_progress":
|
||||
updateStepProgress(data.taskId, data.step, data.progress);
|
||||
|
|
@ -91,10 +667,12 @@ function handleWebSocketMessage(data) {
|
|||
showApprovalNotification(data.approval);
|
||||
break;
|
||||
case "task_completed":
|
||||
onTaskCompleted(data.task);
|
||||
case "intent_completed":
|
||||
onIntentCompleted(data.task || data.intent);
|
||||
break;
|
||||
case "task_failed":
|
||||
onTaskFailed(data.task, data.error);
|
||||
case "intent_failed":
|
||||
onIntentFailed(data.task || data.intent, data.error);
|
||||
break;
|
||||
case "stats_update":
|
||||
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
|
||||
// =============================================================================
|
||||
|
|
@ -212,28 +825,33 @@ function updateStats() {
|
|||
}
|
||||
|
||||
function updateStatsFromData(stats) {
|
||||
// Header stats
|
||||
document.getElementById("stat-running").textContent = stats.running || 0;
|
||||
document.getElementById("stat-pending").textContent = stats.pending || 0;
|
||||
document.getElementById("stat-completed").textContent = stats.completed || 0;
|
||||
document.getElementById("stat-approval").textContent =
|
||||
stats.pending_approval || 0;
|
||||
// Sentient filter counts
|
||||
const countComplete = document.getElementById("count-complete");
|
||||
const countActive = document.getElementById("count-active");
|
||||
const countAwaiting = document.getElementById("count-awaiting");
|
||||
const countPaused = document.getElementById("count-paused");
|
||||
const countBlocked = document.getElementById("count-blocked");
|
||||
const timeSaved = document.getElementById("time-saved");
|
||||
|
||||
// Filter counts
|
||||
document.getElementById("count-all").textContent = stats.total || 0;
|
||||
document.getElementById("count-running").textContent = stats.running || 0;
|
||||
document.getElementById("count-approval").textContent =
|
||||
stats.pending_approval || 0;
|
||||
document.getElementById("count-decision").textContent =
|
||||
stats.pending_decision || 0;
|
||||
if (countComplete) countComplete.textContent = stats.completed || 0;
|
||||
if (countActive) countActive.textContent = stats.running || stats.active || 0;
|
||||
if (countAwaiting)
|
||||
countAwaiting.textContent = stats.pending_decision || stats.awaiting || 0;
|
||||
if (countPaused) countPaused.textContent = stats.paused || 0;
|
||||
if (countBlocked)
|
||||
countBlocked.textContent = stats.blocked || stats.failed || 0;
|
||||
if (timeSaved) timeSaved.textContent = stats.time_saved || "0 hrs this week";
|
||||
|
||||
// Highlight if approvals needed
|
||||
const approvalStat = document.querySelector(".stat-item.highlight");
|
||||
if (approvalStat && stats.pending_approval > 0) {
|
||||
approvalStat.classList.add("attention");
|
||||
} else if (approvalStat) {
|
||||
approvalStat.classList.remove("attention");
|
||||
}
|
||||
// Legacy support
|
||||
const statRunning = document.getElementById("stat-running");
|
||||
const statPending = document.getElementById("stat-pending");
|
||||
const statCompleted = document.getElementById("stat-completed");
|
||||
const statApproval = document.getElementById("stat-approval");
|
||||
|
||||
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() {
|
||||
const filter = AutoTaskState.currentFilter;
|
||||
htmx.ajax("GET", `/api/autotask/list?filter=${filter}`, {
|
||||
|
|
@ -265,6 +900,176 @@ function refreshTasks() {
|
|||
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
|
||||
// =============================================================================
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -40,10 +40,11 @@ function setupIntentInputHandlers() {
|
|||
const input = document.getElementById("quick-intent-input");
|
||||
const btn = document.getElementById("quick-intent-btn");
|
||||
|
||||
if (input) {
|
||||
if (input && btn) {
|
||||
input.addEventListener("keypress", function (e) {
|
||||
if (e.key === "Enter" && input.value.trim()) {
|
||||
btn.click();
|
||||
e.preventDefault();
|
||||
htmx.trigger(btn, "click");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -106,56 +107,441 @@ function setupIntentInputHandlers() {
|
|||
|
||||
function initWebSocket() {
|
||||
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 {
|
||||
TasksState.wsConnection = new WebSocket(wsUrl);
|
||||
|
||||
TasksState.wsConnection.onopen = function () {
|
||||
console.log("[Sentient Tasks] WebSocket connected");
|
||||
console.log("[Tasks WS] WebSocket connected successfully");
|
||||
addAgentLog("info", "[SYSTEM] Connected to task orchestrator");
|
||||
};
|
||||
|
||||
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 () {
|
||||
console.log("[Sentient Tasks] WebSocket disconnected, reconnecting...");
|
||||
TasksState.wsConnection.onclose = function (event) {
|
||||
console.log(
|
||||
"[Tasks WS] WebSocket disconnected, code:",
|
||||
event.code,
|
||||
"reason:",
|
||||
event.reason,
|
||||
);
|
||||
setTimeout(initWebSocket, 5000);
|
||||
};
|
||||
|
||||
TasksState.wsConnection.onerror = function (error) {
|
||||
console.error("[Sentient Tasks] WebSocket error:", error);
|
||||
console.error("[Tasks WS] WebSocket error:", error);
|
||||
};
|
||||
} catch (e) {
|
||||
console.warn("[Sentient Tasks] WebSocket not available");
|
||||
console.error("[Tasks WS] Failed to create WebSocket:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function handleWebSocketMessage(data) {
|
||||
console.log("[Tasks WS] handleWebSocketMessage called with type:", data.type);
|
||||
|
||||
switch (data.type) {
|
||||
case "connected":
|
||||
console.log("[Tasks WS] Connected to task progress stream");
|
||||
addAgentLog("info", "[SYSTEM] Task progress stream connected");
|
||||
break;
|
||||
|
||||
case "task_started":
|
||||
console.log(
|
||||
"[Tasks WS] TASK_STARTED - showing floating progress:",
|
||||
data.message,
|
||||
);
|
||||
addAgentLog("accent", `[TASK] Started: ${data.message}`);
|
||||
showFloatingProgress(data.message);
|
||||
updateFloatingProgressBar(0, data.total_steps, data.message);
|
||||
updateActivityMetrics(data.activity);
|
||||
updateProgressUI(data);
|
||||
break;
|
||||
|
||||
case "task_progress":
|
||||
console.log(
|
||||
"[Tasks WS] TASK_PROGRESS - step:",
|
||||
data.step,
|
||||
"message:",
|
||||
data.message,
|
||||
);
|
||||
addAgentLog("info", `[${data.step}] ${data.message}`);
|
||||
if (data.activity) {
|
||||
updateActivityMetrics(data.activity);
|
||||
if (data.activity.current_item) {
|
||||
addAgentLog("info", ` → Processing: ${data.activity.current_item}`);
|
||||
}
|
||||
} else if (data.details) {
|
||||
addAgentLog("info", ` → ${data.details}`);
|
||||
}
|
||||
updateFloatingProgressBar(
|
||||
data.current_step,
|
||||
data.total_steps,
|
||||
data.message,
|
||||
data.step,
|
||||
data.details,
|
||||
data.activity,
|
||||
);
|
||||
updateProgressUI(data);
|
||||
break;
|
||||
|
||||
case "task_completed":
|
||||
console.log("[Tasks WS] TASK_COMPLETED:", data.message);
|
||||
addAgentLog("success", `[COMPLETE] ${data.message}`);
|
||||
if (data.activity) {
|
||||
updateActivityMetrics(data.activity);
|
||||
logFinalStats(data.activity);
|
||||
}
|
||||
completeFloatingProgress(data.message, data.activity);
|
||||
updateProgressUI(data);
|
||||
onTaskCompleted(data);
|
||||
if (typeof htmx !== "undefined") {
|
||||
htmx.trigger(document.body, "taskCreated");
|
||||
}
|
||||
break;
|
||||
|
||||
case "task_error":
|
||||
console.log("[Tasks WS] TASK_ERROR:", data.error || data.message);
|
||||
addAgentLog("error", `[ERROR] ${data.error || data.message}`);
|
||||
errorFloatingProgress(data.error || data.message);
|
||||
onTaskFailed(data, data.error);
|
||||
break;
|
||||
|
||||
case "task_update":
|
||||
updateTaskCard(data.task);
|
||||
if (data.task.id === TasksState.selectedTaskId) {
|
||||
if (data.task && data.task.id === TasksState.selectedTaskId) {
|
||||
updateTaskDetail(data.task);
|
||||
}
|
||||
break;
|
||||
|
||||
case "step_progress":
|
||||
updateStepProgress(data.taskId, data.step);
|
||||
break;
|
||||
|
||||
case "agent_log":
|
||||
addAgentLog(data.level, data.message);
|
||||
break;
|
||||
|
||||
case "decision_required":
|
||||
showDecisionRequired(data.decision);
|
||||
break;
|
||||
case "task_completed":
|
||||
onTaskCompleted(data.task);
|
||||
break;
|
||||
case "task_failed":
|
||||
onTaskFailed(data.task, data.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function updateActivityMetrics(activity) {
|
||||
if (!activity) return;
|
||||
|
||||
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
|
||||
function updateFilterCounts() {
|
||||
fetch("/api/tasks/stats")
|
||||
fetch("/api/tasks/stats/json")
|
||||
.then((response) => response.json())
|
||||
.then((stats) => {
|
||||
if (stats.pending_count !== undefined) {
|
||||
document.getElementById("count-pending").textContent =
|
||||
stats.pending_count;
|
||||
if (stats.total !== undefined) {
|
||||
const el = document.getElementById("count-all");
|
||||
if (el) el.textContent = stats.total;
|
||||
}
|
||||
if (stats.goals_count !== undefined) {
|
||||
document.getElementById("count-goals").textContent = stats.goals_count;
|
||||
if (stats.completed !== undefined) {
|
||||
const el = document.getElementById("count-complete");
|
||||
if (el) el.textContent = stats.completed;
|
||||
}
|
||||
if (stats.schedulers_count !== undefined) {
|
||||
document.getElementById("count-schedulers").textContent =
|
||||
stats.schedulers_count;
|
||||
if (stats.active !== undefined) {
|
||||
const el = document.getElementById("count-active");
|
||||
if (el) el.textContent = stats.active;
|
||||
}
|
||||
if (stats.monitors_count !== undefined) {
|
||||
document.getElementById("count-monitors").textContent =
|
||||
stats.monitors_count;
|
||||
if (stats.awaiting !== undefined) {
|
||||
const el = document.getElementById("count-awaiting");
|
||||
if (el) el.textContent = stats.awaiting;
|
||||
}
|
||||
if (stats.paused !== undefined) {
|
||||
const el = document.getElementById("count-paused");
|
||||
if (el) el.textContent = stats.paused;
|
||||
}
|
||||
if (stats.blocked !== undefined) {
|
||||
const el = document.getElementById("count-blocked");
|
||||
if (el) el.textContent = stats.blocked;
|
||||
}
|
||||
if (stats.time_saved !== undefined) {
|
||||
const el = document.getElementById("time-saved-value");
|
||||
if (el) el.textContent = stats.time_saved;
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch((e) => console.warn("Failed to load task stats:", e));
|
||||
}
|
||||
|
||||
// Call updateFilterCounts on load
|
||||
document.addEventListener("DOMContentLoaded", 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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue