diff --git a/static/src/components/chat/VirtualMonitorSurface.vue b/static/src/components/chat/VirtualMonitorSurface.vue index 0d0d1e6..f60c726 100644 --- a/static/src/components/chat/VirtualMonitorSurface.vue +++ b/static/src/components/chat/VirtualMonitorSurface.vue @@ -138,6 +138,11 @@
+
@@ -145,21 +150,11 @@ - 任务看板 + 待办事项
-
-
-

Backlog

-
-
-
-

进行中

-
-
-
-

完成

-
-
+
+
+
@@ -256,9 +251,8 @@ const memoryStatus = ref(null); const memoryCount = ref(null); const memoryTime = ref(null); const todoWindow = ref(null); -const todoBacklog = ref(null); -const todoDoing = ref(null); -const todoDone = ref(null); +const todoSummary = ref(null); +const todoList = ref(null); const desktopMenu = ref(null); const folderMenu = ref(null); const fileMenu = ref(null); @@ -338,9 +332,8 @@ const mountDirector = async () => { memoryCount: memoryCount.value!, memoryTime: memoryTime.value!, todoWindow: todoWindow.value!, - todoBacklog: todoBacklog.value!, - todoDoing: todoDoing.value!, - todoDone: todoDone.value!, + todoSummary: todoSummary.value!, + todoList: todoList.value!, desktopMenu: desktopMenu.value!, folderMenu: folderMenu.value!, fileMenu: fileMenu.value!, diff --git a/static/src/components/chat/monitor/MonitorDirector.ts b/static/src/components/chat/monitor/MonitorDirector.ts index ffe1a7f..fb045ae 100644 --- a/static/src/components/chat/monitor/MonitorDirector.ts +++ b/static/src/components/chat/monitor/MonitorDirector.ts @@ -63,9 +63,8 @@ export interface MonitorElements { memoryCount: HTMLElement; memoryTime: HTMLElement; todoWindow: HTMLElement; - todoBacklog: HTMLElement; - todoDoing: HTMLElement; - todoDone: HTMLElement; + todoSummary: HTMLElement; + todoList: HTMLElement; desktopMenu: HTMLElement; folderMenu: HTMLElement; fileMenu: HTMLElement; @@ -140,7 +139,7 @@ const DESKTOP_APPS: Array<{ id: string; label: string; assetKey: string }> = [ { id: 'command', label: '命令行', assetKey: 'command' }, { id: 'python', label: 'Python', assetKey: 'python' }, { id: 'memory', label: '记忆', assetKey: 'memory' }, - { id: 'todo', label: '看板', assetKey: 'todo' }, + { id: 'todo', label: '待办事项', assetKey: 'todo' }, { id: 'subagent', label: '子代理', assetKey: 'subagent' } ]; @@ -344,9 +343,8 @@ export class MonitorDirector implements MonitorDriver { this.terminalLastFocusedAt = 0; this.elements.readerLines.innerHTML = ''; this.elements.readerOcr.innerHTML = ''; - this.elements.todoBacklog.innerHTML = ''; - this.elements.todoDoing.innerHTML = ''; - this.elements.todoDone.innerHTML = ''; + this.elements.todoSummary.textContent = ''; + this.elements.todoList.innerHTML = ''; this.folderIcons.clear(); this.fileIcons.clear(); } @@ -2770,34 +2768,56 @@ export class MonitorDirector implements MonitorDriver { this.sceneHandlers.todoCreate = async (payload, runtime) => { this.applySceneStatus(runtime, 'todoCreate', '正在更新待办'); - await this.movePointerToApp('todo'); - await this.click({ count: 2 }); - this.showWindow(this.elements.todoWindow); - const title = payload?.arguments?.title || payload?.arguments?.task || '新的待办'; - this.pushTodoCard('backlog', title); - await sleep(400); + const summary = + payload?.arguments?.summary || + payload?.arguments?.overview || + payload?.arguments?.title || + payload?.arguments?.task || + '待办摘要'; + const tasks = this.normalizeTodoTasks(payload?.arguments?.tasks || summary); + await this.ensureTodoWindowVisible(); + await this.typeTodoSummary(summary); + for (const task of tasks) { + await this.animateTodoAppend(task); + } + await sleep(320); }; this.sceneHandlers.todoUpdate = async (payload, runtime) => { - await this.sceneHandlers.todoCreate(payload, runtime); - this.applySceneStatus(runtime, 'todoUpdate', '正在调整进度'); - this.shiftTodoCard('backlog', 'doing'); - await sleep(400); + this.applySceneStatus(runtime, 'todoUpdate', '正在调整待办'); + await this.ensureTodoWindowVisible(); + const targetText = payload?.arguments?.title || payload?.arguments?.task || null; + const completed = + payload?.arguments?.completed ?? payload?.arguments?.done ?? payload?.arguments?.checked ?? true; + await this.toggleTodoItem(targetText, !!completed); + await sleep(260); }; this.sceneHandlers.todoFinish = async (_payload, runtime) => { this.applySceneStatus(runtime, 'todoFinish', '正在完成任务'); - this.shiftTodoCard('doing', 'done'); - await sleep(400); + const redDot = this.elements.todoWindow.querySelector('.traffic-dot.red') as HTMLElement | null; + if (redDot) { + await this.movePointerToElement(redDot, { duration: 420 }); + await this.click(); + } + this.closeWindow(this.elements.todoWindow); + await sleep(320); }; this.sceneHandlers.todoFinishConfirm = this.sceneHandlers.todoFinish; this.sceneHandlers.todoDelete = async (payload, runtime) => { - this.applySceneStatus(runtime, 'todoDelete', '正在移除任务'); - const title = payload?.arguments?.title; - this.removeTodoCard(title); - await sleep(300); + this.applySceneStatus(runtime, 'todoDelete', '正在移除待办'); + const targetText = payload?.arguments?.title || payload?.arguments?.task || null; + const card = this.findTodoItemByText(targetText); + if (card) { + await this.scrollTodoItemIntoView(card); + card.classList.add('removing'); + await this.movePointerToElement(card, { duration: 420 }); + await this.click({ right: true }); + setTimeout(() => card.remove(), 260); + } + await sleep(280); }; this.sceneHandlers.terminalSession = async (payload, runtime) => { @@ -4989,51 +5009,116 @@ export class MonitorDirector implements MonitorDriver { this.updateMemoryMeta(); } - private pushTodoCard(column: 'backlog' | 'doing' | 'done', title: string) { - const map: Record<'backlog' | 'doing' | 'done', HTMLElement> = { - backlog: this.elements.todoBacklog, - doing: this.elements.todoDoing, - done: this.elements.todoDone - }; - const card = document.createElement('div'); - card.className = 'todo-card'; - card.textContent = title; - map[column].appendChild(card); + private getTodoItems(): HTMLElement[] { + if (!this.elements.todoList) return []; + return Array.from(this.elements.todoList.children) as HTMLElement[]; } - private shiftTodoCard(source: 'backlog' | 'doing', target: 'doing' | 'done') { - const map: Record<'backlog' | 'doing' | 'done', HTMLElement> = { - backlog: this.elements.todoBacklog, - doing: this.elements.todoDoing, - done: this.elements.todoDone - }; - const sourceList = map[source]; - if (!sourceList.firstElementChild) { - return; + private findTodoItemByText(text?: string | null): HTMLElement | null { + if (!text) return this.getTodoItems()[0] || null; + const target = (text || '').trim(); + return this.getTodoItems().find(item => item.querySelector('.todo-text')?.textContent?.trim() === target) || null; + } + + private normalizeTodoTasks(raw: any): Array<{ text: string; done?: boolean }> { + if (Array.isArray(raw)) { + return raw + .map(item => { + if (typeof item === 'string') return { text: item, done: false }; + if (item && typeof item === 'object') { + const text = String(item.title || item.task || item.text || '').trim(); + const done = typeof item.completed === 'boolean' ? item.completed : !!item.done; + if (!text) return null; + return { text, done }; + } + return null; + }) + .filter(Boolean) as Array<{ text: string; done?: boolean }>; } - const card = sourceList.firstElementChild as HTMLElement; - sourceList.removeChild(card); - card.classList.add('moving'); - requestAnimationFrame(() => { - card.classList.remove('moving'); - map[target].appendChild(card); - if (target === 'done') { - card.classList.add('done'); - } + if (typeof raw === 'string') { + return [{ text: raw, done: false }]; + } + return []; + } + + private createTodoItem(text: string, done = false) { + const item = document.createElement('div'); + item.className = 'todo-item fly-in'; + const body = document.createElement('div'); + body.className = 'todo-text'; + body.textContent = text; + const check = document.createElement('div'); + check.className = 'todo-check'; + if (done) { + check.classList.add('checked'); + item.classList.add('done'); + } + item.appendChild(body); + item.appendChild(check); + return item; + } + + private renderTodoItems(items: Array<{ text: string; done?: boolean }>) { + if (!this.elements.todoList) return; + this.elements.todoList.innerHTML = ''; + items.forEach(task => { + const card = this.createTodoItem(task.text, !!task.done); + card.classList.add('visible'); + this.elements.todoList.appendChild(card); }); } - private removeTodoCard(title?: string) { - if (!title) { + private async ensureTodoWindowVisible() { + if (this.isWindowVisible(this.elements.todoWindow)) { + this.showWindow(this.elements.todoWindow); return; } - const selector = `.todo-card`; - const cards = this.elements.todoWindow.querySelectorAll(selector); - cards.forEach(card => { - if (card.textContent?.trim() === title.trim()) { - card.classList.add('removing'); - setTimeout(() => card.remove(), 300); - } - }); + await this.movePointerToApp('todo'); + await this.click({ count: 2 }); + this.showWindow(this.elements.todoWindow); + } + + private async typeTodoSummary(text: string) { + if (!this.elements.todoSummary) return; + await this.movePointerToElement(this.elements.todoSummary, { duration: 420 }); + await this.click(); + this.elements.todoSummary.textContent = ''; + const chars = Array.from(text || '暂无摘要'); + for (const ch of chars) { + this.elements.todoSummary.textContent = (this.elements.todoSummary.textContent || '') + ch; + await sleep(20); + } + } + + private async animateTodoAppend(task: { text: string; done?: boolean }) { + if (!this.elements.todoList) return; + const card = this.createTodoItem(task.text, !!task.done); + this.elements.todoList.appendChild(card); + requestAnimationFrame(() => card.classList.add('visible')); + await this.scrollTodoItemIntoView(card); + await sleep(180); + } + + private async scrollTodoItemIntoView(card: HTMLElement | null) { + if (!card || !this.elements.todoList) return; + const body = this.elements.todoList; + const targetTop = card.offsetTop - body.clientHeight * 0.2; + const clamped = Math.max(0, targetTop); + body.scrollTo({ top: clamped, behavior: 'smooth' }); + await this.waitForScrollSettled(body, clamped); + } + + private async toggleTodoItem(text?: string | null, done?: boolean) { + const card = this.findTodoItemByText(text); + if (!card) return; + await this.scrollTodoItemIntoView(card); + const check = card.querySelector('.todo-check') as HTMLElement | null; + if (!check) return; + await this.movePointerToElement(check, { duration: 420 }); + await this.click(); + const targetState = typeof done === 'boolean' ? done : !check.classList.contains('checked'); + check.classList.toggle('checked', targetState); + card.classList.toggle('done', targetState); + await sleep(200); } } diff --git a/static/src/styles/components/chat/_virtual-monitor.scss b/static/src/styles/components/chat/_virtual-monitor.scss index 84ba492..8325e09 100644 --- a/static/src/styles/components/chat/_virtual-monitor.scss +++ b/static/src/styles/components/chat/_virtual-monitor.scss @@ -1135,32 +1135,114 @@ transition: transform 0.24s ease, opacity 0.22s ease; } -/* memory-tag removed to simplify UI */ +.virtual-monitor-surface .memory-footer.hidden { display: none; } -.virtual-monitor-surface .todo-columns { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 12px; -} - -.virtual-monitor-surface .todo-column { - background: rgba(255, 255, 255, 0.65); - border-radius: 12px; - padding: 10px; -} - -.virtual-monitor-surface .todo-list { +/* Todo Window */ +.virtual-monitor-surface .todo-window { + width: 520px; + height: 360px; + top: 180px; + left: 520px; display: flex; flex-direction: column; - gap: 8px; } -.virtual-monitor-surface .todo-card { - background: #fff; +.virtual-monitor-surface .todo-body { + flex: 1; + display: flex; + flex-direction: column; + gap: 12px; + padding: 12px; +} + +.virtual-monitor-surface .todo-summary { + max-height: 120px; + min-height: 64px; + padding: 10px 12px; border-radius: 10px; - padding: 8px 10px; + background: rgba(15, 23, 42, 0.04); + border: 1px solid rgba(15, 23, 42, 0.08); + overflow-y: auto; + line-height: 1.5; font-size: 13px; - box-shadow: 0 4px 12px rgba(15, 23, 42, 0.12); +} + +.virtual-monitor-surface .todo-progress { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + gap: 10px; + overflow-y: auto; + padding-right: 2px; +} + +.virtual-monitor-surface .todo-summary, +.virtual-monitor-surface .todo-progress { + scrollbar-width: none; + -ms-overflow-style: none; +} +.virtual-monitor-surface .todo-summary::-webkit-scrollbar, +.virtual-monitor-surface .todo-progress::-webkit-scrollbar { + display: none; + width: 0; + height: 0; +} + +.virtual-monitor-surface .todo-item { + display: grid; + grid-template-columns: 1fr 32px; + gap: 10px; + align-items: center; + padding: 12px 14px; + border-radius: 12px; + background: #fff; + border: 1px solid rgba(15, 23, 42, 0.08); + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.12); + opacity: 0; + transform: translateX(-28px); + transition: transform 0.32s ease, opacity 0.28s ease, box-shadow 0.22s ease; +} + +.virtual-monitor-surface .todo-item.visible { + opacity: 1; + transform: translateX(0); +} + +.virtual-monitor-surface .todo-item.done { + border-color: rgba(72, 187, 120, 0.55); + box-shadow: 0 10px 24px rgba(72, 187, 120, 0.18); +} + +.virtual-monitor-surface .todo-text { + line-height: 1.5; + word-break: break-word; + white-space: pre-wrap; + font-size: 13px; +} + +.virtual-monitor-surface .todo-check { + width: 20px; + height: 20px; + border-radius: 6px; + border: 2px solid rgba(15, 23, 42, 0.16); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + position: relative; +} + +.virtual-monitor-surface .todo-check.checked { + background: linear-gradient(135deg, #4caf50, #60c78f); + border-color: transparent; +} + +.virtual-monitor-surface .todo-check.checked::after { + content: '✔'; + color: #fff; + font-size: 12px; + line-height: 1; } .virtual-monitor-surface .todo-card.done {