feat(web): refine AI sessions and task filtering
This commit is contained in:
@@ -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 对话</Title>
|
||||
<Text type="secondary">创建项目内普通会话;不会自动生成任务、笔记或资料。</Text>
|
||||
</div>
|
||||
</div>
|
||||
function selectSession(session: AISessionDTO) {
|
||||
setSelectedSessionID(session.id)
|
||||
setError('')
|
||||
onSelectItem(session.title)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="project-channel-page project-ai-page">
|
||||
{error ? <Alert className="agent-request-error" type="error" content={error} closable onClose={() => setError('')} /> : null}
|
||||
|
||||
<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%' }}>
|
||||
{sessions.length ? (
|
||||
<div className="agent-session-groups">
|
||||
{sessions.map((session) => (
|
||||
<button className="agent-session" key={session.id} onClick={() => onSelectItem(session.title)}>
|
||||
<span>{session.title}</span>
|
||||
<Tag size="small" color={session.status === 'ready' ? 'arcoblue' : 'gray'}>
|
||||
{sessionStatusLabel(session.status)}
|
||||
</Tag>
|
||||
</button>
|
||||
))}
|
||||
<section className="agent-session-group">
|
||||
<Text type="secondary">最近会话</Text>
|
||||
{sessions.map((session) => (
|
||||
<button
|
||||
className={selectedSessionID === session.id ? 'agent-session active' : 'agent-session'}
|
||||
key={session.id}
|
||||
title={session.title}
|
||||
onClick={() => selectSession(session)}
|
||||
>
|
||||
<IconRobot />
|
||||
<span>{session.title}</span>
|
||||
</button>
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
) : loading ? null : <Empty description="暂无 AI 会话" />}
|
||||
) : loading ? null : <Empty className="agent-session-empty" description="暂无会话" />}
|
||||
</Spin>
|
||||
</Card>
|
||||
|
||||
<Card className="agent-chat-panel queue-section" bordered>
|
||||
<Space direction="vertical" size={16} className="action-form">
|
||||
<div>
|
||||
<Title heading={5}>创建会话</Title>
|
||||
<Text type="secondary">会话状态为“待开始”只表示入口已建立,不代表 AI 已完成处理。</Text>
|
||||
<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>
|
||||
<Text className="agent-assistant-name">森林AI</Text>
|
||||
<p>会话已创建。你可以继续补充问题或项目背景。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="agent-empty-state">
|
||||
<span className="agent-empty-icon"><IconRobot /></span>
|
||||
<Text>在下方输入内容,开始新的项目会话</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="agent-composer">
|
||||
<Input.TextArea
|
||||
value={prompt}
|
||||
placeholder={`给森林AI发送消息,结合“${activeWorkspace.project.name}”项目上下文`}
|
||||
autoSize={{ minRows: 2, maxRows: 7 }}
|
||||
onChange={setPrompt}
|
||||
onPressEnter={(event) => {
|
||||
if (!event.shiftKey) {
|
||||
event.preventDefault()
|
||||
void createSession()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="agent-composer-footer">
|
||||
<Button className="agent-model-chip" icon={<IconRobot />}>项目上下文</Button>
|
||||
<div className="agent-composer-actions">
|
||||
<Button aria-label="添加附件" type="text" shape="circle" icon={<IconAttachment />} />
|
||||
<Button
|
||||
aria-label="发送消息"
|
||||
className="agent-send-button"
|
||||
type="primary"
|
||||
shape="circle"
|
||||
icon={<IconArrowUp />}
|
||||
loading={creating}
|
||||
disabled={!prompt.trim()}
|
||||
onClick={() => void createSession()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<label>
|
||||
<Text>会话标题</Text>
|
||||
<Input value={title} onChange={setTitle} placeholder="例如:报价分析" maxLength={120} />
|
||||
</label>
|
||||
<label>
|
||||
<Text>项目上下文</Text>
|
||||
<Input.TextArea
|
||||
value={context}
|
||||
onChange={setContext}
|
||||
placeholder="描述本次会话要参考的项目背景"
|
||||
autoSize={{ minRows: 5, maxRows: 10 }}
|
||||
/>
|
||||
</label>
|
||||
<Button type="primary" icon={<IconPlusCircle />} loading={creating} onClick={() => void createSession()}>
|
||||
创建会话
|
||||
</Button>
|
||||
<Text type="secondary"><IconRobot /> AI 输出如需转为正式对象,必须另行确认。</Text>
|
||||
</Space>
|
||||
<Text className="agent-composer-hint" type="secondary">AI 生成内容仅作为建议,转为正式对象前仍需确认</Text>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export function ProjectNewChannel({ activeWorkspace }: { activeWorkspace: Projec
|
||||
<Tag color="arcoblue">外部资料库</Tag>
|
||||
<Tag color="green">项目流程页</Tag>
|
||||
<Tag color="orange">RSS/探索结果</Tag>
|
||||
<Tag color="purple">AI 对话上下文</Tag>
|
||||
<Tag color="purple">AI 会话上下文</Tag>
|
||||
</Space>
|
||||
</Card>
|
||||
</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 { 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<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) {
|
||||
return <TaskDetail activeWorkspace={activeWorkspace} task={activeTask} onBack={onCloseTask} />
|
||||
@@ -60,10 +68,25 @@ export function ProjectTasks({
|
||||
<Title heading={6}>标签</Title>
|
||||
<Button className="tag-create-button" size="mini" type="text" icon={<IconPlus />} onClick={() => setTagModalOpen(true)}>新建</Button>
|
||||
</div>
|
||||
<Space wrap>
|
||||
<Tag color="arcoblue">全部</Tag>
|
||||
<Space className="task-tag-filters" wrap>
|
||||
<button
|
||||
className={`task-tag-filter${selectedTag === '全部' ? ' active' : ''}`}
|
||||
type="button"
|
||||
aria-pressed={selectedTag === '全部'}
|
||||
onClick={() => setSelectedTag('全部')}
|
||||
>
|
||||
<Tag color={selectedTag === '全部' ? 'arcoblue' : 'gray'}>全部</Tag>
|
||||
</button>
|
||||
{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>
|
||||
</Card>
|
||||
@@ -84,7 +107,9 @@ export function ProjectTasks({
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
{pendingTasks.length === 0 && <Text type="secondary">当前没有未完成计划</Text>}
|
||||
{pendingTasks.length === 0 && (
|
||||
<Text type="secondary">{selectedTag === '全部' ? '当前没有未完成计划' : `“${selectedTag}”标签下没有未完成计划`}</Text>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -104,7 +129,9 @@ export function ProjectTasks({
|
||||
<span><IconClockCircle /> {task.duration || '未知'}</span>
|
||||
</button>
|
||||
))}
|
||||
{completedTasks.length === 0 && <Text type="secondary">当前没有完成计划</Text>}
|
||||
{completedTasks.length === 0 && (
|
||||
<Text type="secondary">{selectedTag === '全部' ? '当前没有完成计划' : `“${selectedTag}”标签下没有完成计划`}</Text>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
|
||||
@@ -80,7 +80,7 @@ export function WorkspacePage({
|
||||
const workspaceMetrics = [
|
||||
{ title: '项目总数', value: projects.length, delta: `${urgentProjects} 个高优先级`, icon: <IconDashboard />, color: 'arcoblue' },
|
||||
{ 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' },
|
||||
]
|
||||
|
||||
@@ -89,7 +89,7 @@ export function WorkspacePage({
|
||||
<div className="overview-head">
|
||||
<div>
|
||||
<Title heading={4}>工作台</Title>
|
||||
<Text type="secondary">所有项目的统一工作台,汇总项目任务、AI 对话和知识资产。</Text>
|
||||
<Text type="secondary">所有项目的统一工作台,汇总项目任务、AI 会话和知识资产。</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user