From 7e6eb6a7b332cb0e9c47851b3f1eb1f4ab175248 Mon Sep 17 00:00:00 2001 From: "WIN-JHFT4D3SIVT\\caoxiaozhu" Date: Sat, 11 Apr 2026 08:48:37 +0800 Subject: [PATCH] feat(frontend): update chat page composables and sidebar plan implementation Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- frontend/src/pages/chat/chatPage.css | 29 +- .../chat/composables/useChatView.test.ts | 105 ++++- .../src/pages/chat/composables/useChatView.ts | 65 +++- .../pages/chat/composables/useSidebarPlan.ts | 359 +++++++++++++----- frontend/src/pages/chat/index.vue | 166 ++++++-- 5 files changed, 586 insertions(+), 138 deletions(-) diff --git a/frontend/src/pages/chat/chatPage.css b/frontend/src/pages/chat/chatPage.css index 60c8d7f..f261aa0 100644 --- a/frontend/src/pages/chat/chatPage.css +++ b/frontend/src/pages/chat/chatPage.css @@ -27,7 +27,7 @@ display: flex; flex-direction: column; gap: 10px; - background: linear-gradient(180deg, rgba(7, 12, 24, 0.92), rgba(7, 11, 20, 0.82)); + background: linear-gradient(180deg, rgba(7, 12, 24, 0.65), rgba(7, 11, 20, 0.55)); overflow-y: auto; min-height: 0; } @@ -36,7 +36,7 @@ flex-shrink: 0; padding: 12px 14px 14px; border-top: 1px solid rgba(34, 211, 238, 0.08); - background: linear-gradient(180deg, rgba(6, 10, 18, 0.96), rgba(5, 8, 16, 0.98)); + background: linear-gradient(180deg, rgba(15, 25, 45, 0.4), rgba(10, 20, 40, 0.45)); } .runtime-grid { @@ -618,8 +618,9 @@ .conv-sidebar { width: 320px; min-width: 320px; - background: var(--bg-panel); - border-right: 1px solid rgba(0, 245, 212, 0.15); + background: rgba(15, 25, 45, 0.2); + backdrop-filter: blur(16px); + border-right: 1px solid rgba(0, 245, 212, 0.12); display: flex; flex-direction: column; overflow: visible; @@ -636,7 +637,8 @@ .runtime-sidebar { width: 280px; min-width: 280px; - background: var(--bg-panel); + background: rgba(15, 25, 45, 0.2); + backdrop-filter: blur(16px); border-left: 1px solid var(--border-dim); overflow: visible; display: flex; @@ -1701,7 +1703,8 @@ /* ── JARVIS SIDEBAR STYLES ── */ .jarvis-sidebar { - background: rgba(4, 10, 20, 0.95) !important; + background: rgba(4, 10, 20, 0.6) !important; + backdrop-filter: blur(12px); padding: 10px !important; gap: 12px; } @@ -2029,10 +2032,11 @@ display: flex; flex-direction: column; min-height: 0; - background: var(--bg-panel) !important; + background: rgba(10, 15, 26, 0.6) !important; + backdrop-filter: blur(12px); padding: 0 !important; border-right: 1px solid rgba(0, 245, 212, 0.15); - box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.3); + box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.1); overflow: visible; } @@ -2131,7 +2135,7 @@ } .jarvis-sidebar .jarvis-panel { - background: linear-gradient(180deg, rgba(8, 18, 36, 0.78), rgba(8, 14, 26, 0.62)) !important; + background: linear-gradient(180deg, rgba(8, 18, 36, 0.5), rgba(8, 14, 26, 0.4)) !important; border: 1px solid rgba(56, 189, 248, 0.14) !important; border-radius: 12px; padding: 12px 10px 11px; @@ -2139,7 +2143,7 @@ position: relative; overflow: visible !important; flex-shrink: 0; - box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.2); + box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.1); clip-path: none !important; } @@ -2437,7 +2441,7 @@ .jarvis-progress-core strong { font-family: var(--font-display); - font-size: 20px; + font-size: 16px; color: #ecfeff; } @@ -2641,7 +2645,8 @@ gap: 12px; padding: 10px 24px 11px; border-bottom: 1px solid rgba(34, 211, 238, 0.08); - background: linear-gradient(180deg, rgba(5, 10, 19, 0.92), rgba(4, 9, 17, 0.8)); + background: linear-gradient(180deg, rgba(5, 10, 19, 0.6), rgba(4, 9, 17, 0.5)); + backdrop-filter: blur(8px); overflow: hidden; } diff --git a/frontend/src/pages/chat/composables/useChatView.test.ts b/frontend/src/pages/chat/composables/useChatView.test.ts index 5512cf9..8a165fe 100644 --- a/frontend/src/pages/chat/composables/useChatView.test.ts +++ b/frontend/src/pages/chat/composables/useChatView.test.ts @@ -7,6 +7,7 @@ const mocks = vi.hoisted(() => ({ list: vi.fn(), getMessages: vi.fn(), deleteConversation: vi.fn(), + taskCreate: vi.fn(), settingsGet: vi.fn(), upload: vi.fn(), systemStatusGet: vi.fn(), @@ -27,6 +28,12 @@ vi.mock('@/api/settings', () => ({ }, })) +vi.mock('@/api/task', () => ({ + taskApi: { + create: mocks.taskCreate, + }, +})) + vi.mock('@/api/system', () => ({ systemApi: { getStatus: mocks.systemStatusGet, @@ -57,6 +64,14 @@ describe('useChatView orchestration state', () => { mocks.list.mockResolvedValue({ data: [] }) mocks.getMessages.mockResolvedValue({ data: [] }) mocks.deleteConversation.mockResolvedValue(undefined) + mocks.taskCreate.mockResolvedValue({ + data: { + id: 'task-1', + title: '创建测试任务', + status: 'todo', + dispatch_status: 'idle', + }, + }) mocks.settingsGet.mockResolvedValue({ data: { llm_config: { @@ -86,7 +101,7 @@ describe('useChatView orchestration state', () => { }) it('derives telemetry state from real system status and session events without persisting it to message history', async () => { - mocks.chatStream.mockImplementation(async (_message, _conversationId, _fileIds, _modelName, handlers) => { + mocks.chatStream.mockImplementation(async (_message, _conversationId, _fileIds, _modelName, _runtime, handlers) => { handlers.onMetadata?.({ conversation_id: 'conv-1', message_id: 'msg-1' }) handlers.onProgress?.({ stage: 'tool', @@ -126,7 +141,7 @@ describe('useChatView orchestration state', () => { }) it('keeps the orchestration panel persistent and confines thinking state to the side panel', async () => { - mocks.chatStream.mockImplementation(async (_message, _conversationId, _fileIds, _modelName, handlers) => { + mocks.chatStream.mockImplementation(async (_message, _conversationId, _fileIds, _modelName, _runtime, handlers) => { handlers.onMetadata?.({ conversation_id: 'conv-1', message_id: 'msg-1' }) handlers.onProgress?.({ stage: 'planning', @@ -167,7 +182,7 @@ describe('useChatView orchestration state', () => { }) it('surfaces schedule fulfillment progress when chat creates a reminder', async () => { - mocks.chatStream.mockImplementation(async (_message, _conversationId, _fileIds, _modelName, handlers) => { + mocks.chatStream.mockImplementation(async (_message, _conversationId, _fileIds, _modelName, _runtime, handlers) => { handlers.onMetadata?.({ conversation_id: 'conv-2', message_id: 'msg-2' }) handlers.onProgress?.({ stage: 'tool', @@ -194,4 +209,88 @@ describe('useChatView orchestration state', () => { expect(view.store.messages.at(-1)?.content).toBe('已经帮你建好提醒。') }) + + it('sends jarvis as the default runtime', async () => { + mocks.chatStream.mockImplementation(async (_message, _conversationId, _fileIds, _modelName, _runtime, handlers) => { + handlers.onMetadata?.({ conversation_id: 'conv-1', message_id: 'msg-1' }) + handlers.onChunk?.({ content: '最终回复' }) + }) + + const view = useChatView() + view.inputMessage.value = '测试默认 runtime' + + await view.sendMessage() + + expect(mocks.chatStream).toHaveBeenCalledWith( + '测试默认 runtime', + undefined, + [], + undefined, + 'jarvis', + expect.any(Object), + ) + }) + + it('sends selected hermes runtime', async () => { + mocks.chatStream.mockImplementation(async (_message, _conversationId, _fileIds, _modelName, _runtime, handlers) => { + handlers.onMetadata?.({ conversation_id: 'conv-1', message_id: 'msg-1' }) + handlers.onChunk?.({ content: 'Hermes 回复' }) + }) + + const view = useChatView() + view.selectedRuntime.value = 'hermes' + view.inputMessage.value = '测试 hermes runtime' + + await view.sendMessage() + + expect(mocks.chatStream).toHaveBeenCalledWith( + '测试 hermes runtime', + undefined, + [], + undefined, + 'hermes', + expect.any(Object), + ) + expect(view.store.messages.at(-1)?.model).toBe('hermes') + }) + + it('creates a task directly from /task slash command', async () => { + const view = useChatView() + view.inputMessage.value = '/task 创建测试任务' + + await view.sendMessage() + + expect(mocks.taskCreate).toHaveBeenCalledWith({ + title: '创建测试任务', + source: 'chat', + conversation_id: undefined, + assignee_type: undefined, + dispatch_to_commander: false, + }) + expect(view.store.messages.at(-1)?.content).toContain('已创建任务:创建测试任务') + }) + + it('creates and dispatches a task directly from /task@commander slash command', async () => { + mocks.taskCreate.mockResolvedValueOnce({ + data: { + id: 'task-2', + title: '派发测试任务', + status: 'todo', + dispatch_status: 'queued', + }, + }) + const view = useChatView() + view.inputMessage.value = '/task@commander 派发测试任务' + + await view.sendMessage() + + expect(mocks.taskCreate).toHaveBeenCalledWith({ + title: '派发测试任务', + source: 'chat', + conversation_id: undefined, + assignee_type: 'commander', + dispatch_to_commander: true, + }) + expect(view.store.messages.at(-1)?.content).toContain('已创建并派发任务:派发测试任务') + }) }) diff --git a/frontend/src/pages/chat/composables/useChatView.ts b/frontend/src/pages/chat/composables/useChatView.ts index e7fec9b..b8a5559 100644 --- a/frontend/src/pages/chat/composables/useChatView.ts +++ b/frontend/src/pages/chat/composables/useChatView.ts @@ -4,11 +4,13 @@ import { useAuthStore } from '@/stores/auth' import { conversationApi, type ChatProgressEvent, + type ChatRuntime, type Message, } from '@/api/conversation' import { documentApi } from '@/api/document' import { settingsApi, type LLMModelConfig } from '@/api/settings' import { systemApi } from '@/api/system' +import { taskApi } from '@/api/task' export interface SelectedFile { id: string @@ -94,6 +96,17 @@ const ORCHESTRATION_EVENT_STORAGE_KEY = 'jarvis.chat.orchestration.events' const ORCHESTRATION_EVENT_GROUP_LIMIT = 40 const ORCHESTRATION_EVENT_ITEM_LIMIT = 24 +function isTaskSlashCommand(value: string) { + return value.startsWith('/task ') || value === '/task' || value.startsWith('/task@commander ') || value === '/task@commander' +} + +function parseTaskSlashCommand(value: string) { + const isCommander = value.startsWith('/task@commander') + const prefix = isCommander ? '/task@commander' : '/task' + const title = value.slice(prefix.length).trim() + return { isCommander, title } +} + export function useChatView() { const store = useConversationStore() const auth = useAuthStore() @@ -151,6 +164,7 @@ export function useChatView() { agentCount: 0, }) const selectedModel = computed(() => chatModels.value.find((model) => model.name === selectedModelName.value) ?? null) + const selectedRuntime = ref('jarvis') let systemTelemetryTimer: ReturnType | null = null let sessionTelemetryTimer: ReturnType | null = null @@ -467,6 +481,51 @@ export function useChatView() { await nextTick() scrollToBottom() + if (isTaskSlashCommand(text)) { + try { + const { isCommander, title } = parseTaskSlashCommand(text) + if (!title) { + throw new Error(isCommander ? '用法:/task@commander 任务标题' : '用法:/task 任务标题') + } + const response = await taskApi.create({ + title, + source: 'chat', + conversation_id: store.currentConversationId ?? undefined, + assignee_type: isCommander ? 'commander' : undefined, + dispatch_to_commander: isCommander, + }) + const task = response.data + const reply = isCommander + ? `已创建并派发任务:${task.title}\n状态:${task.dispatch_status.toUpperCase()}` + : `已创建任务:${task.title}\n状态:${task.status.toUpperCase()}` + if (typeof window !== 'undefined') { + window.dispatchEvent(new CustomEvent('jarvis:today-status-refresh')) + } + store.addMessage({ + id: `assistant-${Date.now()}`, + role: 'assistant', + content: reply, + created_at: new Date().toISOString(), + }) + finalizeOrchestration('complete', isCommander ? '任务已创建并派发' : '任务已创建') + } catch (error: any) { + const content = error?.message || '任务创建失败' + finalizeOrchestration('error', content) + store.addMessage({ + id: `err-${Date.now()}`, + role: 'assistant', + content, + created_at: new Date().toISOString(), + }) + } + isTyping.value = false + isSending.value = false + selectedFiles.value = [] + await nextTick() + scrollToBottom() + return + } + let finalConversationId = previousConversationId let finalMessageId = '' let finalContent = '' @@ -478,6 +537,7 @@ export function useChatView() { previousConversationId ?? undefined, attachments.map((file) => file.id), selectedModelName.value || undefined, + selectedRuntime.value, { onMetadata(payload) { finalConversationId = payload.conversation_id @@ -517,7 +577,9 @@ export function useChatView() { id: finalMessageId || `assistant-${Date.now()}`, role: 'assistant', content: finalContent || '抱歉,我暂时没有生成可用回复。', - model: selectedModelName.value || selectedModel.value?.name || 'jarvis', + model: selectedRuntime.value === 'hermes' + ? 'hermes' + : (selectedModelName.value || selectedModel.value?.name || 'jarvis'), created_at: new Date().toISOString(), }) if (finalConversationId && previousConversationId === null) { @@ -714,6 +776,7 @@ export function useChatView() { chatModels, selectedModelName, selectedModel, + selectedRuntime, isLoadingModels, conversationsError, thinkingState, diff --git a/frontend/src/pages/chat/composables/useSidebarPlan.ts b/frontend/src/pages/chat/composables/useSidebarPlan.ts index 0610072..d034a6e 100644 --- a/frontend/src/pages/chat/composables/useSidebarPlan.ts +++ b/frontend/src/pages/chat/composables/useSidebarPlan.ts @@ -1,23 +1,90 @@ -import { computed, onMounted, ref, watch, type Ref } from 'vue' +import { computed, onMounted, onUnmounted, ref, watch, type Ref } from 'vue' import { CornerDownLeft, Database, Sparkles, Sun, ListTodo } from 'lucide-vue-next' -import { scheduleCenterApi, type ScheduleCenterDateResponse, type ScheduleCenterDaySummary } from '@/api/scheduleCenter' +import { + scheduleCenterApi, + type ScheduleCenterCommanderSummary, + type ScheduleCenterDateResponse, + type ScheduleCenterDaySummary, +} from '@/api/scheduleCenter' +import { taskApi, type Task, type TaskDispatchStatus, type TaskQuadrant } from '@/api/task' import type { Conversation } from '@/api/conversation' export interface SidebarFocusItem { - id: string; label: string; title: string; meta: string; tone: 'done' | 'doing' | 'pending' + id: string + label: string + title: string + meta: string + tone: 'done' | 'doing' | 'pending' } + export interface SidebarNewsItem { - id: string; title: string; meta: string + id: string + title: string + meta: string +} + +export interface TodayStatusQuadrantViewTask { + id: string + title: string + completed: boolean + status: string + dispatchStatus: string + assigneeLabel: string +} + +export interface TodayStatusQuadrantView { + id: string + title: string + subtitle: string + color: string + glowColor: string + icon: string + tasks: TodayStatusQuadrantViewTask[] } export const sidebarCollapsedModules = [ - { id: 'calendar', label: '日历', icon: Sun }, - { id: 'status', label: '计划', icon: Database }, - { id: 'focus', label: '重点', icon: Sparkles }, - { id: 'kanban', label: '待办', icon: ListTodo }, - { id: 'review', label: '复盘', icon: CornerDownLeft }, + { id: 'calendar', label: 'Calendar', icon: Sun }, + { id: 'status', label: 'Status', icon: Database }, + { id: 'focus', label: 'Focus', icon: Sparkles }, + { id: 'kanban', label: 'Issues', icon: ListTodo }, + { id: 'review', label: 'Review', icon: CornerDownLeft }, ] +const ISSUE_QUADRANT_META: Record> = { + 'urgent-important': { + id: 'urgent-important', + title: 'Important & Urgent', + subtitle: 'CRITICAL', + color: '#ff4757', + glowColor: 'rgba(255, 71, 87, 0.4)', + icon: '!', + }, + 'not-urgent-important': { + id: 'not-urgent-important', + title: 'Important & Planned', + subtitle: 'PLANNED', + color: '#ffd93d', + glowColor: 'rgba(255, 217, 61, 0.4)', + icon: 'P', + }, + 'urgent-not-important': { + id: 'urgent-not-important', + title: 'Urgent & Delegated', + subtitle: 'DELEGATE', + color: '#00d4ff', + glowColor: 'rgba(0, 212, 255, 0.4)', + icon: 'D', + }, + 'not-urgent-not-important': { + id: 'not-urgent-not-important', + title: 'Backlog / Low Priority', + subtitle: 'BACKLOG', + color: '#6bcf7f', + glowColor: 'rgba(107, 207, 127, 0.4)', + icon: 'B', + }, +} + export function formatDateKey(date: Date) { const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') @@ -31,6 +98,80 @@ function formatMonthKey(date: Date) { return `${year}-${month}` } +function toneFromStatus(status: string) { + if (status === 'done' || status === 'completed') return 'done' as const + if (status === 'in_progress' || status === 'running' || status === 'queued') return 'doing' as const + return 'pending' as const +} + +function focusLabelByQuadrant(quadrant?: string | null) { + if (!quadrant) return 'Task' + if (quadrant === 'urgent-important') return 'Critical' + if (quadrant === 'not-urgent-important') return 'Planned' + if (quadrant === 'urgent-not-important') return 'Delegated' + return 'Backlog' +} + +function focusMeta(status: string, dispatchStatus: string, assigneeType?: string | null, assigneeId?: string | null) { + const statusMap: Record = { + todo: 'Pending', + in_progress: 'In progress', + done: 'Done', + cancelled: 'Cancelled', + } + const dispatchMap: Record = { + idle: 'Not dispatched', + queued: 'Queued', + running: 'Running', + completed: 'Completed', + failed: 'Failed', + } + const statusLabel = dispatchStatus !== 'idle' ? dispatchMap[dispatchStatus] : (statusMap[status] || 'Pending') + if (assigneeType && assigneeId) return `${statusLabel} / ${assigneeType}:${assigneeId}` + if (assigneeType) return `${statusLabel} / ${assigneeType}` + return statusLabel +} + +function deriveIssueQuadrant(task: Task): TaskQuadrant { + if (task.quadrant) return task.quadrant + if (task.priority === 'high' || task.priority === 'urgent') return 'urgent-important' + if (task.status === 'in_progress') return 'not-urgent-important' + if (task.priority === 'medium') return 'urgent-not-important' + return 'not-urgent-not-important' +} + +function buildIssueCommanderSummary(tasks: Task[]): ScheduleCenterCommanderSummary { + const summary: ScheduleCenterCommanderSummary = { + total: 0, + queued: 0, + running: 0, + completed: 0, + failed: 0, + overall_status: 'idle', + } + + tasks.forEach((task) => { + const state = task.dispatch_status as TaskDispatchStatus + if (state === 'idle') return + + summary.total += 1 + if (state === 'queued') summary.queued += 1 + if (state === 'running') summary.running += 1 + if (state === 'completed') summary.completed += 1 + if (state === 'failed') summary.failed += 1 + }) + + if (summary.running > 0) { + summary.overall_status = 'running' + } else if (summary.queued > 0) { + summary.overall_status = 'queued' + } else if (summary.failed > 0 && summary.completed === 0) { + summary.overall_status = 'failed' + } + + return summary +} + export function useSidebarPlan( clientTimeRef: { value: Date }, loadDailyDigestFn: () => void, @@ -38,9 +179,9 @@ export function useSidebarPlan( ) { const todayPlanDetail = ref(null) const monthPlanDays = ref([]) + const issueTasks = ref([]) const selectedDate = ref(null) - // Build a map of date -> has conversation const conversationDateMap = computed(() => { const map = new Map() const conversations = Array.isArray(conversationsRef) ? conversationsRef : (conversationsRef.value ?? []) @@ -53,6 +194,24 @@ export function useSidebarPlan( const todayDateKey = computed(() => formatDateKey(clientTimeRef.value)) const monthPlanSummaryMap = computed(() => new Map(monthPlanDays.value.map((item) => [item.date, item]))) + const issueStatusQuadrants = computed(() => ( + Object.values(ISSUE_QUADRANT_META).map((meta) => ({ + ...meta, + tasks: issueTasks.value + .filter((task) => deriveIssueQuadrant(task) === meta.id) + .map((task) => ({ + id: task.id, + title: task.title, + completed: task.status === 'done', + status: task.status, + dispatchStatus: task.dispatch_status, + assigneeLabel: task.assignee_id ? `${task.assignee_type ?? 'owner'}:${task.assignee_id}` : (task.assignee_type ?? 'unassigned'), + })), + })) + )) + + const issueCommanderSummary = computed(() => buildIssueCommanderSummary(issueTasks.value)) + const calendarCells = computed(() => { const year = clientTimeRef.value.getFullYear() const month = clientTimeRef.value.getMonth() @@ -60,9 +219,11 @@ export function useSidebarPlan( const firstDayOffset = (new Date(year, month, 1).getDay() + 6) % 7 const todayKey = todayDateKey.value const cells: Array<{ key: string; value: number | null; active: boolean; busy: boolean; selected: boolean; hasConversation: boolean }> = [] + for (let index = 0; index < firstDayOffset; index += 1) { cells.push({ key: `blank-start-${index}`, value: null, active: false, busy: false, selected: false, hasConversation: false }) } + for (let day = 1; day <= daysInMonth; day += 1) { const monthDate = new Date(year, month, day) const dateKey = formatDateKey(monthDate) @@ -71,30 +232,21 @@ export function useSidebarPlan( const hasConv = conversationDateMap.value.get(dateKey) || false cells.push({ key: dateKey, value: day, active: dateKey === todayKey, busy, selected: dateKey === selectedDate.value, hasConversation: hasConv }) } + while (cells.length % 7 !== 0) { cells.push({ key: `blank-end-${cells.length}`, value: null, active: false, busy: false, selected: false, hasConversation: false }) } + return cells }) const calendarYear = computed(() => clientTimeRef.value.getFullYear()) const calendarMonth = computed(() => clientTimeRef.value.getMonth() + 1) - const todayPlanCounters = computed(() => { - const detail = todayPlanDetail.value - if (!detail) return { done: 0, doing: 0, pending: 0, total: 0, completion: 0 } - const todoDone = detail.todos.filter((item) => item.is_completed).length - const todoPending = detail.todos.filter((item) => !item.is_completed).length - const taskDone = detail.tasks.filter((item) => item.status === 'done').length - const taskDoing = detail.tasks.filter((item) => item.status === 'in_progress').length - const taskPending = detail.tasks.filter((item) => item.status === 'todo').length - const goalDone = detail.goals.filter((item) => item.status === 'done').length - const goalPending = detail.goals.filter((item) => item.status !== 'done').length - const reminderDone = detail.reminders.filter((item) => item.status === 'done' || item.is_dismissed).length - const reminderPending = detail.reminders.filter((item) => item.status !== 'done' && !item.is_dismissed).length - const done = todoDone + taskDone + goalDone + reminderDone - const doing = taskDoing - const pending = todoPending + taskPending + goalPending + reminderPending + const issueStatusCounters = computed(() => { + const done = issueTasks.value.filter((item) => item.status === 'done').length + const doing = issueTasks.value.filter((item) => item.status === 'in_progress').length + const pending = issueTasks.value.filter((item) => item.status === 'todo').length const total = done + doing + pending return { done, doing, pending, total, completion: total > 0 ? Math.round((done / total) * 100) : 0 } }) @@ -114,89 +266,80 @@ export function useSidebarPlan( )) const sidebarWeekLabels = [ - { label: '一', isWeekend: false }, - { label: '二', isWeekend: false }, - { label: '三', isWeekend: false }, - { label: '四', isWeekend: false }, - { label: '五', isWeekend: false }, - { label: '六', isWeekend: true }, - { label: '日', isWeekend: true }, + { label: 'M', isWeekend: false }, + { label: 'T', isWeekend: false }, + { label: 'W', isWeekend: false }, + { label: 'T', isWeekend: false }, + { label: 'F', isWeekend: false }, + { label: 'S', isWeekend: true }, + { label: 'S', isWeekend: true }, ] - const sidebarStatusHeadline = computed(() => ( - todayPlanCounters.value.total - ? `今日共 ${todayPlanCounters.value.total} 项计划,已完成 ${todayPlanCounters.value.done} 项` - : '' - )) + const sidebarStatusHeadline = computed(() => '') const sidebarStatusBreakdown = computed(() => [ - { key: 'done', label: 'Completed', value: todayPlanCounters.value.done, tone: 'done' }, - { key: 'doing', label: 'In Progress', value: todayPlanCounters.value.doing, tone: 'doing' }, - { key: 'pending', label: 'Pending', value: todayPlanCounters.value.pending, tone: 'pending' }, - { key: 'total', label: 'Total', value: todayPlanCounters.value.total, tone: 'total' }, + { key: 'done', label: 'Completed', value: issueStatusCounters.value.done, tone: 'done' }, + { key: 'doing', label: 'In Progress', value: issueStatusCounters.value.doing, tone: 'doing' }, + { key: 'pending', label: 'Pending', value: issueStatusCounters.value.pending, tone: 'pending' }, + { key: 'total', label: 'Total', value: issueStatusCounters.value.total, tone: 'total' }, ]) - // 模拟数据 - 用于测试滑动条 - const mockFocusItems: SidebarFocusItem[] = [ - { id: 'mock-1', label: '任务', title: '完成用户登录模块开发', meta: '处理中', tone: 'doing' }, - { id: 'mock-2', label: '任务', title: '修复首页加载慢的问题', meta: '待启动', tone: 'pending' }, - { id: 'mock-3', label: '目标', title: '本周完成核心功能上线', meta: '今日目标推进', tone: 'doing' }, - { id: 'mock-4', label: '待办', title: '整理本周工作报告', meta: '手动记录', tone: 'done' }, - { id: 'mock-5', label: '任务', title: '优化数据库查询性能', meta: '待启动', tone: 'pending' }, - { id: 'mock-6', label: '提醒', title: '下午3点团队会议', meta: '15:00', tone: 'pending' }, - { id: 'mock-7', label: '任务', title: 'Code Review 代码审查', meta: '处理中', tone: 'doing' }, - { id: 'mock-8', label: '待办', title: '更新项目文档', meta: '系统同步', tone: 'done' }, - { id: 'mock-9', label: '任务', title: '部署测试环境', meta: '待启动', tone: 'pending' }, - { id: 'mock-10', label: '目标', title: '本月用户增长10%', meta: '今日目标推进', tone: 'pending' }, - { id: 'mock-11', label: '待办', title: '提交本周周报', meta: '手动记录', tone: 'done' }, - { id: 'mock-12', label: '任务', title: '接口联调测试', meta: '待启动', tone: 'pending' }, - { id: 'mock-13', label: '提醒', title: '周三产品评审会', meta: '14:00', tone: 'pending' }, - { id: 'mock-14', label: '任务', title: '性能优化与监控', meta: '处理中', tone: 'doing' }, - { id: 'mock-15', label: '待办', title: '备份重要数据', meta: '系统同步', tone: 'done' }, - { id: 'mock-16', label: '任务', title: '编写单元测试用例', meta: '待启动', tone: 'pending' }, - { id: 'mock-17', label: '目标', title: '提升系统安全性', meta: '今日目标推进', tone: 'pending' }, - { id: 'mock-18', label: '待办', title: '清理无用代码文件', meta: '手动记录', tone: 'done' }, - { id: 'mock-19', label: '任务', title: '配置CI/CD自动化部署', meta: '待启动', tone: 'pending' }, - { id: 'mock-20', label: '提醒', title: '周五项目复盘会', meta: '10:00', tone: 'pending' }, - ] - - const sidebarFocusItems = computed(() => { - // 暂时强制返回模拟数据用于测试 - return mockFocusItems - }) + const sidebarFocusItems = computed(() => ( + (todayPlanDetail.value?.focus_tasks ?? []).map((task) => ({ + id: task.id, + label: focusLabelByQuadrant(task.quadrant), + title: task.title, + meta: focusMeta(task.status, task.dispatch_status, task.assignee_type, task.assignee_id), + tone: toneFromStatus(task.dispatch_status !== 'idle' ? task.dispatch_status : task.status), + })) + )) const sidebarReviewAchievements = computed(() => { const stats = monthReviewStats.value const items = [ - stats.todoCompleted > 0 ? `累计完成 ${stats.todoCompleted} 项待办,执行节啬已形成闭环。` : '', - stats.activeDays > 0 ? `本月已有 ${stats.activeDays} 天产生有效计划记录,日程连贯性稳定。` : '', - stats.highPriorityTotal > 0 ? `高优事项共 ${stats.highPriorityTotal} 项进行中,重点任务没有脱离视野。` : '', + stats.todoCompleted > 0 ? `Completed ${stats.todoCompleted} todos this month.` : '', + stats.activeDays > 0 ? `${stats.activeDays} active planning days recorded this month.` : '', + stats.highPriorityTotal > 0 ? `${stats.highPriorityTotal} high-priority items were tracked.` : '', ].filter(Boolean) + if (items.length > 0) return items.slice(0, 3) - return ['本月计划数据还在积累中,可以从今日重点开始逐步建立复盘样本。'] + return ['Monthly review data is still accumulating.'] }) const sidebarReviewReflections = computed(() => { const stats = monthReviewStats.value const pendingTodoCount = Math.max(stats.todoTotal - stats.todoCompleted, 0) const items = [ - pendingTodoCount > 0 ? `仍有 ${pendingTodoCount} 项待办未完成,建议拆成更短的收尾窗口。` : '', - stats.highPriorityTotal >= 8 ? '高优事项密度偏高,最好提前锁定 1 到 2 个绝对优先级别。' : '', - stats.reminderTotal >= Math.max(6, stats.activeDays) ? '提醒数量较多,说明执行中断点偏多,适合增加固定回固时段。' : '', + pendingTodoCount > 0 ? `${pendingTodoCount} todos are still open and may need to be broken down further.` : '', + stats.highPriorityTotal >= 8 ? 'High-priority load is dense. Consider narrowing the active mainline.' : '', + stats.reminderTotal >= Math.max(6, stats.activeDays) ? 'Reminder volume is high. A fixed review block may help reduce interruption cost.' : '', ].filter(Boolean) + if (items.length > 0) return items.slice(0, 3) - return ['本月节啬相对稳定,下一步可以把重点事项再收到更清晰的主线。'] + return ['Execution rhythm looks stable.'] }) const sidebarFeedItems = computed(() => [ - { id: 'fallback-1', title: 'AI 研发节啬继续升温,模型与工作流一体化成为主溜话题。', meta: 'Industry' }, - { id: 'fallback-2', title: '本地知识库与计划系统的联动体验,正在成为效率工具的新竞争点。', meta: 'Product' }, - { id: 'fallback-3', title: '建议接入真实 RSS 源后替换当前占位卡片,以获得即时资讯流。', meta: 'System' }, + { id: 'fallback-1', title: 'Task orchestration is replacing simple one-shot assistants in modern AI workflows.', meta: 'Industry' }, + { id: 'fallback-2', title: 'Shared planning state across chat and execution surfaces is becoming a core product boundary.', meta: 'Product' }, + { id: 'fallback-3', title: 'This feed is still placeholder content and can be replaced by a real RSS source later.', meta: 'System' }, ]) - const topbarFeedItems = computed(() => sidebarFeedItems.value.length > 0 ? [...sidebarFeedItems.value, ...sidebarFeedItems.value] : []) + const topbarFeedItems = computed(() => ( + sidebarFeedItems.value.length > 0 ? [...sidebarFeedItems.value, ...sidebarFeedItems.value] : [] + )) - async function loadSidebarPlanSnapshot(date = new Date()) { + async function loadIssueStatusSnapshot() { + try { + const response = await taskApi.list() + issueTasks.value = response.data.filter((task) => task.status !== 'cancelled') + } catch (err) { + console.warn('Failed to load issue status snapshot:', err) + issueTasks.value = [] + } + } + + async function loadDailyPlanSnapshot(date = new Date()) { const dateKey = formatDateKey(date) const monthKey = formatMonthKey(date) try { @@ -213,6 +356,13 @@ export function useSidebarPlan( } } + async function loadSidebarPlanSnapshot(date = new Date()) { + await Promise.all([ + loadDailyPlanSnapshot(date), + loadIssueStatusSnapshot(), + ]) + } + watch(todayDateKey, (next, previous) => { if (next === previous) return void loadDailyDigestFn() @@ -225,15 +375,46 @@ export function useSidebarPlan( onMounted(() => { void loadDailyDigestFn() - void loadSidebarPlanSnapshot(new Date()) + void loadSidebarPlanSnapshot(clientTimeRef.value) + if (typeof window !== 'undefined') { + window.addEventListener('jarvis:today-status-refresh', handleExternalRefresh) + } + }) + + function handleExternalRefresh() { + void loadSidebarPlanSnapshot(clientTimeRef.value) + } + + onUnmounted(() => { + if (typeof window !== 'undefined') { + window.removeEventListener('jarvis:today-status-refresh', handleExternalRefresh) + } }) return { - todayPlanDetail, monthPlanDays, todayDateKey, monthPlanSummaryMap, - calendarCells, calendarYear, calendarMonth, todayPlanCounters, monthReviewStats, - sidebarWeekLabels, sidebarStatusHeadline, sidebarStatusBreakdown, - sidebarFocusItems, sidebarReviewAchievements, sidebarReviewReflections, - sidebarFeedItems, topbarFeedItems, loadSidebarPlanSnapshot, sidebarCollapsedModules, - selectedDate, selectCalendarDate, conversationDateMap + todayPlanDetail, + monthPlanDays, + todayDateKey, + monthPlanSummaryMap, + calendarCells, + calendarYear, + calendarMonth, + issueStatusCounters, + monthReviewStats, + sidebarWeekLabels, + sidebarStatusHeadline, + sidebarStatusBreakdown, + sidebarFocusItems, + issueStatusQuadrants, + issueCommanderSummary, + sidebarReviewAchievements, + sidebarReviewReflections, + sidebarFeedItems, + topbarFeedItems, + loadSidebarPlanSnapshot, + sidebarCollapsedModules, + selectedDate, + selectCalendarDate, + conversationDateMap, } } diff --git a/frontend/src/pages/chat/index.vue b/frontend/src/pages/chat/index.vue index 249b979..bee65bd 100644 --- a/frontend/src/pages/chat/index.vue +++ b/frontend/src/pages/chat/index.vue @@ -1,6 +1,7 @@