mirror of
https://github.com/larchanka/manbot.git
synced 2026-05-13 21:42:08 +00:00
chore: dashboard task view update
This commit is contained in:
@@ -383,7 +383,7 @@ export class ExecutorAgent extends BaseProcess {
|
||||
}
|
||||
}
|
||||
|
||||
this.sendTaskComplete(taskId);
|
||||
this.sendTaskFinalize(taskId);
|
||||
this.sendResponse(request, { taskId, result: aggregated });
|
||||
}
|
||||
|
||||
@@ -949,15 +949,15 @@ export class ExecutorAgent extends BaseProcess {
|
||||
});
|
||||
}
|
||||
|
||||
private sendTaskComplete(taskId: string): void {
|
||||
private sendTaskFinalize(taskId: string): void {
|
||||
this.send({
|
||||
id: randomUUID(),
|
||||
timestamp: Date.now(),
|
||||
from: PROCESS_NAME,
|
||||
to: "task-memory",
|
||||
type: "task.complete",
|
||||
type: "task.updateStatus",
|
||||
version: PROTOCOL_VERSION,
|
||||
payload: { taskId },
|
||||
payload: { taskId, status: "finalizing" },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -480,6 +480,16 @@ export class Orchestrator {
|
||||
if (isRetry) {
|
||||
this.sendToTelegram(chatId, "⏳ Re-planning with error feedback...", true);
|
||||
} else {
|
||||
// Create initial 'planning' task in memory
|
||||
this.sendAndWait(taskMemory, "task.create", {
|
||||
taskId,
|
||||
userId: String(userId),
|
||||
conversationId: conversationId ?? String(chatId),
|
||||
goal,
|
||||
status: "planning",
|
||||
nodes: [],
|
||||
edges: []
|
||||
}).catch(() => { });
|
||||
this.sendToTelegram(chatId, " Planning started...", true);
|
||||
}
|
||||
|
||||
@@ -527,18 +537,15 @@ export class Orchestrator {
|
||||
previousPlan = plan;
|
||||
const nodes = plan.nodes as Array<{ id: string; type: string; service: string; input?: unknown }>;
|
||||
const edges = (plan.edges ?? []) as Array<{ from: string; to: string }>;
|
||||
const taskCreatePayload = {
|
||||
|
||||
// Update task DAG in memory
|
||||
this.sendAndWait(taskMemory, "task.updateDag", {
|
||||
taskId,
|
||||
userId: String(userId),
|
||||
conversationId: conversationId ?? String(chatId),
|
||||
goal,
|
||||
complexity: plan.complexity,
|
||||
nodes: nodes.map((n) => ({ id: n.id, type: n.type, service: n.service, input: n.input })),
|
||||
edges: edges
|
||||
.filter((e) => e && typeof e === "object" && e.from && e.to)
|
||||
.map((e) => ({ fromNode: e.from, toNode: e.to })),
|
||||
};
|
||||
this.sendAndWait(taskMemory, "task.create", taskCreatePayload).catch(() => { });
|
||||
}).catch(() => { });
|
||||
|
||||
this.sendToTelegram(chatId, "💨 Planning complete. Execution started...", true);
|
||||
let execEnv: Envelope;
|
||||
@@ -598,6 +605,8 @@ export class Orchestrator {
|
||||
}
|
||||
}
|
||||
this.sendToTelegram(chatId, text, false, "HTML");
|
||||
// Mark task as completed after message is sent
|
||||
this.sendAndWait(taskMemory, "task.complete", { taskId }).catch(() => { });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -225,8 +225,8 @@ export class DashboardService extends BaseProcess {
|
||||
SELECT t.id, t.goal, t.status, t.complexity, t.updated_at,
|
||||
(SELECT json_group_array(json_object('id', id, 'type', type, 'status', status, 'input', input)) FROM task_nodes WHERE task_id = t.id ORDER BY started_at ASC) as nodes,
|
||||
(SELECT json_group_array(json_object('from', from_node, 'to', to_node)) FROM task_edges WHERE task_id = t.id) as edges
|
||||
FROM tasks t WHERE t.status IN (?, ?) ORDER BY t.updated_at DESC
|
||||
`).all('pending', 'running').map((t: any) => ({
|
||||
FROM tasks t WHERE t.status IN (?, ?, ?, ?) ORDER BY t.updated_at DESC
|
||||
`).all('planning', 'pending', 'running', 'finalizing').map((t: any) => ({
|
||||
...t,
|
||||
nodes: JSON.parse(t.nodes || '[]'),
|
||||
edges: JSON.parse(t.edges || '[]')
|
||||
@@ -263,7 +263,7 @@ export class DashboardService extends BaseProcess {
|
||||
if (!total) return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}"><text x="50%" y="50%" text-anchor="middle" fill="#8b8b8b">No Data</text></svg>`;
|
||||
const radius = 75, circumference = 2 * Math.PI * radius;
|
||||
let offset = 0;
|
||||
const colors: any = { completed: '#0b6e4f', failed: '#df2a5f', pending: '#d9730d', running: '#2383e2' };
|
||||
const colors: any = { completed: '#0b6e4f', failed: '#df2a5f', pending: '#d9730d', running: '#2383e2', planning: '#8b8b8b', finalizing: '#9333ea' };
|
||||
const slices = Object.entries(data).map(([k, v]: [string, any]) => {
|
||||
const p = v / total, dash = (p * circumference) + ' ' + circumference, currentOffset = -offset;
|
||||
offset += p * circumference;
|
||||
|
||||
@@ -236,8 +236,11 @@ function updateDashboard() {
|
||||
if (d.pendingTasks && d.pendingTasks.length > 0) {
|
||||
qs.style.display = "block";
|
||||
qt.innerHTML = d.pendingTasks.map(t => {
|
||||
const isRunning = t.status === 'running';
|
||||
const tc = isRunning ? "running" : "warning";
|
||||
const isRunning = t.status === 'running' || t.status === 'finalizing';
|
||||
let tc = "warning";
|
||||
if (t.status === 'running') tc = "running";
|
||||
else if (t.status === 'planning') tc = "planning";
|
||||
else if (t.status === 'finalizing') tc = "finalizing";
|
||||
const indicator = isRunning ? '<div class="pulse"></div>' : '';
|
||||
return `<tr>
|
||||
<td style="padding-left: 20px; color: var(--text-muted); white-space: nowrap; vertical-align: top; padding-top: 15px; border-bottom: none;">${fmtDate(t.updated_at)}</td>
|
||||
|
||||
@@ -8,6 +8,24 @@
|
||||
--success: #0b6e4f;
|
||||
--error: #df2a5f;
|
||||
--warning: #d9730d;
|
||||
--purple: #9333ea;
|
||||
|
||||
--primary-bg: rgba(35, 131, 226, 0.1);
|
||||
--success-bg: rgba(11, 110, 79, 0.1);
|
||||
--error-bg: rgba(223, 42, 95, 0.1);
|
||||
--warning-bg: rgba(217, 115, 13, 0.1);
|
||||
--planning-bg: rgba(139, 139, 139, 0.1);
|
||||
--finalizing-bg: rgba(147, 51, 234, 0.1);
|
||||
|
||||
--stage-bg: rgba(0, 0, 0, 0.02);
|
||||
--chip-running-bg: rgba(35, 131, 226, 0.05);
|
||||
--chip-completed-bg: rgba(11, 110, 79, 0.05);
|
||||
--chip-failed-bg: rgba(223, 42, 95, 0.05);
|
||||
|
||||
--tooltip-bg: #1e1e1e;
|
||||
--tooltip-text: #ffffff;
|
||||
--tooltip-shadow: rgba(0, 0, 0, 0.2);
|
||||
--tooltip-border: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@@ -21,6 +39,24 @@
|
||||
--success: #529e72;
|
||||
--error: #ff4d4d;
|
||||
--warning: #ffdc4d;
|
||||
--purple: #bf7fff;
|
||||
|
||||
--primary-bg: rgba(46, 167, 255, 0.15);
|
||||
--success-bg: rgba(82, 158, 114, 0.15);
|
||||
--error-bg: rgba(255, 77, 77, 0.15);
|
||||
--warning-bg: rgba(255, 220, 77, 0.15);
|
||||
--planning-bg: rgba(255, 255, 255, 0.05);
|
||||
--finalizing-bg: rgba(191, 127, 255, 0.15);
|
||||
|
||||
--stage-bg: rgba(255, 255, 255, 0.03);
|
||||
--chip-running-bg: rgba(46, 167, 255, 0.08);
|
||||
--chip-completed-bg: rgba(82, 158, 114, 0.08);
|
||||
--chip-failed-bg: rgba(255, 77, 77, 0.08);
|
||||
|
||||
--tooltip-bg: #2a2a2a;
|
||||
--tooltip-text: #e0e0e0;
|
||||
--tooltip-shadow: rgba(0, 0, 0, 0.4);
|
||||
--tooltip-border: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,20 +277,30 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag.success { background: rgba(11, 110, 79, 0.1); color: var(--success); }
|
||||
.tag.error { background: rgba(223, 42, 95, 0.1); color: var(--error); }
|
||||
.tag.warning { background: rgba(217, 115, 13, 0.1); color: var(--warning); }
|
||||
.tag.success { background: var(--success-bg); color: var(--success); }
|
||||
.tag.error { background: var(--error-bg); color: var(--error); }
|
||||
.tag.warning { background: var(--warning-bg); color: var(--warning); }
|
||||
.tag.running {
|
||||
background: rgba(35, 131, 226, 0.1);
|
||||
background: var(--primary-bg);
|
||||
color: var(--primary);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.tag.planning { background: var(--planning-bg); color: var(--text-muted); padding: 2px 8px; border-radius: 4px; }
|
||||
.tag.finalizing {
|
||||
background: var(--finalizing-bg);
|
||||
color: var(--purple);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tag.complexity-small { background: var(--subtle); color: var(--text-muted); border: 1px solid var(--border); }
|
||||
.tag.complexity-medium { background: rgba(35, 131, 226, 0.1); color: var(--primary); }
|
||||
.tag.complexity-large { background: rgba(147, 51, 234, 0.1); color: #9333ea; }
|
||||
.tag.complexity-medium { background: var(--primary-bg); color: var(--primary); }
|
||||
.tag.complexity-large { background: var(--finalizing-bg); color: var(--purple); }
|
||||
|
||||
.pulse {
|
||||
width: 6px;
|
||||
@@ -285,7 +331,7 @@
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
border: 1px dashed var(--border);
|
||||
background: rgba(0,0,0,0.02);
|
||||
background: var(--stage-bg);
|
||||
}
|
||||
.node-chip {
|
||||
font-size: 11px;
|
||||
@@ -302,18 +348,18 @@
|
||||
.node-chip.running {
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
background: rgba(35, 131, 226, 0.05);
|
||||
background: var(--chip-running-bg);
|
||||
font-weight: 600;
|
||||
}
|
||||
.node-chip.completed {
|
||||
border-color: var(--success);
|
||||
color: var(--success);
|
||||
background: rgba(11, 110, 79, 0.05);
|
||||
background: var(--chip-completed-bg);
|
||||
}
|
||||
.node-chip.failed {
|
||||
border-color: var(--error);
|
||||
color: var(--error);
|
||||
background: rgba(223, 42, 95, 0.05);
|
||||
background: var(--chip-failed-bg);
|
||||
}
|
||||
|
||||
.goal-text {
|
||||
@@ -396,15 +442,15 @@
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 6px 10px;
|
||||
background: #1e1e1e;
|
||||
color: #fff;
|
||||
background: var(--tooltip-bg);
|
||||
color: var(--tooltip-text);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
white-space: nowrap;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
box-shadow: 0 4px 12px var(--tooltip-shadow);
|
||||
border: 1px solid var(--tooltip-border);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -416,7 +462,7 @@
|
||||
transform: translateX(-50%);
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: #1e1e1e transparent transparent transparent;
|
||||
border-color: var(--tooltip-bg) transparent transparent transparent;
|
||||
z-index: 1001;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS tasks (
|
||||
user_id TEXT,
|
||||
conversation_id TEXT,
|
||||
goal TEXT NOT NULL,
|
||||
status TEXT CHECK(status IN ('pending','running','completed','failed')),
|
||||
status TEXT CHECK(status IN ('planning','running','finalizing','completed','failed','pending')),
|
||||
complexity TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
@@ -92,6 +92,7 @@ interface TaskCreatePayload {
|
||||
conversationId?: string;
|
||||
goal: string;
|
||||
complexity?: string;
|
||||
status?: "planning" | "pending" | "running" | "finalizing" | "completed" | "failed";
|
||||
nodes: Array<{ id: string; type: string; service: string; input?: unknown }>;
|
||||
edges: Array<{ fromNode: string; toNode: string }>;
|
||||
}
|
||||
@@ -129,6 +130,11 @@ interface TaskGetByConversationIdPayload {
|
||||
conversationId: string;
|
||||
}
|
||||
|
||||
interface TaskUpdateStatusPayload {
|
||||
taskId: string;
|
||||
status: "planning" | "pending" | "running" | "finalizing" | "completed" | "failed";
|
||||
}
|
||||
|
||||
// --- Store ---
|
||||
|
||||
export class TaskMemoryStore {
|
||||
@@ -179,13 +185,14 @@ export class TaskMemoryStore {
|
||||
this.db
|
||||
.prepare(
|
||||
`INSERT INTO tasks (id, user_id, conversation_id, goal, status, complexity, created_at, updated_at, metadata)
|
||||
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?)`,
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
payload.taskId,
|
||||
payload.userId ?? null,
|
||||
payload.conversationId ?? null,
|
||||
payload.goal,
|
||||
payload.status || 'pending',
|
||||
payload.complexity ?? null,
|
||||
now,
|
||||
now,
|
||||
@@ -243,9 +250,9 @@ export class TaskMemoryStore {
|
||||
)
|
||||
.run(status, output, startedAt, completedAt, taskId, nodeId);
|
||||
|
||||
// Also update task status to 'running' if it was 'pending'
|
||||
// Also update task status to 'running' if it was 'pending' or 'planning'
|
||||
this.db
|
||||
.prepare(`UPDATE tasks SET status = 'running', updated_at = ? WHERE id = ? AND status = 'pending'`)
|
||||
.prepare(`UPDATE tasks SET status = 'running', updated_at = ? WHERE id = ? AND status IN ('pending', 'planning')`)
|
||||
.run(now, taskId);
|
||||
|
||||
// Always touch updated_at even if status didn't change (e.g. from running to running with more nodes)
|
||||
@@ -291,6 +298,45 @@ export class TaskMemoryStore {
|
||||
.run(now, reason ? this.json({ reason }) : "", taskId);
|
||||
}
|
||||
|
||||
updateTaskStatus(taskId: string, status: string): void {
|
||||
const now = this.now();
|
||||
this.db
|
||||
.prepare(`UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?`)
|
||||
.run(status, now, taskId);
|
||||
}
|
||||
|
||||
updateTaskDag(taskId: string, nodes: TaskCreatePayload['nodes'], edges: TaskCreatePayload['edges']): void {
|
||||
const now = this.now();
|
||||
|
||||
// Clear existing nodes and edges if any
|
||||
this.db.prepare(`DELETE FROM task_edges WHERE task_id = ?`).run(taskId);
|
||||
this.db.prepare(`DELETE FROM task_nodes WHERE task_id = ?`).run(taskId);
|
||||
|
||||
const insertNode = this.db.prepare(
|
||||
`INSERT INTO task_nodes (task_id, id, type, service, status, input)
|
||||
VALUES (?, ?, ?, ?, 'pending', ?)`,
|
||||
);
|
||||
for (const n of nodes) {
|
||||
insertNode.run(
|
||||
taskId,
|
||||
n.id,
|
||||
n.type,
|
||||
n.service,
|
||||
this.json(n.input),
|
||||
);
|
||||
}
|
||||
|
||||
const insertEdge = this.db.prepare(
|
||||
`INSERT INTO task_edges (id, task_id, from_node, to_node)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
);
|
||||
for (const e of edges) {
|
||||
insertEdge.run(randomUUID(), taskId, e.fromNode, e.toNode);
|
||||
}
|
||||
|
||||
this.db.prepare(`UPDATE tasks SET updated_at = ? WHERE id = ?`).run(now, taskId);
|
||||
}
|
||||
|
||||
getTask(taskId: string): unknown {
|
||||
const task = this.db.prepare(`SELECT * FROM tasks WHERE id = ?`).get(taskId) as Record<string, unknown> | undefined;
|
||||
if (!task) return null;
|
||||
@@ -339,7 +385,9 @@ function isTaskMessage(type: string): boolean {
|
||||
type === "task.getByConversationId" ||
|
||||
type === "task.appendReflection" ||
|
||||
type === "task.complete" ||
|
||||
type === "task.fail"
|
||||
type === "task.fail" ||
|
||||
type === "task.updateStatus" ||
|
||||
type === "task.updateDag"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -409,6 +457,18 @@ export class TaskMemoryService extends BaseProcess {
|
||||
result = { taskId: p.taskId };
|
||||
break;
|
||||
}
|
||||
case "task.updateStatus": {
|
||||
const p = payload as unknown as TaskUpdateStatusPayload;
|
||||
this.store.updateTaskStatus(p.taskId, p.status);
|
||||
result = { taskId: p.taskId, status: p.status };
|
||||
break;
|
||||
}
|
||||
case "task.updateDag": {
|
||||
const p = payload as unknown as TaskCreatePayload;
|
||||
this.store.updateTaskDag(p.taskId, p.nodes, p.edges);
|
||||
result = { taskId: p.taskId };
|
||||
break;
|
||||
}
|
||||
default:
|
||||
this.sendError(envelope, "UNKNOWN_TYPE", `Unknown type: ${envelope.type}`);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user