feat(web): refine AI sessions and task filtering
This commit is contained in:
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
森林AI 面向个人知识工作者、团队负责人和可信的内部协作成员,帮助用户把分散的事项、资料与沟通内容整理到清晰的项目空间中。
|
森林AI 面向个人知识工作者、团队负责人和可信的内部协作成员,帮助用户把分散的事项、资料与沟通内容整理到清晰的项目空间中。
|
||||||
|
|
||||||
每个项目都有独立的概况、工作计划、AI 对话、笔记资料和计划任务。用户既可以在工作台查看所有项目的整体进展,也可以进入具体项目,围绕目标持续收集信息、安排工作、沉淀知识并复盘成果。
|
每个项目都有独立的概况、工作计划、AI 会话、笔记资料和计划任务。用户既可以在工作台查看所有项目的整体进展,也可以进入具体项目,围绕目标持续收集信息、安排工作、沉淀知识并复盘成果。
|
||||||
|
|
||||||
## 适用场景
|
## 适用场景
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ Inbox 用于承接尚未归类的想法、消息和材料。用户可以查看
|
|||||||
|
|
||||||
任务按项目组织,可记录标题、说明、标签、负责人、创建时间和完成状态。项目内标签帮助用户按主题或优先级整理计划,同时保留进行中与已完成事项,方便跟进和复盘。
|
任务按项目组织,可记录标题、说明、标签、负责人、创建时间和完成状态。项目内标签帮助用户按主题或优先级整理计划,同时保留进行中与已完成事项,方便跟进和复盘。
|
||||||
|
|
||||||
### AI 对话
|
### AI 会话
|
||||||
|
|
||||||
用户可以在项目内创建带有项目背景的 AI 会话入口,提前明确本次协作的目标和参考上下文。AI 输出与正式项目内容相互区分,需要用户确认后才能转化为任务、笔记或资料。
|
用户可以在项目内创建带有项目背景的 AI 会话入口,提前明确本次协作的目标和参考上下文。AI 输出与正式项目内容相互区分,需要用户确认后才能转化为任务、笔记或资料。
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
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')
|
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}`)
|
if (!aiPageSource.includes(required)) failures.push(`project AI page must include ${required}`)
|
||||||
}
|
}
|
||||||
for (const required of ['AbortController', 'generationRef', 'projectRef', 'setSessions([])']) {
|
for (const required of ['AbortController', 'generationRef', 'projectRef', 'setSessions([])']) {
|
||||||
if (!aiPageSource.includes(required)) failures.push(`project AI page must gate stale requests with ${required}`)
|
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('overview-head') || aiPageSource.includes('<Title heading=')) {
|
||||||
if (aiPageSource.includes(forbidden)) failures.push(`project AI page contains unsupported chat control ${forbidden}`)
|
failures.push('project AI page must not render a title or subtitle header')
|
||||||
|
}
|
||||||
|
for (const forbidden of ['DeepSeek V4.0 Flash', '给 DeepSeek 发送消息']) {
|
||||||
|
if (aiPageSource.includes(forbidden)) failures.push(`project AI page contains outdated or borrowed copy ${forbidden}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tasksPageSource = readFileSync('src/pages/projects/project-tasks.tsx', 'utf8')
|
||||||
|
for (const required of ['selectedTag', 'setSelectedTag', 'visibleTasks', 'task-tag-filter', 'aria-pressed']) {
|
||||||
|
if (!tasksPageSource.includes(required)) failures.push(`project tasks page must support tag filtering with ${required}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const explorePageSource = readFileSync('src/pages/workspace-explore.tsx', 'utf8')
|
const explorePageSource = readFileSync('src/pages/workspace-explore.tsx', 'utf8')
|
||||||
|
|||||||
@@ -37,11 +37,14 @@ const visualCheckWorkspace = {
|
|||||||
{ id: 'overview', projectId, type: 'overview', title: '概况', icon: 'home', count: 0, url: '', sortOrder: 0 },
|
{ id: 'overview', projectId, type: 'overview', title: '概况', icon: 'home', count: 0, url: '', sortOrder: 0 },
|
||||||
{ id: 'inbox', projectId, type: 'inbox', title: 'Inbox 消息流', icon: 'email', count: 2, url: '', sortOrder: 1 },
|
{ id: 'inbox', projectId, type: 'inbox', title: 'Inbox 消息流', icon: 'email', count: 2, url: '', sortOrder: 1 },
|
||||||
{ id: 'tasks', projectId, type: 'tasks', title: '工作计划', icon: 'list', count: 0, url: '', sortOrder: 1 },
|
{ id: 'tasks', projectId, type: 'tasks', title: '工作计划', icon: 'list', count: 0, url: '', sortOrder: 1 },
|
||||||
{ id: 'ai', projectId, type: 'ai_sessions', title: 'AI 对话', icon: 'robot', count: 0, url: '', sortOrder: 2 },
|
{ id: 'ai', projectId, type: 'ai_sessions', title: 'AI 会话', icon: 'robot', count: 0, url: '', sortOrder: 2 },
|
||||||
{ id: 'notes', projectId, type: 'notes_sources', title: '笔记资料', icon: 'file', count: 0, url: '', sortOrder: 3 },
|
{ id: 'notes', projectId, type: 'notes_sources', title: '笔记资料', icon: 'file', count: 0, url: '', sortOrder: 3 },
|
||||||
{ id: 'cron', projectId, type: 'cron', title: '计划任务', icon: 'clock', count: 0, url: '', sortOrder: 4 },
|
{ id: 'cron', projectId, type: 'cron', title: '计划任务', icon: 'clock', count: 0, url: '', sortOrder: 4 },
|
||||||
],
|
],
|
||||||
tags: [{ id: '019b0000-0000-7000-8000-000000000002', name: '产品' }],
|
tags: [
|
||||||
|
{ id: '019b0000-0000-7000-8000-000000000002', name: '产品' },
|
||||||
|
{ id: '019b0000-0000-7000-8000-000000000005', name: '设计' },
|
||||||
|
],
|
||||||
recentSessions: [],
|
recentSessions: [],
|
||||||
inbox: [
|
inbox: [
|
||||||
{
|
{
|
||||||
@@ -65,7 +68,50 @@ const visualCheckWorkspace = {
|
|||||||
time: '2026-07-22T07:00:00Z',
|
time: '2026-07-22T07:00:00Z',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
tasks: [],
|
tasks: [
|
||||||
|
{
|
||||||
|
id: '019b0000-0000-7000-8000-000000000006',
|
||||||
|
projectId,
|
||||||
|
title: '梳理产品需求',
|
||||||
|
summary: '确认首版工作台的功能边界。',
|
||||||
|
completed: false,
|
||||||
|
owner: '森林AI',
|
||||||
|
createdAt: '2026-07-22',
|
||||||
|
completedAt: '',
|
||||||
|
duration: '',
|
||||||
|
progress: '40%',
|
||||||
|
tagId: '019b0000-0000-7000-8000-000000000002',
|
||||||
|
tag: '产品',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '019b0000-0000-7000-8000-000000000007',
|
||||||
|
projectId,
|
||||||
|
title: '优化会话界面',
|
||||||
|
summary: '统一 AI 会话的阅读与输入体验。',
|
||||||
|
completed: false,
|
||||||
|
owner: '森林AI',
|
||||||
|
createdAt: '2026-07-22',
|
||||||
|
completedAt: '',
|
||||||
|
duration: '',
|
||||||
|
progress: '60%',
|
||||||
|
tagId: '019b0000-0000-7000-8000-000000000005',
|
||||||
|
tag: '设计',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '019b0000-0000-7000-8000-000000000008',
|
||||||
|
projectId,
|
||||||
|
title: '确认项目定位',
|
||||||
|
summary: '完成项目功能与亮点说明。',
|
||||||
|
completed: true,
|
||||||
|
owner: '森林AI',
|
||||||
|
createdAt: '2026-07-21',
|
||||||
|
completedAt: '2026-07-22',
|
||||||
|
duration: '1 天',
|
||||||
|
progress: '100%',
|
||||||
|
tagId: '019b0000-0000-7000-8000-000000000002',
|
||||||
|
tag: '产品',
|
||||||
|
},
|
||||||
|
],
|
||||||
aiSessions: [],
|
aiSessions: [],
|
||||||
notesSources: [],
|
notesSources: [],
|
||||||
cronPlans: [],
|
cronPlans: [],
|
||||||
@@ -90,7 +136,32 @@ await page.route('http://localhost:9150/api/v1/**', async (route) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (url.pathname === `/api/v1/projects/${projectId}/ai-sessions`) {
|
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
|
return
|
||||||
}
|
}
|
||||||
await route.fulfill({ status: 404, json: { error: { code: 'not_found', message: '视觉检查未配置该接口' } } })
|
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 channelListHoverMetrics = await collectMetrics()
|
||||||
|
|
||||||
const channelPageChecks = []
|
const channelPageChecks = []
|
||||||
|
let taskTagFilterCheck = null
|
||||||
|
let aiSessionCheck = null
|
||||||
for (const channel of [
|
for (const channel of [
|
||||||
{ label: '工作计划', pageClass: 'project-tasks-page', expectedHeading: '工作计划' },
|
{ 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-notes-page', expectedHeading: '笔记资料' },
|
||||||
{ label: '计划任务', pageClass: 'project-cron-page', expectedHeading: '计划任务' },
|
{ label: '计划任务', pageClass: 'project-cron-page', expectedHeading: '计划任务' },
|
||||||
{ label: '新建频道', pageClass: 'project-new-channel-page', expectedHeading: '新建频道' },
|
{ label: '新建频道', pageClass: 'project-new-channel-page', expectedHeading: '新建频道' },
|
||||||
]) {
|
]) {
|
||||||
await page.locator('.channel-button', { hasText: channel.label }).click()
|
await page.locator('.channel-button', { hasText: channel.label }).click()
|
||||||
await page.waitForTimeout(250)
|
await page.waitForTimeout(250)
|
||||||
channelPageChecks.push(await page.evaluate((expected) => {
|
const pageCheck = await page.evaluate((expected) => {
|
||||||
const pageNode = document.querySelector(`.${expected.pageClass}`)
|
const pageNode = document.querySelector(`.${expected.pageClass}`)
|
||||||
const heading = pageNode?.querySelector('h4, h5')?.textContent ?? ''
|
const heading = pageNode?.querySelector('h4, h5')?.textContent ?? ''
|
||||||
const activeChannel = document.querySelector('.channel-button.active')?.textContent ?? ''
|
const activeChannel = document.querySelector('.channel-button.active')?.textContent ?? ''
|
||||||
@@ -258,8 +331,37 @@ for (const channel of [
|
|||||||
activeChannel,
|
activeChannel,
|
||||||
inputCount: pageNode?.querySelectorAll('input, textarea').length ?? 0,
|
inputCount: pageNode?.querySelectorAll('input, textarea').length ?? 0,
|
||||||
hasSaveChannel: [...(pageNode?.querySelectorAll('button') ?? [])].some((button) => button.textContent?.includes('保存频道')),
|
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()
|
await page.locator('.topbar-actions .arco-btn').first().click()
|
||||||
@@ -280,6 +382,8 @@ console.log(JSON.stringify({
|
|||||||
stageHoverMetrics,
|
stageHoverMetrics,
|
||||||
channelListHoverMetrics,
|
channelListHoverMetrics,
|
||||||
channelPageChecks,
|
channelPageChecks,
|
||||||
|
taskTagFilterCheck,
|
||||||
|
aiSessionCheck,
|
||||||
errors,
|
errors,
|
||||||
}, null, 2))
|
}, null, 2))
|
||||||
|
|
||||||
@@ -439,7 +543,7 @@ for (const check of channelPageChecks) {
|
|||||||
if (!check.foundPage) {
|
if (!check.foundPage) {
|
||||||
failures.push(`expected ${check.label} to render .${check.pageClass}`)
|
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)}`)
|
failures.push(`expected ${check.label} heading to include ${check.expectedHeading}, got ${JSON.stringify(check.heading)}`)
|
||||||
}
|
}
|
||||||
if (!check.activeChannel.includes(check.label)) {
|
if (!check.activeChannel.includes(check.label)) {
|
||||||
@@ -448,6 +552,27 @@ for (const check of channelPageChecks) {
|
|||||||
if (check.label === '新建频道' && (check.inputCount < 4 || !check.hasSaveChannel)) {
|
if (check.label === '新建频道' && (check.inputCount < 4 || !check.hasSaveChannel)) {
|
||||||
failures.push(`expected legacy new channel editor, got inputs=${check.inputCount}, save=${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) {
|
if (failures.length) {
|
||||||
|
|||||||
@@ -1529,15 +1529,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-ai-page {
|
||||||
|
position: relative;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.agent-chat-shell {
|
.agent-chat-shell {
|
||||||
min-height: calc(100vh - 170px);
|
height: calc(100vh - 90px);
|
||||||
|
min-height: 620px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 280px minmax(0, 1fr);
|
grid-template-columns: 248px minmax(0, 1fr);
|
||||||
gap: 0;
|
gap: 0;
|
||||||
margin: -12px -16px -16px;
|
margin: -16px;
|
||||||
background: var(--senlin-panel);
|
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-session-list.queue-section.arco-card,
|
||||||
.agent-chat-panel.queue-section.arco-card {
|
.agent-chat-panel.queue-section.arco-card {
|
||||||
border-top: 0;
|
border-top: 0;
|
||||||
@@ -1556,20 +1571,20 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 28px;
|
gap: 22px;
|
||||||
padding: 18px 20px;
|
padding: 18px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-new-chat.arco-btn {
|
.agent-new-chat.arco-btn {
|
||||||
height: 42px;
|
height: 40px;
|
||||||
justify-content: center;
|
justify-content: flex-start;
|
||||||
border: 1px solid var(--senlin-border);
|
border: 1px solid color-mix(in srgb, var(--senlin-primary) 26%, var(--senlin-border));
|
||||||
border-radius: 999px;
|
border-radius: 10px;
|
||||||
background: var(--senlin-panel);
|
background: color-mix(in srgb, var(--senlin-primary) 8%, var(--senlin-panel));
|
||||||
color: var(--senlin-text);
|
color: var(--senlin-primary);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
box-shadow: 0 4px 14px rgba(29, 33, 41, 0.08);
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-new-chat .arco-icon {
|
.agent-new-chat .arco-icon {
|
||||||
@@ -1578,7 +1593,7 @@
|
|||||||
|
|
||||||
.agent-session-groups {
|
.agent-session-groups {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 28px;
|
gap: 20px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
@@ -1589,66 +1604,152 @@
|
|||||||
|
|
||||||
.agent-session-group {
|
.agent-session-group {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-session-group > .arco-typography {
|
.agent-session-group > .arco-typography {
|
||||||
font-size: 14px;
|
margin: 0 8px 5px;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-session {
|
.agent-session {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--senlin-text);
|
color: var(--senlin-text);
|
||||||
padding: 5px 0;
|
padding: 9px 10px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-session span {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
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:hover,
|
||||||
.agent-session.active {
|
.agent-session.active {
|
||||||
|
background: color-mix(in srgb, var(--senlin-primary) 8%, var(--senlin-panel));
|
||||||
color: var(--senlin-primary);
|
color: var(--senlin-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.agent-session-empty {
|
||||||
|
margin-top: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
.agent-chat-panel.queue-section .arco-card-body {
|
.agent-chat-panel.queue-section .arco-card-body {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: calc(100vh - 170px);
|
height: 100%;
|
||||||
|
min-height: 620px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: minmax(0, 1fr) auto;
|
grid-template-rows: minmax(0, 1fr) auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-chat-main {
|
.agent-chat-main {
|
||||||
display: grid;
|
min-height: 0;
|
||||||
place-items: center;
|
overflow-y: auto;
|
||||||
padding: 80px 32px 190px;
|
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;
|
margin: 0;
|
||||||
color: var(--senlin-text);
|
color: var(--senlin-text);
|
||||||
font-size: 24px;
|
font-size: 14px;
|
||||||
font-weight: 800;
|
line-height: 1.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-composer {
|
.agent-composer {
|
||||||
width: min(860px, calc(100% - 80px));
|
width: min(780px, calc(100% - 64px));
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
margin: 0 0 28px;
|
margin: 0 0 24px;
|
||||||
border: 1px solid var(--senlin-border);
|
border: 1px solid var(--senlin-border);
|
||||||
border-radius: 24px;
|
border-radius: 20px;
|
||||||
background: var(--senlin-panel);
|
background: var(--senlin-panel);
|
||||||
padding: 18px 16px 14px;
|
padding: 14px 14px 10px;
|
||||||
box-shadow: 0 16px 40px rgba(29, 33, 41, 0.08);
|
box-shadow: 0 12px 32px rgba(29, 33, 41, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-composer .arco-textarea-wrapper {
|
.agent-composer .arco-textarea-wrapper {
|
||||||
@@ -1659,7 +1760,8 @@
|
|||||||
|
|
||||||
.agent-composer textarea {
|
.agent-composer textarea {
|
||||||
color: var(--senlin-text);
|
color: var(--senlin-text);
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-composer textarea::placeholder {
|
.agent-composer textarea::placeholder {
|
||||||
@@ -1674,7 +1776,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.agent-model-chip.arco-btn {
|
.agent-model-chip.arco-btn {
|
||||||
height: 36px;
|
height: 32px;
|
||||||
border-color: color-mix(in srgb, var(--senlin-primary) 32%, var(--senlin-border));
|
border-color: color-mix(in srgb, var(--senlin-primary) 32%, var(--senlin-border));
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--senlin-soft-blue);
|
background: var(--senlin-soft-blue);
|
||||||
@@ -1683,9 +1785,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.agent-send-button.arco-btn {
|
.agent-send-button.arco-btn {
|
||||||
width: 38px;
|
width: 34px;
|
||||||
height: 38px;
|
height: 34px;
|
||||||
background: #aebeff;
|
}
|
||||||
|
|
||||||
|
.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 {
|
.theme-dark .agent-session-list.queue-section.arco-card {
|
||||||
@@ -1697,6 +1811,16 @@
|
|||||||
box-shadow: 0 16px 40px rgb(0 0 0 / 22%);
|
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,
|
.reply-box,
|
||||||
.chat-input {
|
.chat-input {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1728,6 +1852,34 @@
|
|||||||
padding: 12px;
|
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 {
|
.task-plan-card-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ function channelLabel(channel: WorkspaceChannelDTO) {
|
|||||||
case 'tasks':
|
case 'tasks':
|
||||||
return '工作计划'
|
return '工作计划'
|
||||||
case 'ai_sessions':
|
case 'ai_sessions':
|
||||||
return 'AI 对话'
|
return 'AI 会话'
|
||||||
case 'notes_sources':
|
case 'notes_sources':
|
||||||
return '笔记资料'
|
return '笔记资料'
|
||||||
case 'cron':
|
case 'cron':
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Alert, Button, Card, Empty, Input, Space, Spin, Tag, Typography } from '@arco-design/web-react'
|
import { Alert, Button, Card, Empty, Input, Spin, Typography } from '@arco-design/web-react'
|
||||||
import { IconPlusCircle, IconRobot } from '@arco-design/web-react/icon'
|
import { IconArrowUp, IconAttachment, IconPlus, IconRobot } from '@arco-design/web-react/icon'
|
||||||
import type { AISessionDTO, CreateAISessionInput } from '../../api/ai'
|
import type { AISessionDTO, CreateAISessionInput } from '../../api/ai'
|
||||||
import type { ProjectWorkspace } from './project-types'
|
import type { ProjectWorkspace } from './project-types'
|
||||||
|
|
||||||
const { Title, Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
export function ProjectAi({
|
export function ProjectAi({
|
||||||
activeWorkspace,
|
activeWorkspace,
|
||||||
@@ -18,8 +18,8 @@ export function ProjectAi({
|
|||||||
onCreateSession: (projectId: string, input: CreateAISessionInput, signal?: AbortSignal) => Promise<AISessionDTO>
|
onCreateSession: (projectId: string, input: CreateAISessionInput, signal?: AbortSignal) => Promise<AISessionDTO>
|
||||||
}) {
|
}) {
|
||||||
const [sessions, setSessions] = useState<AISessionDTO[]>([])
|
const [sessions, setSessions] = useState<AISessionDTO[]>([])
|
||||||
const [title, setTitle] = useState('')
|
const [selectedSessionID, setSelectedSessionID] = useState<string | null>(null)
|
||||||
const [context, setContext] = useState('')
|
const [prompt, setPrompt] = useState('')
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [creating, setCreating] = useState(false)
|
const [creating, setCreating] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
@@ -38,8 +38,8 @@ export function ProjectAi({
|
|||||||
listControllerRef.current = controller
|
listControllerRef.current = controller
|
||||||
createControllerRef.current = null
|
createControllerRef.current = null
|
||||||
setSessions([])
|
setSessions([])
|
||||||
setTitle('')
|
setSelectedSessionID(null)
|
||||||
setContext('')
|
setPrompt('')
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setCreating(false)
|
setCreating(false)
|
||||||
setError('')
|
setError('')
|
||||||
@@ -68,10 +68,15 @@ export function ProjectAi({
|
|||||||
}
|
}
|
||||||
}, [onListSessions, projectId])
|
}, [onListSessions, projectId])
|
||||||
|
|
||||||
|
const selectedSession = useMemo(
|
||||||
|
() => sessions.find((session) => session.id === selectedSessionID) ?? null,
|
||||||
|
[selectedSessionID, sessions],
|
||||||
|
)
|
||||||
|
|
||||||
const createSession = async () => {
|
const createSession = async () => {
|
||||||
const trimmedTitle = title.trim()
|
const message = prompt.trim()
|
||||||
if (!trimmedTitle) {
|
if (!message) {
|
||||||
setError('请输入 AI 会话标题')
|
setError('请输入会话内容')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const generation = ++generationRef.current
|
const generation = ++generationRef.current
|
||||||
@@ -87,13 +92,13 @@ export function ProjectAi({
|
|||||||
setError('')
|
setError('')
|
||||||
try {
|
try {
|
||||||
const created = await onCreateSession(projectId, {
|
const created = await onCreateSession(projectId, {
|
||||||
title: trimmedTitle,
|
title: sessionTitle(message),
|
||||||
context: context.trim(),
|
context: message,
|
||||||
}, controller.signal)
|
}, controller.signal)
|
||||||
if (!isCurrent()) return
|
if (!isCurrent()) return
|
||||||
setSessions((current) => [created, ...current.filter((session) => session.id !== created.id)])
|
setSessions((current) => [created, ...current.filter((session) => session.id !== created.id)])
|
||||||
setTitle('')
|
setSelectedSessionID(created.id)
|
||||||
setContext('')
|
setPrompt('')
|
||||||
onSelectItem(created.title)
|
onSelectItem(created.title)
|
||||||
} catch (requestError) {
|
} catch (requestError) {
|
||||||
if (!isCurrent()) return
|
if (!isCurrent()) return
|
||||||
@@ -106,67 +111,112 @@ export function ProjectAi({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
function selectSession(session: AISessionDTO) {
|
||||||
<div className="project-channel-page project-ai-page overview-page">
|
setSelectedSessionID(session.id)
|
||||||
<div className="overview-head">
|
setError('')
|
||||||
<div>
|
onSelectItem(session.title)
|
||||||
<Title heading={4}>AI 对话</Title>
|
}
|
||||||
<Text type="secondary">创建项目内普通会话;不会自动生成任务、笔记或资料。</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="project-channel-page project-ai-page">
|
||||||
{error ? <Alert className="agent-request-error" type="error" content={error} closable onClose={() => setError('')} /> : null}
|
{error ? <Alert className="agent-request-error" type="error" content={error} closable onClose={() => setError('')} /> : null}
|
||||||
|
|
||||||
<div className="agent-chat-shell">
|
<div className="agent-chat-shell">
|
||||||
<Card className="agent-session-list queue-section" bordered title="会话列表">
|
<Card className="agent-session-list queue-section" bordered>
|
||||||
|
<Button
|
||||||
|
className="agent-new-chat"
|
||||||
|
icon={<IconPlus />}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedSessionID(null)
|
||||||
|
setPrompt('')
|
||||||
|
setError('')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
新建会话
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Spin loading={loading} style={{ width: '100%' }}>
|
<Spin loading={loading} style={{ width: '100%' }}>
|
||||||
{sessions.length ? (
|
{sessions.length ? (
|
||||||
<div className="agent-session-groups">
|
<div className="agent-session-groups">
|
||||||
|
<section className="agent-session-group">
|
||||||
|
<Text type="secondary">最近会话</Text>
|
||||||
{sessions.map((session) => (
|
{sessions.map((session) => (
|
||||||
<button className="agent-session" key={session.id} onClick={() => onSelectItem(session.title)}>
|
<button
|
||||||
|
className={selectedSessionID === session.id ? 'agent-session active' : 'agent-session'}
|
||||||
|
key={session.id}
|
||||||
|
title={session.title}
|
||||||
|
onClick={() => selectSession(session)}
|
||||||
|
>
|
||||||
|
<IconRobot />
|
||||||
<span>{session.title}</span>
|
<span>{session.title}</span>
|
||||||
<Tag size="small" color={session.status === 'ready' ? 'arcoblue' : 'gray'}>
|
|
||||||
{sessionStatusLabel(session.status)}
|
|
||||||
</Tag>
|
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
) : loading ? null : <Empty description="暂无 AI 会话" />}
|
) : loading ? null : <Empty className="agent-session-empty" description="暂无会话" />}
|
||||||
</Spin>
|
</Spin>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="agent-chat-panel queue-section" bordered>
|
<Card className="agent-chat-panel queue-section" bordered>
|
||||||
<Space direction="vertical" size={16} className="action-form">
|
<div className="agent-chat-main">
|
||||||
|
{selectedSession ? (
|
||||||
|
<div className="agent-thread">
|
||||||
|
<div className="agent-user-message">{selectedSession.context || selectedSession.title}</div>
|
||||||
|
<div className="agent-assistant-message">
|
||||||
|
<span className="agent-assistant-avatar"><IconRobot /></span>
|
||||||
<div>
|
<div>
|
||||||
<Title heading={5}>创建会话</Title>
|
<Text className="agent-assistant-name">森林AI</Text>
|
||||||
<Text type="secondary">会话状态为“待开始”只表示入口已建立,不代表 AI 已完成处理。</Text>
|
<p>会话已创建。你可以继续补充问题或项目背景。</p>
|
||||||
</div>
|
</div>
|
||||||
<label>
|
</div>
|
||||||
<Text>会话标题</Text>
|
</div>
|
||||||
<Input value={title} onChange={setTitle} placeholder="例如:报价分析" maxLength={120} />
|
) : (
|
||||||
</label>
|
<div className="agent-empty-state">
|
||||||
<label>
|
<span className="agent-empty-icon"><IconRobot /></span>
|
||||||
<Text>项目上下文</Text>
|
<Text>在下方输入内容,开始新的项目会话</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="agent-composer">
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
value={context}
|
value={prompt}
|
||||||
onChange={setContext}
|
placeholder={`给森林AI发送消息,结合“${activeWorkspace.project.name}”项目上下文`}
|
||||||
placeholder="描述本次会话要参考的项目背景"
|
autoSize={{ minRows: 2, maxRows: 7 }}
|
||||||
autoSize={{ minRows: 5, maxRows: 10 }}
|
onChange={setPrompt}
|
||||||
|
onPressEnter={(event) => {
|
||||||
|
if (!event.shiftKey) {
|
||||||
|
event.preventDefault()
|
||||||
|
void createSession()
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</label>
|
<div className="agent-composer-footer">
|
||||||
<Button type="primary" icon={<IconPlusCircle />} loading={creating} onClick={() => void createSession()}>
|
<Button className="agent-model-chip" icon={<IconRobot />}>项目上下文</Button>
|
||||||
创建会话
|
<div className="agent-composer-actions">
|
||||||
</Button>
|
<Button aria-label="添加附件" type="text" shape="circle" icon={<IconAttachment />} />
|
||||||
<Text type="secondary"><IconRobot /> AI 输出如需转为正式对象,必须另行确认。</Text>
|
<Button
|
||||||
</Space>
|
aria-label="发送消息"
|
||||||
|
className="agent-send-button"
|
||||||
|
type="primary"
|
||||||
|
shape="circle"
|
||||||
|
icon={<IconArrowUp />}
|
||||||
|
loading={creating}
|
||||||
|
disabled={!prompt.trim()}
|
||||||
|
onClick={() => void createSession()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Text className="agent-composer-hint" type="secondary">AI 生成内容仅作为建议,转为正式对象前仍需确认</Text>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function sessionStatusLabel(status: string) {
|
function sessionTitle(message: string) {
|
||||||
if (status === 'ready') return '待开始'
|
const firstLine = message.split(/\r?\n/, 1)[0].trim()
|
||||||
if (status === 'failed') return '创建失败'
|
const characters = Array.from(firstLine)
|
||||||
return status || '未知状态'
|
return characters.length > 36 ? `${characters.slice(0, 36).join('')}…` : firstLine
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export function ProjectNewChannel({ activeWorkspace }: { activeWorkspace: Projec
|
|||||||
<Tag color="arcoblue">外部资料库</Tag>
|
<Tag color="arcoblue">外部资料库</Tag>
|
||||||
<Tag color="green">项目流程页</Tag>
|
<Tag color="green">项目流程页</Tag>
|
||||||
<Tag color="orange">RSS/探索结果</Tag>
|
<Tag color="orange">RSS/探索结果</Tag>
|
||||||
<Tag color="purple">AI 对话上下文</Tag>
|
<Tag color="purple">AI 会话上下文</Tag>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 { 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 { IconArrowLeft, IconCalendar, IconCheckCircle, IconCheckCircleFill, IconClockCircle, IconPlus } from '@arco-design/web-react/icon'
|
||||||
import { ProjectTaskEditModal, type ProjectTaskUpdate } from './project-task-edit-modal'
|
import { ProjectTaskEditModal, type ProjectTaskUpdate } from './project-task-edit-modal'
|
||||||
@@ -26,12 +26,20 @@ export function ProjectTasks({
|
|||||||
}) {
|
}) {
|
||||||
const { tasks, project, tags } = activeWorkspace
|
const { tasks, project, tags } = activeWorkspace
|
||||||
const activeTask = tasks.find((task) => task.id === activeTaskID)
|
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 projectTags = tags.filter((tag) => tag !== 'all' && tag !== '全部')
|
||||||
|
const [selectedTag, setSelectedTag] = useState('全部')
|
||||||
const [tagModalOpen, setTagModalOpen] = useState(false)
|
const [tagModalOpen, setTagModalOpen] = useState(false)
|
||||||
const [tagName, setTagName] = useState('')
|
const [tagName, setTagName] = useState('')
|
||||||
const [editingTask, setEditingTask] = useState<TaskItem | null>(null)
|
const [editingTask, setEditingTask] = useState<TaskItem | null>(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) {
|
if (activeTask) {
|
||||||
return <TaskDetail activeWorkspace={activeWorkspace} task={activeTask} onBack={onCloseTask} />
|
return <TaskDetail activeWorkspace={activeWorkspace} task={activeTask} onBack={onCloseTask} />
|
||||||
@@ -60,10 +68,25 @@ export function ProjectTasks({
|
|||||||
<Title heading={6}>标签</Title>
|
<Title heading={6}>标签</Title>
|
||||||
<Button className="tag-create-button" size="mini" type="text" icon={<IconPlus />} onClick={() => setTagModalOpen(true)}>新建</Button>
|
<Button className="tag-create-button" size="mini" type="text" icon={<IconPlus />} onClick={() => setTagModalOpen(true)}>新建</Button>
|
||||||
</div>
|
</div>
|
||||||
<Space wrap>
|
<Space className="task-tag-filters" wrap>
|
||||||
<Tag color="arcoblue">全部</Tag>
|
<button
|
||||||
|
className={`task-tag-filter${selectedTag === '全部' ? ' active' : ''}`}
|
||||||
|
type="button"
|
||||||
|
aria-pressed={selectedTag === '全部'}
|
||||||
|
onClick={() => setSelectedTag('全部')}
|
||||||
|
>
|
||||||
|
<Tag color={selectedTag === '全部' ? 'arcoblue' : 'gray'}>全部</Tag>
|
||||||
|
</button>
|
||||||
{projectTags.map((tag) => (
|
{projectTags.map((tag) => (
|
||||||
<Tag key={tag} color="gray">{tag}</Tag>
|
<button
|
||||||
|
className={`task-tag-filter${selectedTag === tag ? ' active' : ''}`}
|
||||||
|
type="button"
|
||||||
|
aria-pressed={selectedTag === tag}
|
||||||
|
key={tag}
|
||||||
|
onClick={() => setSelectedTag(tag)}
|
||||||
|
>
|
||||||
|
<Tag color={selectedTag === tag ? 'arcoblue' : 'gray'}>{tag}</Tag>
|
||||||
|
</button>
|
||||||
))}
|
))}
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -84,7 +107,9 @@ export function ProjectTasks({
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
{pendingTasks.length === 0 && <Text type="secondary">当前没有未完成计划</Text>}
|
{pendingTasks.length === 0 && (
|
||||||
|
<Text type="secondary">{selectedTag === '全部' ? '当前没有未完成计划' : `“${selectedTag}”标签下没有未完成计划`}</Text>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -104,7 +129,9 @@ export function ProjectTasks({
|
|||||||
<span><IconClockCircle /> {task.duration || '未知'}</span>
|
<span><IconClockCircle /> {task.duration || '未知'}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
{completedTasks.length === 0 && <Text type="secondary">当前没有完成计划</Text>}
|
{completedTasks.length === 0 && (
|
||||||
|
<Text type="secondary">{selectedTag === '全部' ? '当前没有完成计划' : `“${selectedTag}”标签下没有完成计划`}</Text>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function WorkspacePage({
|
|||||||
const workspaceMetrics = [
|
const workspaceMetrics = [
|
||||||
{ title: '项目总数', value: projects.length, delta: `${urgentProjects} 个高优先级`, icon: <IconDashboard />, color: 'arcoblue' },
|
{ title: '项目总数', value: projects.length, delta: `${urgentProjects} 个高优先级`, icon: <IconDashboard />, color: 'arcoblue' },
|
||||||
{ title: '未完成计划', value: unfinishedTasks.length, delta: '跨项目聚合', icon: <IconCheckCircleFill />, color: 'green' },
|
{ title: '未完成计划', value: unfinishedTasks.length, delta: '跨项目聚合', icon: <IconCheckCircleFill />, color: 'green' },
|
||||||
{ title: 'AI 对话', value: aiSessions.length, delta: '项目内上下文', icon: <IconRobot />, color: 'purple' },
|
{ title: 'AI 会话', value: aiSessions.length, delta: '项目内上下文', icon: <IconRobot />, color: 'purple' },
|
||||||
{ title: '知识资料', value: notes.length, delta: '笔记与资料合计', icon: <IconFile />, color: 'cyan' },
|
{ title: '知识资料', value: notes.length, delta: '笔记与资料合计', icon: <IconFile />, color: 'cyan' },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ export function WorkspacePage({
|
|||||||
<div className="overview-head">
|
<div className="overview-head">
|
||||||
<div>
|
<div>
|
||||||
<Title heading={4}>工作台</Title>
|
<Title heading={4}>工作台</Title>
|
||||||
<Text type="secondary">所有项目的统一工作台,汇总项目任务、AI 对话和知识资产。</Text>
|
<Text type="secondary">所有项目的统一工作台,汇总项目任务、AI 会话和知识资产。</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user