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 <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-11 08:48:37 +08:00
parent c70e7e7253
commit 7e6eb6a7b3
5 changed files with 586 additions and 138 deletions

View File

@@ -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<TaskQuadrant, Omit<TodayStatusQuadrantView, 'tasks'>> = {
'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<string, string> = {
todo: 'Pending',
in_progress: 'In progress',
done: 'Done',
cancelled: 'Cancelled',
}
const dispatchMap: Record<string, string> = {
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<ScheduleCenterDateResponse | null>(null)
const monthPlanDays = ref<ScheduleCenterDaySummary[]>([])
const issueTasks = ref<Task[]>([])
const selectedDate = ref<string | null>(null)
// Build a map of date -> has conversation
const conversationDateMap = computed(() => {
const map = new Map<string, boolean>()
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<TodayStatusQuadrantView[]>(() => (
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<ScheduleCenterCommanderSummary>(() => 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<SidebarFocusItem[]>(() => {
// 暂时强制返回模拟数据用于测试
return mockFocusItems
})
const sidebarFocusItems = computed<SidebarFocusItem[]>(() => (
(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<SidebarNewsItem[]>(() => [
{ 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,
}
}