2026-07-21 19:54:25 +08:00
|
|
|
|
import { useEffect, useRef, useState } from 'react'
|
2026-07-21 19:19:36 +08:00
|
|
|
|
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'
|
2026-07-20 12:26:21 +08:00
|
|
|
|
|
2026-07-21 11:29:36 +08:00
|
|
|
|
const { Title, Text } = Typography
|
2026-07-20 12:26:21 +08:00
|
|
|
|
|
2026-07-21 19:19:36 +08:00
|
|
|
|
export function ProjectAi({
|
|
|
|
|
|
activeWorkspace,
|
|
|
|
|
|
onSelectItem,
|
|
|
|
|
|
onListSessions,
|
|
|
|
|
|
onCreateSession,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
activeWorkspace: ProjectWorkspace
|
|
|
|
|
|
onSelectItem: (title: string) => void
|
2026-07-21 19:54:25 +08:00
|
|
|
|
onListSessions: (projectId: string, signal?: AbortSignal) => Promise<AISessionDTO[]>
|
|
|
|
|
|
onCreateSession: (projectId: string, input: CreateAISessionInput, signal?: AbortSignal) => Promise<AISessionDTO>
|
2026-07-21 19:19:36 +08:00
|
|
|
|
}) {
|
|
|
|
|
|
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
|
2026-07-21 19:54:25 +08:00
|
|
|
|
const projectRef = useRef(projectId)
|
|
|
|
|
|
const generationRef = useRef(0)
|
|
|
|
|
|
const listControllerRef = useRef<AbortController | null>(null)
|
|
|
|
|
|
const createControllerRef = useRef<AbortController | null>(null)
|
|
|
|
|
|
projectRef.current = projectId
|
2026-07-21 19:19:36 +08:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-07-21 19:54:25 +08:00
|
|
|
|
const generation = ++generationRef.current
|
|
|
|
|
|
const controller = new AbortController()
|
|
|
|
|
|
listControllerRef.current?.abort()
|
|
|
|
|
|
createControllerRef.current?.abort()
|
|
|
|
|
|
listControllerRef.current = controller
|
|
|
|
|
|
createControllerRef.current = null
|
|
|
|
|
|
setSessions([])
|
|
|
|
|
|
setTitle('')
|
|
|
|
|
|
setContext('')
|
2026-07-21 19:19:36 +08:00
|
|
|
|
setLoading(true)
|
2026-07-21 19:54:25 +08:00
|
|
|
|
setCreating(false)
|
2026-07-21 19:19:36 +08:00
|
|
|
|
setError('')
|
2026-07-21 19:54:25 +08:00
|
|
|
|
const isCurrent = () => projectRef.current === projectId && generationRef.current === generation && !controller.signal.aborted
|
|
|
|
|
|
|
|
|
|
|
|
void onListSessions(projectId, controller.signal)
|
2026-07-21 19:19:36 +08:00
|
|
|
|
.then((items) => {
|
2026-07-21 19:54:25 +08:00
|
|
|
|
if (isCurrent()) setSessions(items)
|
2026-07-21 19:19:36 +08:00
|
|
|
|
})
|
|
|
|
|
|
.catch((requestError: unknown) => {
|
2026-07-21 19:54:25 +08:00
|
|
|
|
if (isCurrent()) setError(requestError instanceof Error ? requestError.message : 'AI 会话加载失败,请稍后重试')
|
2026-07-21 19:19:36 +08:00
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
2026-07-21 19:54:25 +08:00
|
|
|
|
if (isCurrent()) {
|
|
|
|
|
|
setLoading(false)
|
|
|
|
|
|
listControllerRef.current = null
|
|
|
|
|
|
}
|
2026-07-21 19:19:36 +08:00
|
|
|
|
})
|
2026-07-21 19:54:25 +08:00
|
|
|
|
|
2026-07-21 19:19:36 +08:00
|
|
|
|
return () => {
|
2026-07-21 19:54:25 +08:00
|
|
|
|
generationRef.current += 1
|
|
|
|
|
|
controller.abort()
|
|
|
|
|
|
if (listControllerRef.current === controller) listControllerRef.current = null
|
|
|
|
|
|
createControllerRef.current?.abort()
|
|
|
|
|
|
createControllerRef.current = null
|
2026-07-21 19:19:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}, [onListSessions, projectId])
|
|
|
|
|
|
|
|
|
|
|
|
const createSession = async () => {
|
|
|
|
|
|
const trimmedTitle = title.trim()
|
|
|
|
|
|
if (!trimmedTitle) {
|
|
|
|
|
|
setError('请输入 AI 会话标题')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-21 19:54:25 +08:00
|
|
|
|
const generation = ++generationRef.current
|
|
|
|
|
|
listControllerRef.current?.abort()
|
|
|
|
|
|
listControllerRef.current = null
|
|
|
|
|
|
createControllerRef.current?.abort()
|
|
|
|
|
|
const controller = new AbortController()
|
|
|
|
|
|
createControllerRef.current = controller
|
|
|
|
|
|
const isCurrent = () => projectRef.current === projectId && generationRef.current === generation && !controller.signal.aborted
|
|
|
|
|
|
|
|
|
|
|
|
setLoading(false)
|
2026-07-21 19:19:36 +08:00
|
|
|
|
setCreating(true)
|
|
|
|
|
|
setError('')
|
|
|
|
|
|
try {
|
|
|
|
|
|
const created = await onCreateSession(projectId, {
|
|
|
|
|
|
title: trimmedTitle,
|
|
|
|
|
|
context: context.trim(),
|
2026-07-21 19:54:25 +08:00
|
|
|
|
}, controller.signal)
|
|
|
|
|
|
if (!isCurrent()) return
|
2026-07-21 19:19:36 +08:00
|
|
|
|
setSessions((current) => [created, ...current.filter((session) => session.id !== created.id)])
|
|
|
|
|
|
setTitle('')
|
|
|
|
|
|
setContext('')
|
|
|
|
|
|
onSelectItem(created.title)
|
|
|
|
|
|
} catch (requestError) {
|
2026-07-21 19:54:25 +08:00
|
|
|
|
if (!isCurrent()) return
|
2026-07-21 19:19:36 +08:00
|
|
|
|
setError(requestError instanceof Error ? requestError.message : 'AI 会话创建失败,请稍后重试')
|
|
|
|
|
|
} finally {
|
2026-07-21 19:54:25 +08:00
|
|
|
|
if (isCurrent()) {
|
|
|
|
|
|
setCreating(false)
|
|
|
|
|
|
createControllerRef.current = null
|
|
|
|
|
|
}
|
2026-07-21 19:19:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-20 13:02:26 +08:00
|
|
|
|
|
2026-07-20 12:26:21 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="project-channel-page project-ai-page overview-page">
|
2026-07-21 19:19:36 +08:00
|
|
|
|
<div className="overview-head">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Title heading={4}>AI 助手</Title>
|
|
|
|
|
|
<Text type="secondary">创建项目内普通会话;不会自动生成任务、笔记或资料。</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-07-21 11:29:36 +08:00
|
|
|
|
|
2026-07-21 19:19:36 +08:00
|
|
|
|
{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>
|
2026-07-20 12:26:21 +08:00
|
|
|
|
</Card>
|
|
|
|
|
|
|
2026-07-21 11:29:36 +08:00
|
|
|
|
<Card className="agent-chat-panel queue-section" bordered>
|
2026-07-21 19:19:36 +08:00
|
|
|
|
<Space direction="vertical" size={16} className="action-form">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Title heading={5}>创建会话</Title>
|
|
|
|
|
|
<Text type="secondary">会话状态为“待开始”只表示入口已建立,不代表 AI 已完成处理。</Text>
|
2026-07-21 11:29:36 +08:00
|
|
|
|
</div>
|
2026-07-21 19:19:36 +08:00
|
|
|
|
<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>
|
2026-07-20 12:26:21 +08:00
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-07-21 11:29:36 +08:00
|
|
|
|
|
2026-07-21 19:19:36 +08:00
|
|
|
|
function sessionStatusLabel(status: string) {
|
|
|
|
|
|
if (status === 'ready') return '待开始'
|
|
|
|
|
|
if (status === 'failed') return '创建失败'
|
|
|
|
|
|
return status || '未知状态'
|
2026-07-21 11:29:36 +08:00
|
|
|
|
}
|