feat: align controlled AI sessions and MVP controls
This commit is contained in:
@@ -1,80 +1,134 @@
|
||||
import { Button, Card, Input, Space, Typography } from '@arco-design/web-react'
|
||||
import {
|
||||
IconArrowUp,
|
||||
IconAttachment,
|
||||
IconPlusCircle,
|
||||
IconRobot,
|
||||
} from '@arco-design/web-react/icon'
|
||||
import type { AISession, ProjectWorkspace } from './project-types'
|
||||
import { useEffect, 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 type { AISessionDTO, CreateAISessionInput } from '../../api/ai'
|
||||
import type { ProjectWorkspace } from './project-types'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
|
||||
export function ProjectAi({ activeWorkspace, onSelectItem }: { activeWorkspace: ProjectWorkspace; onSelectItem: (title: string) => void }) {
|
||||
const { aiSessions } = activeWorkspace
|
||||
const groupedSessions = groupSessions(aiSessions)
|
||||
export function ProjectAi({
|
||||
activeWorkspace,
|
||||
onSelectItem,
|
||||
onListSessions,
|
||||
onCreateSession,
|
||||
}: {
|
||||
activeWorkspace: ProjectWorkspace
|
||||
onSelectItem: (title: string) => void
|
||||
onListSessions: (projectId: string) => Promise<AISessionDTO[]>
|
||||
onCreateSession: (projectId: string, input: CreateAISessionInput) => Promise<AISessionDTO>
|
||||
}) {
|
||||
const [sessions, setSessions] = useState<AISessionDTO[]>([])
|
||||
const [title, setTitle] = useState('')
|
||||
const [context, setContext] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const projectId = activeWorkspace.project.id
|
||||
|
||||
useEffect(() => {
|
||||
let current = true
|
||||
setLoading(true)
|
||||
setError('')
|
||||
void onListSessions(projectId)
|
||||
.then((items) => {
|
||||
if (current) setSessions(items)
|
||||
})
|
||||
.catch((requestError: unknown) => {
|
||||
if (current) setError(requestError instanceof Error ? requestError.message : 'AI 会话加载失败,请稍后重试')
|
||||
})
|
||||
.finally(() => {
|
||||
if (current) setLoading(false)
|
||||
})
|
||||
return () => {
|
||||
current = false
|
||||
}
|
||||
}, [onListSessions, projectId])
|
||||
|
||||
const createSession = async () => {
|
||||
const trimmedTitle = title.trim()
|
||||
if (!trimmedTitle) {
|
||||
setError('请输入 AI 会话标题')
|
||||
return
|
||||
}
|
||||
setCreating(true)
|
||||
setError('')
|
||||
try {
|
||||
const created = await onCreateSession(projectId, {
|
||||
title: trimmedTitle,
|
||||
context: context.trim(),
|
||||
})
|
||||
setSessions((current) => [created, ...current.filter((session) => session.id !== created.id)])
|
||||
setTitle('')
|
||||
setContext('')
|
||||
onSelectItem(created.title)
|
||||
} catch (requestError) {
|
||||
setError(requestError instanceof Error ? requestError.message : 'AI 会话创建失败,请稍后重试')
|
||||
} finally {
|
||||
setCreating(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="project-channel-page project-ai-page overview-page">
|
||||
<div className="agent-chat-shell">
|
||||
<Card className="agent-session-list queue-section" bordered>
|
||||
<Button className="agent-new-chat" icon={<IconPlusCircle />}>
|
||||
开启新对话
|
||||
</Button>
|
||||
<div className="overview-head">
|
||||
<div>
|
||||
<Title heading={4}>AI 助手</Title>
|
||||
<Text type="secondary">创建项目内普通会话;不会自动生成任务、笔记或资料。</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="agent-session-groups">
|
||||
{groupedSessions.length ? (
|
||||
groupedSessions.map((group) => (
|
||||
<section className="agent-session-group" key={group.label}>
|
||||
<Text type="secondary">{group.label}</Text>
|
||||
{group.items.map((session, index) => (
|
||||
<button
|
||||
className={index === 0 && group.label === groupedSessions[0]?.label ? 'agent-session active' : 'agent-session'}
|
||||
key={session.id}
|
||||
onClick={() => onSelectItem(session.title)}
|
||||
>
|
||||
{session.title}
|
||||
</button>
|
||||
))}
|
||||
</section>
|
||||
))
|
||||
) : (
|
||||
<Text type="secondary">暂无对话</Text>
|
||||
)}
|
||||
</div>
|
||||
{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="会话列表">
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
) : loading ? null : <Empty description="暂无 AI 会话" />}
|
||||
</Spin>
|
||||
</Card>
|
||||
|
||||
<Card className="agent-chat-panel queue-section" bordered>
|
||||
<div className="agent-chat-main">
|
||||
<Title heading={2}>选择专家,开始对话</Title>
|
||||
</div>
|
||||
<div className="agent-composer">
|
||||
<Input.TextArea placeholder="给 DeepSeek 发送消息" autoSize={{ minRows: 3, maxRows: 6 }} />
|
||||
<div className="agent-composer-footer">
|
||||
<Button className="agent-model-chip" icon={<IconRobot />}>
|
||||
DeepSeek V4.0 Flash
|
||||
</Button>
|
||||
<Space>
|
||||
<Button type="text" icon={<IconAttachment />} />
|
||||
<Button className="agent-send-button" type="primary" shape="circle" icon={<IconArrowUp />} />
|
||||
</Space>
|
||||
<Space direction="vertical" size={16} className="action-form">
|
||||
<div>
|
||||
<Title heading={5}>创建会话</Title>
|
||||
<Text type="secondary">会话状态为“待开始”只表示入口已建立,不代表 AI 已完成处理。</Text>
|
||||
</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>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function groupSessions(sessions: AISession[]) {
|
||||
const labels = ['今天', '昨天', '7 天内']
|
||||
return labels.map((label) => ({
|
||||
label,
|
||||
items: sessions.filter((session, index) => session.time.includes(label) || (!labels.some((item) => session.time.includes(item)) && labels.indexOf(label) === fallbackGroupIndex(index))),
|
||||
})).filter((group) => group.items.length > 0)
|
||||
}
|
||||
|
||||
function fallbackGroupIndex(index: number) {
|
||||
if (index === 0) return 0
|
||||
if (index === 1) return 1
|
||||
return 2
|
||||
function sessionStatusLabel(status: string) {
|
||||
if (status === 'ready') return '待开始'
|
||||
if (status === 'failed') return '创建失败'
|
||||
return status || '未知状态'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user