const state = { sessions: [], messages: [], tasks: [], approvals: [], workers: [], events: [], selectedSessionId: null, }; const elements = { sessionList: document.querySelector("#session-list"), workerList: document.querySelector("#worker-list"), messageList: document.querySelector("#message-list"), taskList: document.querySelector("#task-list"), approvalList: document.querySelector("#approval-list"), eventList: document.querySelector("#event-list"), planHint: document.querySelector("#plan-hint"), sessionTitleDisplay: document.querySelector("#session-title-display"), sessionSummary: document.querySelector("#session-summary"), createSessionForm: document.querySelector("#create-session-form"), sessionTitleInput: document.querySelector("#session-title"), createWorkerForm: document.querySelector("#create-worker-form"), workerName: document.querySelector("#worker-name"), workerOs: document.querySelector("#worker-os"), workerCapabilities: document.querySelector("#worker-capabilities"), messageForm: document.querySelector("#message-form"), messageInput: document.querySelector("#message-input"), resetDemo: document.querySelector("#reset-demo"), archiveSession: document.querySelector("#archive-session"), }; function escapeHtml(input) { return String(input) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } async function request(url, options = {}) { const response = await fetch(url, { headers: { "Content-Type": "application/json" }, ...options, }); let payload = {}; try { payload = await response.json(); } catch { payload = {}; } if (!response.ok) { throw new Error(payload.error || `${response.status} ${response.statusText}`); } return payload; } function selectedSession() { return state.sessions.find((session) => session.id === state.selectedSessionId) ?? null; } function tasksForSelectedSession() { return state.tasks.filter((task) => task.sessionId === state.selectedSessionId); } function messagesForSelectedSession() { return state.messages.filter((message) => message.sessionId === state.selectedSessionId); } function approvalsForSelectedSession() { return state.approvals.filter((approval) => approval.sessionId === state.selectedSessionId); } function eventsForSelectedSession() { return state.events .filter((event) => event.sessionId === null || event.sessionId === state.selectedSessionId) .slice(-50) .reverse(); } function renderSessions() { if (state.sessions.length === 0) { elements.sessionList.innerHTML = `

先创建一个项目会话。

`; return; } elements.sessionList.innerHTML = state.sessions .map((session) => { const active = session.id === state.selectedSessionId ? "active" : ""; const archived = session.status === "archived" ? "archived" : ""; return ` `; }) .join(""); elements.sessionList.querySelectorAll("[data-session-id]").forEach((button) => { button.addEventListener("click", async () => { state.selectedSessionId = button.dataset.sessionId; await loadSession(state.selectedSessionId); render(); }); }); } function renderWorkers() { if (state.workers.length === 0) { elements.workerList.innerHTML = `

还没有 worker。可以手动注册,或直接运行 \`npm run demo\`。

`; return; } elements.workerList.innerHTML = state.workers .map( (worker) => `
${escapeHtml(worker.name)} ${escapeHtml(worker.status)}
${escapeHtml(worker.os)}
current: ${escapeHtml(worker.currentTaskId || "idle")}
last seen: ${new Date(worker.lastSeenAt).toLocaleTimeString()}
${worker.capabilities.map((capability) => `${escapeHtml(capability)}`).join("")}
`, ) .join(""); } function renderSessionHeader() { const session = selectedSession(); if (!session) { elements.sessionTitleDisplay.textContent = "选择一个项目会话"; elements.sessionSummary.textContent = "创建会话后,在这里持续对话并观察任务状态。"; elements.archiveSession.disabled = true; elements.messageInput.disabled = true; elements.messageInput.placeholder = "先创建或选择一个项目会话。"; return; } elements.sessionTitleDisplay.textContent = `${session.title} (${session.status})`; elements.sessionSummary.textContent = session.lastPlannerSummary || session.activeObjective || "等待用户输入。"; elements.archiveSession.disabled = session.status === "archived"; elements.messageInput.disabled = session.status === "archived"; elements.messageInput.placeholder = session.status === "archived" ? "当前会话已归档,不能继续发送消息。" : "输入需求。示例:先调研登录失败根因,不要急着改代码。"; } function renderMessages() { const messages = messagesForSelectedSession(); elements.messageList.innerHTML = messages.length ? messages .map( (message) => `
${escapeHtml(message.role)} ${new Date(message.createdAt).toLocaleTimeString()}

${escapeHtml(message.content)}

`, ) .join("") : `

当前没有消息。

`; } function renderTasks() { const latestPlan = [...state.events] .reverse() .find((event) => event.sessionId === state.selectedSessionId && event.type === "plan.created"); if (latestPlan) { const pausedCount = Array.isArray(latestPlan.payload.pausedTaskIds) ? latestPlan.payload.pausedTaskIds.length : 0; elements.planHint.textContent = `最新计划创建了 ${latestPlan.payload.taskIds.length} 个任务,自动暂停旧任务 ${pausedCount} 个。`; } else { elements.planHint.textContent = ""; } const tasks = tasksForSelectedSession(); elements.taskList.innerHTML = tasks.length ? tasks .map( (task) => `
${escapeHtml(task.title)} ${escapeHtml(task.status)}

${escapeHtml(task.description)}

worker: ${escapeHtml(task.assignedWorkerId || "未分配")}
progress: ${task.progressPercent}%
summary: ${escapeHtml(task.summary || "暂无")}
next: ${escapeHtml(task.nextStep || "暂无")}
${ ["queued", "assigned", "running"].includes(task.status) ? `` : "" } ${ ["paused", "blocked"].includes(task.status) ? `` : "" } ${ !["completed", "cancelled", "failed"].includes(task.status) ? `` : "" }
`, ) .join("") : `

当前没有任务。

`; elements.taskList.querySelectorAll("[data-action]").forEach((button) => { button.addEventListener("click", async () => { const taskId = button.dataset.taskId; const action = button.dataset.action; await request(`/api/tasks/${taskId}/${action}`, { method: "POST", body: "{}" }); await loadSession(state.selectedSessionId); await loadBootstrap(); render(); }); }); } function renderApprovals() { const approvals = approvalsForSelectedSession(); elements.approvalList.innerHTML = approvals.length ? approvals .map( (approval) => `
${escapeHtml(approval.summary)} ${escapeHtml(approval.status)}
risk: ${escapeHtml(approval.riskLevel)}
`, ) .join("") : `

当前没有待审批项。

`; elements.approvalList.querySelectorAll("[data-approval-id]").forEach((button) => { button.addEventListener("click", async () => { const approvalId = button.dataset.approvalId; const approved = button.dataset.approved === "true"; await request(`/api/approvals/${approvalId}/respond`, { method: "POST", body: JSON.stringify({ approved, responder: "web-user" }), }); await loadSession(state.selectedSessionId); await loadBootstrap(); render(); }); }); } function renderEvents() { const events = eventsForSelectedSession(); elements.eventList.innerHTML = events.length ? events .map( (event) => `
${escapeHtml(event.type)} ${new Date(event.timestamp).toLocaleTimeString()}
${escapeHtml(JSON.stringify(event.payload, null, 2))}
`, ) .join("") : `

当前没有事件。

`; } function render() { renderSessions(); renderWorkers(); renderSessionHeader(); renderMessages(); renderTasks(); renderApprovals(); renderEvents(); } async function loadBootstrap() { const bootstrap = await request("/api/bootstrap"); state.sessions = bootstrap.sessions; state.messages = bootstrap.messages; state.tasks = bootstrap.tasks; state.workers = bootstrap.workers; state.approvals = bootstrap.approvals; state.events = bootstrap.events; if (!state.selectedSessionId && state.sessions[0]) { state.selectedSessionId = state.sessions[0].id; } } async function loadSession(sessionId) { if (!sessionId) return; const details = await request(`/api/sessions/${sessionId}`); state.sessions = state.sessions.map((session) => (session.id === sessionId ? details.session : session)); state.messages = [ ...state.messages.filter((message) => message.sessionId !== sessionId), ...details.messages, ]; state.tasks = [ ...state.tasks.filter((task) => task.sessionId !== sessionId), ...details.tasks, ]; state.approvals = [ ...state.approvals.filter((approval) => approval.sessionId !== sessionId), ...details.approvals, ]; } elements.createSessionForm.addEventListener("submit", async (event) => { event.preventDefault(); const title = elements.sessionTitleInput.value.trim(); const details = await request("/api/sessions", { method: "POST", body: JSON.stringify({ title }), }); state.sessions.unshift(details.session); state.selectedSessionId = details.session.id; await loadSession(details.session.id); elements.sessionTitleInput.value = ""; render(); }); elements.createWorkerForm.addEventListener("submit", async (event) => { event.preventDefault(); const name = elements.workerName.value.trim(); if (!name) return; const os = elements.workerOs.value; const capabilities = elements.workerCapabilities.value .split(",") .map((item) => item.trim()) .filter(Boolean); await request("/api/workers/register", { method: "POST", body: JSON.stringify({ name, os, capabilities }), }); elements.workerName.value = ""; elements.workerCapabilities.value = "terminal"; await loadBootstrap(); render(); }); elements.messageForm.addEventListener("submit", async (event) => { event.preventDefault(); if (!state.selectedSessionId) return; const content = elements.messageInput.value.trim(); if (!content) return; await request(`/api/sessions/${state.selectedSessionId}/messages`, { method: "POST", body: JSON.stringify({ content, channel: "web" }), }); elements.messageInput.value = ""; await loadSession(state.selectedSessionId); await loadBootstrap(); render(); }); elements.archiveSession.addEventListener("click", async () => { if (!state.selectedSessionId) return; await request(`/api/sessions/${state.selectedSessionId}/archive`, { method: "POST", body: "{}", }); await loadSession(state.selectedSessionId); await loadBootstrap(); render(); }); elements.resetDemo.addEventListener("click", async () => { await request("/api/demo/reset", { method: "POST", body: "{}" }); state.sessions = []; state.messages = []; state.tasks = []; state.workers = []; state.approvals = []; state.events = []; state.selectedSessionId = null; render(); }); const stream = new EventSource("/api/events/stream"); stream.onmessage = async (event) => { const payload = JSON.parse(event.data); state.events.push(payload); if (payload.sessionId) { await loadSession(payload.sessionId); } await loadBootstrap(); render(); }; loadBootstrap().then(render).catch((error) => { console.error(error); elements.sessionSummary.textContent = error.message; });