From b28a5d635cc791f0d410ee104f6914103bb651eb Mon Sep 17 00:00:00 2001 From: yanweidong Date: Wed, 22 Jul 2026 16:26:05 +0800 Subject: [PATCH] feat(web): refine AI sessions and task filtering --- README.md | 4 +- apps/web_v1/scripts/structure-check.mjs | 14 +- apps/web_v1/scripts/visual-check.mjs | 141 ++++++++++- apps/web_v1/src/App.css | 228 +++++++++++++++--- apps/web_v1/src/api/mappers.tsx | 2 +- apps/web_v1/src/pages/projects/project-ai.tsx | 168 ++++++++----- .../pages/projects/project-new-channel.tsx | 2 +- .../src/pages/projects/project-tasks.tsx | 43 +++- apps/web_v1/src/pages/workspace-body.tsx | 4 +- 9 files changed, 484 insertions(+), 122 deletions(-) diff --git a/README.md b/README.md index 9c54aa1..0f4381f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ 森林AI 面向个人知识工作者、团队负责人和可信的内部协作成员,帮助用户把分散的事项、资料与沟通内容整理到清晰的项目空间中。 -每个项目都有独立的概况、工作计划、AI 对话、笔记资料和计划任务。用户既可以在工作台查看所有项目的整体进展,也可以进入具体项目,围绕目标持续收集信息、安排工作、沉淀知识并复盘成果。 +每个项目都有独立的概况、工作计划、AI 会话、笔记资料和计划任务。用户既可以在工作台查看所有项目的整体进展,也可以进入具体项目,围绕目标持续收集信息、安排工作、沉淀知识并复盘成果。 ## 适用场景 @@ -38,7 +38,7 @@ Inbox 用于承接尚未归类的想法、消息和材料。用户可以查看 任务按项目组织,可记录标题、说明、标签、负责人、创建时间和完成状态。项目内标签帮助用户按主题或优先级整理计划,同时保留进行中与已完成事项,方便跟进和复盘。 -### AI 对话 +### AI 会话 用户可以在项目内创建带有项目背景的 AI 会话入口,提前明确本次协作的目标和参考上下文。AI 输出与正式项目内容相互区分,需要用户确认后才能转化为任务、笔记或资料。 diff --git a/apps/web_v1/scripts/structure-check.mjs b/apps/web_v1/scripts/structure-check.mjs index af81095..95f88f9 100644 --- a/apps/web_v1/scripts/structure-check.mjs +++ b/apps/web_v1/scripts/structure-check.mjs @@ -119,14 +119,22 @@ if (!aiApiSource.includes('signal,')) failures.push('AI API requests must pass t if (/\b(?:task|note|source)Id\b/.test(aiApiSource)) failures.push('AI session responses must not expose automatic formal object IDs') const aiPageSource = readFileSync('src/pages/projects/project-ai.tsx', 'utf8') -for (const required of ['AI 对话', '创建会话', 'loading', 'error']) { +for (const required of ['新建会话', 'agent-chat-shell', 'agent-composer', 'IconAttachment', 'agent-send-button', 'onPressEnter', 'loading', 'error']) { if (!aiPageSource.includes(required)) failures.push(`project AI page must include ${required}`) } for (const required of ['AbortController', 'generationRef', 'projectRef', 'setSessions([])']) { if (!aiPageSource.includes(required)) failures.push(`project AI page must gate stale requests with ${required}`) } -for (const forbidden of ['DeepSeek V4.0 Flash', '给 DeepSeek 发送消息', 'IconAttachment', 'agent-send-button']) { - if (aiPageSource.includes(forbidden)) failures.push(`project AI page contains unsupported chat control ${forbidden}`) +if (aiPageSource.includes('overview-head') || aiPageSource.includes(' { return } if (url.pathname === `/api/v1/projects/${projectId}/ai-sessions`) { - await route.fulfill({ json: [] }) + if (route.request().method() === 'POST') { + const input = route.request().postDataJSON() + await route.fulfill({ + json: { + id: '019b0000-0000-7000-8000-000000000010', + projectId, + title: input.title, + context: input.context, + status: 'ready', + createdAt: '2026-07-22T09:00:00Z', + updatedAt: '2026-07-22T09:00:00Z', + }, + }) + return + } + await route.fulfill({ + json: [{ + id: '019b0000-0000-7000-8000-000000000009', + projectId, + title: '报价分析', + context: '分析当前方案的报价结构。', + status: 'ready', + createdAt: '2026-07-22T08:30:00Z', + updatedAt: '2026-07-22T08:30:00Z', + }], + }) return } await route.fulfill({ status: 404, json: { error: { code: 'not_found', message: '视觉检查未配置该接口' } } }) @@ -238,16 +309,18 @@ await page.locator('.channel-list').hover() const channelListHoverMetrics = await collectMetrics() const channelPageChecks = [] +let taskTagFilterCheck = null +let aiSessionCheck = null for (const channel of [ { label: '工作计划', pageClass: 'project-tasks-page', expectedHeading: '工作计划' }, - { label: 'AI 对话', pageClass: 'project-ai-page', expectedHeading: 'AI 对话' }, + { label: 'AI 会话', pageClass: 'project-ai-page', expectedHeading: '' }, { label: '笔记资料', pageClass: 'project-notes-page', expectedHeading: '笔记资料' }, { label: '计划任务', pageClass: 'project-cron-page', expectedHeading: '计划任务' }, { label: '新建频道', pageClass: 'project-new-channel-page', expectedHeading: '新建频道' }, ]) { await page.locator('.channel-button', { hasText: channel.label }).click() await page.waitForTimeout(250) - channelPageChecks.push(await page.evaluate((expected) => { + const pageCheck = await page.evaluate((expected) => { const pageNode = document.querySelector(`.${expected.pageClass}`) const heading = pageNode?.querySelector('h4, h5')?.textContent ?? '' const activeChannel = document.querySelector('.channel-button.active')?.textContent ?? '' @@ -258,8 +331,37 @@ for (const channel of [ activeChannel, inputCount: pageNode?.querySelectorAll('input, textarea').length ?? 0, hasSaveChannel: [...(pageNode?.querySelectorAll('button') ?? [])].some((button) => button.textContent?.includes('保存频道')), + hasOverviewHead: Boolean(pageNode?.querySelector('.overview-head')), + hasSessionList: Boolean(pageNode?.querySelector('.agent-session-list')), + hasComposer: Boolean(pageNode?.querySelector('.agent-composer')), } - }, channel)) + }, channel) + + if (channel.label === '工作计划') { + await page.getByRole('button', { name: '设计', exact: true }).click() + await page.waitForTimeout(150) + taskTagFilterCheck = await page.evaluate(() => ({ + activeTag: document.querySelector('.task-tag-filter.active')?.textContent?.trim() ?? '', + pendingTitles: [...document.querySelectorAll('.task-plan-title')].map((node) => node.textContent?.trim()), + completedTitles: [...document.querySelectorAll('.completed-plan-title')].map((node) => node.textContent?.trim()), + pressed: document.querySelector('.task-tag-filter[aria-pressed="true"]')?.textContent?.trim() ?? '', + })) + await page.screenshot({ path: 'test-results/project-task-tag-filter-light.png', fullPage: true }) + } + + if (channel.label === 'AI 会话') { + await page.locator('.agent-composer textarea').fill('请分析这个项目的下一步计划') + await page.getByRole('button', { name: '发送消息' }).click() + await page.waitForSelector('.agent-user-message') + aiSessionCheck = await page.evaluate(() => ({ + userMessage: document.querySelector('.agent-user-message')?.textContent?.trim() ?? '', + activeSession: document.querySelector('.agent-session.active')?.textContent?.trim() ?? '', + composerValue: document.querySelector('.agent-composer textarea')?.value ?? '', + })) + await page.screenshot({ path: 'test-results/project-ai-session-light.png', fullPage: true }) + } + + channelPageChecks.push(pageCheck) } await page.locator('.topbar-actions .arco-btn').first().click() @@ -280,6 +382,8 @@ console.log(JSON.stringify({ stageHoverMetrics, channelListHoverMetrics, channelPageChecks, + taskTagFilterCheck, + aiSessionCheck, errors, }, null, 2)) @@ -439,7 +543,7 @@ for (const check of channelPageChecks) { if (!check.foundPage) { failures.push(`expected ${check.label} to render .${check.pageClass}`) } - if (!check.heading.includes(check.expectedHeading)) { + if (check.expectedHeading && !check.heading.includes(check.expectedHeading)) { failures.push(`expected ${check.label} heading to include ${check.expectedHeading}, got ${JSON.stringify(check.heading)}`) } if (!check.activeChannel.includes(check.label)) { @@ -448,6 +552,27 @@ for (const check of channelPageChecks) { if (check.label === '新建频道' && (check.inputCount < 4 || !check.hasSaveChannel)) { failures.push(`expected legacy new channel editor, got inputs=${check.inputCount}, save=${check.hasSaveChannel}`) } + if (check.label === 'AI 会话' && (check.hasOverviewHead || !check.hasSessionList || !check.hasComposer)) { + failures.push(`expected headerless AI session chat layout, got ${JSON.stringify(check)}`) + } +} + +if ( + !taskTagFilterCheck || + taskTagFilterCheck.activeTag !== '设计' || + taskTagFilterCheck.pressed !== '设计' || + taskTagFilterCheck.pendingTitles.join('|') !== '优化会话界面' || + taskTagFilterCheck.completedTitles.length !== 0 +) { + failures.push(`expected clickable task tags to filter pending and completed plans, got ${JSON.stringify(taskTagFilterCheck)}`) +} +if ( + !aiSessionCheck || + aiSessionCheck.userMessage !== '请分析这个项目的下一步计划' || + !aiSessionCheck.activeSession.includes('请分析这个项目的下一步计划') || + aiSessionCheck.composerValue !== '' +) { + failures.push(`expected AI composer to create and select a session, got ${JSON.stringify(aiSessionCheck)}`) } if (failures.length) { diff --git a/apps/web_v1/src/App.css b/apps/web_v1/src/App.css index 1ea1dff..564854d 100644 --- a/apps/web_v1/src/App.css +++ b/apps/web_v1/src/App.css @@ -1529,15 +1529,30 @@ } } +.project-ai-page { + position: relative; + min-height: 0; +} + .agent-chat-shell { - min-height: calc(100vh - 170px); + height: calc(100vh - 90px); + min-height: 620px; display: grid; - grid-template-columns: 280px minmax(0, 1fr); + grid-template-columns: 248px minmax(0, 1fr); gap: 0; - margin: -12px -16px -16px; + margin: -16px; background: var(--senlin-panel); } +.agent-request-error { + position: absolute; + z-index: 8; + top: 12px; + left: 50%; + width: min(560px, calc(100% - 48px)); + transform: translateX(-50%); +} + .agent-session-list.queue-section.arco-card, .agent-chat-panel.queue-section.arco-card { border-top: 0; @@ -1556,20 +1571,20 @@ height: 100%; display: flex; flex-direction: column; - gap: 28px; - padding: 18px 20px; + gap: 22px; + padding: 18px 14px; } .agent-new-chat.arco-btn { - height: 42px; - justify-content: center; - border: 1px solid var(--senlin-border); - border-radius: 999px; - background: var(--senlin-panel); - color: var(--senlin-text); + height: 40px; + justify-content: flex-start; + border: 1px solid color-mix(in srgb, var(--senlin-primary) 26%, var(--senlin-border)); + border-radius: 10px; + background: color-mix(in srgb, var(--senlin-primary) 8%, var(--senlin-panel)); + color: var(--senlin-primary); font-size: 14px; font-weight: 600; - box-shadow: 0 4px 14px rgba(29, 33, 41, 0.08); + box-shadow: none; } .agent-new-chat .arco-icon { @@ -1578,7 +1593,7 @@ .agent-session-groups { display: grid; - gap: 28px; + gap: 20px; overflow: auto; scrollbar-width: none; } @@ -1589,66 +1604,152 @@ .agent-session-group { display: grid; - gap: 12px; + gap: 5px; } .agent-session-group > .arco-typography { - font-size: 14px; + margin: 0 8px 5px; + font-size: 12px; } .agent-session { width: 100%; + min-width: 0; overflow: hidden; + display: flex; + align-items: center; + gap: 8px; border: 0; - border-radius: 6px; + border-radius: 8px; background: transparent; color: var(--senlin-text); - padding: 5px 0; + padding: 9px 10px; text-align: left; + cursor: pointer; + font-size: 13px; + line-height: 1.45; +} + +.agent-session span { + min-width: 0; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - cursor: pointer; - font-size: 14px; - line-height: 1.45; +} + +.agent-session .arco-icon { + flex: 0 0 auto; + color: var(--senlin-muted); } .agent-session:hover, .agent-session.active { + background: color-mix(in srgb, var(--senlin-primary) 8%, var(--senlin-panel)); color: var(--senlin-primary); } +.agent-session-empty { + margin-top: 56px; +} + .agent-chat-panel.queue-section .arco-card-body { position: relative; - min-height: calc(100vh - 170px); + height: 100%; + min-height: 620px; display: grid; grid-template-rows: minmax(0, 1fr) auto; padding: 0; } .agent-chat-main { - display: grid; - place-items: center; - padding: 80px 32px 190px; + min-height: 0; + overflow-y: auto; + display: flex; + justify-content: center; + padding: 52px 32px 34px; + scrollbar-width: thin; } -.agent-chat-main h2 { +.agent-empty-state { + align-self: center; + display: grid; + justify-items: center; + gap: 14px; + color: var(--senlin-muted); +} + +.agent-empty-icon { + width: 58px; + height: 58px; + display: grid; + place-items: center; + border-radius: 18px; + background: var(--senlin-soft-blue); + color: var(--senlin-primary); + font-size: 28px; +} + +.agent-thread { + width: min(780px, 100%); + display: grid; + align-content: start; + gap: 34px; +} + +.agent-user-message { + max-width: 78%; + justify-self: end; + border-radius: 18px 18px 4px 18px; + background: var(--senlin-soft-blue); + color: var(--senlin-text); + padding: 12px 16px; + font-size: 14px; + line-height: 1.7; + white-space: pre-wrap; +} + +.agent-assistant-message { + display: grid; + grid-template-columns: 32px minmax(0, 1fr); + align-items: start; + gap: 12px; +} + +.agent-assistant-avatar { + width: 32px; + height: 32px; + display: grid; + place-items: center; + border-radius: 10px; + background: var(--senlin-primary); + color: #fff; +} + +.agent-assistant-name { + display: block; + margin-bottom: 7px; + color: var(--senlin-text); + font-weight: 700; +} + +.agent-assistant-message p { margin: 0; color: var(--senlin-text); - font-size: 24px; - font-weight: 800; + font-size: 14px; + line-height: 1.75; } .agent-composer { - width: min(860px, calc(100% - 80px)); + width: min(780px, calc(100% - 64px)); display: grid; - gap: 12px; + gap: 10px; justify-self: center; - margin: 0 0 28px; + margin: 0 0 24px; border: 1px solid var(--senlin-border); - border-radius: 24px; + border-radius: 20px; background: var(--senlin-panel); - padding: 18px 16px 14px; - box-shadow: 0 16px 40px rgba(29, 33, 41, 0.08); + padding: 14px 14px 10px; + box-shadow: 0 12px 32px rgba(29, 33, 41, 0.1); } .agent-composer .arco-textarea-wrapper { @@ -1659,7 +1760,8 @@ .agent-composer textarea { color: var(--senlin-text); - font-size: 15px; + font-size: 14px; + line-height: 1.6; } .agent-composer textarea::placeholder { @@ -1674,7 +1776,7 @@ } .agent-model-chip.arco-btn { - height: 36px; + height: 32px; border-color: color-mix(in srgb, var(--senlin-primary) 32%, var(--senlin-border)); border-radius: 999px; background: var(--senlin-soft-blue); @@ -1683,9 +1785,21 @@ } .agent-send-button.arco-btn { - width: 38px; - height: 38px; - background: #aebeff; + width: 34px; + height: 34px; +} + +.agent-composer-actions { + display: flex; + align-items: center; + gap: 4px; +} + +.agent-composer-hint { + display: block; + padding: 0 6px; + text-align: center; + font-size: 11px; } .theme-dark .agent-session-list.queue-section.arco-card { @@ -1697,6 +1811,16 @@ box-shadow: 0 16px 40px rgb(0 0 0 / 22%); } +@media (max-width: 960px) { + .agent-chat-shell { + grid-template-columns: 210px minmax(0, 1fr); + } + + .agent-composer { + width: calc(100% - 32px); + } +} + .reply-box, .chat-input { display: grid; @@ -1728,6 +1852,34 @@ padding: 12px; } +.task-tag-filters { + gap: 8px !important; +} + +.task-tag-filter { + display: inline-flex; + border: 0; + border-radius: 4px; + background: transparent; + padding: 0; + cursor: pointer; +} + +.task-tag-filter .arco-tag { + margin: 0; + transition: box-shadow 0.16s ease, transform 0.16s ease; +} + +.task-tag-filter:hover .arco-tag { + transform: translateY(-1px); + box-shadow: 0 3px 10px color-mix(in srgb, var(--senlin-primary) 15%, transparent); +} + +.task-tag-filter:focus-visible { + outline: 2px solid color-mix(in srgb, var(--senlin-primary) 55%, transparent); + outline-offset: 2px; +} + .task-plan-card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); diff --git a/apps/web_v1/src/api/mappers.tsx b/apps/web_v1/src/api/mappers.tsx index c0dbb28..1ca5b88 100644 --- a/apps/web_v1/src/api/mappers.tsx +++ b/apps/web_v1/src/api/mappers.tsx @@ -95,7 +95,7 @@ function channelLabel(channel: WorkspaceChannelDTO) { case 'tasks': return '工作计划' case 'ai_sessions': - return 'AI 对话' + return 'AI 会话' case 'notes_sources': return '笔记资料' case 'cron': diff --git a/apps/web_v1/src/pages/projects/project-ai.tsx b/apps/web_v1/src/pages/projects/project-ai.tsx index 5147c21..452bab9 100644 --- a/apps/web_v1/src/pages/projects/project-ai.tsx +++ b/apps/web_v1/src/pages/projects/project-ai.tsx @@ -1,10 +1,10 @@ -import { useEffect, useRef, useState } from 'react' -import { Alert, Button, Card, Empty, Input, Space, Spin, Tag, Typography } from '@arco-design/web-react' -import { IconPlusCircle, IconRobot } from '@arco-design/web-react/icon' +import { useEffect, useMemo, useRef, useState } from 'react' +import { Alert, Button, Card, Empty, Input, Spin, Typography } from '@arco-design/web-react' +import { IconArrowUp, IconAttachment, IconPlus, IconRobot } from '@arco-design/web-react/icon' import type { AISessionDTO, CreateAISessionInput } from '../../api/ai' import type { ProjectWorkspace } from './project-types' -const { Title, Text } = Typography +const { Text } = Typography export function ProjectAi({ activeWorkspace, @@ -18,8 +18,8 @@ export function ProjectAi({ onCreateSession: (projectId: string, input: CreateAISessionInput, signal?: AbortSignal) => Promise<AISessionDTO> }) { const [sessions, setSessions] = useState<AISessionDTO[]>([]) - const [title, setTitle] = useState('') - const [context, setContext] = useState('') + const [selectedSessionID, setSelectedSessionID] = useState<string | null>(null) + const [prompt, setPrompt] = useState('') const [loading, setLoading] = useState(true) const [creating, setCreating] = useState(false) const [error, setError] = useState('') @@ -38,8 +38,8 @@ export function ProjectAi({ listControllerRef.current = controller createControllerRef.current = null setSessions([]) - setTitle('') - setContext('') + setSelectedSessionID(null) + setPrompt('') setLoading(true) setCreating(false) setError('') @@ -68,10 +68,15 @@ export function ProjectAi({ } }, [onListSessions, projectId]) + const selectedSession = useMemo( + () => sessions.find((session) => session.id === selectedSessionID) ?? null, + [selectedSessionID, sessions], + ) + const createSession = async () => { - const trimmedTitle = title.trim() - if (!trimmedTitle) { - setError('请输入 AI 会话标题') + const message = prompt.trim() + if (!message) { + setError('请输入会话内容') return } const generation = ++generationRef.current @@ -87,13 +92,13 @@ export function ProjectAi({ setError('') try { const created = await onCreateSession(projectId, { - title: trimmedTitle, - context: context.trim(), + title: sessionTitle(message), + context: message, }, controller.signal) if (!isCurrent()) return setSessions((current) => [created, ...current.filter((session) => session.id !== created.id)]) - setTitle('') - setContext('') + setSelectedSessionID(created.id) + setPrompt('') onSelectItem(created.title) } catch (requestError) { if (!isCurrent()) return @@ -106,67 +111,112 @@ export function ProjectAi({ } } - return ( - <div className="project-channel-page project-ai-page overview-page"> - <div className="overview-head"> - <div> - <Title heading={4}>AI 对话 - 创建项目内普通会话;不会自动生成任务、笔记或资料。 - - + function selectSession(session: AISessionDTO) { + setSelectedSessionID(session.id) + setError('') + onSelectItem(session.title) + } + return ( +
{error ? setError('')} /> : null}
- + + + {sessions.length ? (
- {sessions.map((session) => ( - - ))} +
+ 最近会话 + {sessions.map((session) => ( + + ))} +
- ) : loading ? null : } + ) : loading ? null : }
- -
- 创建会话 - 会话状态为“待开始”只表示入口已建立,不代表 AI 已完成处理。 +
+ {selectedSession ? ( +
+
{selectedSession.context || selectedSession.title}
+
+ +
+ 森林AI +

会话已创建。你可以继续补充问题或项目背景。

+
+
+
+ ) : ( +
+ + 在下方输入内容,开始新的项目会话 +
+ )} +
+ +
+ { + if (!event.shiftKey) { + event.preventDefault() + void createSession() + } + }} + /> +
+ +
+
- - - - AI 输出如需转为正式对象,必须另行确认。 - + AI 生成内容仅作为建议,转为正式对象前仍需确认 +
) } -function sessionStatusLabel(status: string) { - if (status === 'ready') return '待开始' - if (status === 'failed') return '创建失败' - return status || '未知状态' +function sessionTitle(message: string) { + const firstLine = message.split(/\r?\n/, 1)[0].trim() + const characters = Array.from(firstLine) + return characters.length > 36 ? `${characters.slice(0, 36).join('')}…` : firstLine } diff --git a/apps/web_v1/src/pages/projects/project-new-channel.tsx b/apps/web_v1/src/pages/projects/project-new-channel.tsx index f049dfe..51f8032 100644 --- a/apps/web_v1/src/pages/projects/project-new-channel.tsx +++ b/apps/web_v1/src/pages/projects/project-new-channel.tsx @@ -54,7 +54,7 @@ export function ProjectNewChannel({ activeWorkspace }: { activeWorkspace: Projec 外部资料库 项目流程页 RSS/探索结果 - AI 对话上下文 + AI 会话上下文
diff --git a/apps/web_v1/src/pages/projects/project-tasks.tsx b/apps/web_v1/src/pages/projects/project-tasks.tsx index e5bcbcb..96aceee 100644 --- a/apps/web_v1/src/pages/projects/project-tasks.tsx +++ b/apps/web_v1/src/pages/projects/project-tasks.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { Button, Card, Descriptions, Input, Modal, Progress, Space, Tag, Typography } from '@arco-design/web-react' import { IconArrowLeft, IconCalendar, IconCheckCircle, IconCheckCircleFill, IconClockCircle, IconPlus } from '@arco-design/web-react/icon' import { ProjectTaskEditModal, type ProjectTaskUpdate } from './project-task-edit-modal' @@ -26,12 +26,20 @@ export function ProjectTasks({ }) { const { tasks, project, tags } = activeWorkspace const activeTask = tasks.find((task) => task.id === activeTaskID) - const pendingTasks = tasks.filter((task) => !task.completed) - const completedTasks = tasks.filter((task) => task.completed) const projectTags = tags.filter((tag) => tag !== 'all' && tag !== '全部') + const [selectedTag, setSelectedTag] = useState('全部') const [tagModalOpen, setTagModalOpen] = useState(false) const [tagName, setTagName] = useState('') const [editingTask, setEditingTask] = useState(null) + const visibleTasks = selectedTag === '全部' ? tasks : tasks.filter((task) => task.tag === selectedTag) + const pendingTasks = visibleTasks.filter((task) => !task.completed) + const completedTasks = visibleTasks.filter((task) => task.completed) + + useEffect(() => { + if (selectedTag !== '全部' && !projectTags.includes(selectedTag)) { + setSelectedTag('全部') + } + }, [projectTags, selectedTag]) if (activeTask) { return @@ -60,10 +68,25 @@ export function ProjectTasks({ 标签 - - 全部 + + {projectTags.map((tag) => ( - {tag} + ))} @@ -84,7 +107,9 @@ export function ProjectTasks({ ))} - {pendingTasks.length === 0 && 当前没有未完成计划} + {pendingTasks.length === 0 && ( + {selectedTag === '全部' ? '当前没有未完成计划' : `“${selectedTag}”标签下没有未完成计划`} + )} @@ -104,7 +129,9 @@ export function ProjectTasks({ {task.duration || '未知'} ))} - {completedTasks.length === 0 && 当前没有完成计划} + {completedTasks.length === 0 && ( + {selectedTag === '全部' ? '当前没有完成计划' : `“${selectedTag}”标签下没有完成计划`} + )} , color: 'arcoblue' }, { title: '未完成计划', value: unfinishedTasks.length, delta: '跨项目聚合', icon: , color: 'green' }, - { title: 'AI 对话', value: aiSessions.length, delta: '项目内上下文', icon: , color: 'purple' }, + { title: 'AI 会话', value: aiSessions.length, delta: '项目内上下文', icon: , color: 'purple' }, { title: '知识资料', value: notes.length, delta: '笔记与资料合计', icon: , color: 'cyan' }, ] @@ -89,7 +89,7 @@ export function WorkspacePage({
工作台 - 所有项目的统一工作台,汇总项目任务、AI 对话和知识资产。 + 所有项目的统一工作台,汇总项目任务、AI 会话和知识资产。