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, IconSearch } from '@arco-design/web-react/icon' import type { AIExpertDTO, AISessionDTO, CreateAISessionInput } from '../../api/ai' import type { ProjectWorkspace } from './project-types' const { Text } = Typography export function ProjectAi({ activeWorkspace, onSelectItem, onListSessions, onListExperts, onCreateSession, }: { activeWorkspace: ProjectWorkspace onSelectItem: (title: string) => void onListSessions: (projectId: string, signal?: AbortSignal) => Promise onListExperts: (signal?: AbortSignal) => Promise onCreateSession: (projectId: string, input: CreateAISessionInput, signal?: AbortSignal) => Promise }) { const [sessions, setSessions] = useState([]) const [experts, setExperts] = useState([]) const [selectedSessionID, setSelectedSessionID] = useState(null) const [selectedExpertID, setSelectedExpertID] = useState(null) const [expertQuery, setExpertQuery] = useState('') const [expertCategory, setExpertCategory] = useState('all') const [prompt, setPrompt] = useState('') const [loading, setLoading] = useState(true) const [loadingExperts, setLoadingExperts] = useState(true) const [creating, setCreating] = useState(false) const [error, setError] = useState('') const projectId = activeWorkspace.project.id const projectRef = useRef(projectId) const generationRef = useRef(0) const listControllerRef = useRef(null) const createControllerRef = useRef(null) projectRef.current = projectId useEffect(() => { const generation = ++generationRef.current const controller = new AbortController() listControllerRef.current?.abort() createControllerRef.current?.abort() listControllerRef.current = controller createControllerRef.current = null setSessions([]) setExperts([]) setSelectedSessionID(null) setSelectedExpertID(null) setExpertQuery('') setExpertCategory('all') setPrompt(readPromptDraft(projectId)) setLoading(true) setLoadingExperts(true) setCreating(false) setError('') const isCurrent = () => projectRef.current === projectId && generationRef.current === generation && !controller.signal.aborted void onListSessions(projectId, controller.signal) .then((items) => { if (isCurrent()) setSessions(items) }) .catch((requestError: unknown) => { if (isCurrent()) setError(requestError instanceof Error ? requestError.message : 'AI 会话加载失败,请稍后重试') }) .finally(() => { if (isCurrent()) setLoading(false) }) void onListExperts(controller.signal) .then((items) => { if (isCurrent()) setExperts(items) }) .catch((requestError: unknown) => { if (isCurrent()) setError(requestError instanceof Error ? requestError.message : '专家库加载失败,请稍后重试') }) .finally(() => { if (isCurrent()) { setLoadingExperts(false) listControllerRef.current = null } }) return () => { generationRef.current += 1 controller.abort() if (listControllerRef.current === controller) listControllerRef.current = null createControllerRef.current?.abort() createControllerRef.current = null } }, [onListExperts, onListSessions, projectId]) useEffect(() => { try { if (prompt) localStorage.setItem(promptDraftKey(projectId), prompt) else localStorage.removeItem(promptDraftKey(projectId)) } catch { // Draft persistence is best effort; an unavailable browser store must not block chat. } }, [projectId, prompt]) const selectedSession = useMemo( () => sessions.find((session) => session.id === selectedSessionID) ?? null, [selectedSessionID, sessions], ) const selectedExpert = useMemo( () => experts.find((expert) => expert.id === selectedExpertID) ?? selectedSession?.expert ?? null, [experts, selectedExpertID, selectedSession], ) const expertCategories = useMemo(() => { const categories = new Map() experts.forEach((expert) => categories.set(expert.category, expert.categoryName)) return [...categories.entries()].map(([id, name]) => ({ id, name })) }, [experts]) const visibleExperts = useMemo(() => { const keyword = expertQuery.trim().toLocaleLowerCase() return experts.filter((expert) => { if (expertCategory !== 'all' && expert.category !== expertCategory) return false if (!keyword) return true return `${expert.name} ${expert.description} ${expert.categoryName}`.toLocaleLowerCase().includes(keyword) }) }, [expertCategory, expertQuery, experts]) const createSession = async () => { const message = prompt.trim() if (!selectedExpert) { setError('请先选择一位专家') return } if (!message) { setError('请输入会话内容') return } 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) setCreating(true) setError('') try { const created = await onCreateSession(projectId, { title: sessionTitle(message), context: message, expertId: selectedExpert.id, }, controller.signal) if (!isCurrent()) return setSessions((current) => [created, ...current.filter((session) => session.id !== created.id)]) setSelectedSessionID(created.id) setSelectedExpertID(created.expert?.id ?? selectedExpert.id) setPrompt('') onSelectItem(created.title) } catch (requestError) { if (!isCurrent()) return setError(requestError instanceof Error ? requestError.message : 'AI 会话创建失败,请稍后重试') } finally { if (isCurrent()) { setCreating(false) createControllerRef.current = null } } } function selectSession(session: AISessionDTO) { setSelectedSessionID(session.id) setSelectedExpertID(session.expert?.id ?? null) setError('') onSelectItem(session.title) } function startNewSession() { setSelectedSessionID(null) setSelectedExpertID(null) setExpertQuery('') setExpertCategory('all') setPrompt('') setError('') } return (
{error ? setError('')} /> : null}
{sessions.length ? (
最近会话 {sessions.map((session) => ( ))}
) : loading ? null : }
{selectedSession ? (
{selectedSession.context || selectedSession.title}
{selectedExpert?.emoji || }
{selectedExpert?.name || '森林AI'}

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

) : ( )}
{ if (!event.shiftKey) { event.preventDefault() void createSession() } }} />
AI 生成内容仅作为建议,转为正式对象前仍需确认
) } function ExpertPicker({ experts, categories, selectedExpertID, query, category, loading, onQueryChange, onCategoryChange, onSelect, }: { experts: AIExpertDTO[] categories: Array<{ id: string; name: string }> selectedExpertID: string | null query: string category: string loading: boolean onQueryChange: (value: string) => void onCategoryChange: (value: string) => void onSelect: (value: string) => void }) { return (
选择专家进行会话 选择合适的专业角色,让会话从明确的方法和交付目标开始
} value={query} onChange={onQueryChange} placeholder="搜索专家名称、简介或分类" allowClear />
{categories.map((item) => ( ))}
{experts.length ? (
{experts.map((expert) => ( ))}
) : loading ? null : }
) } 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 } function promptDraftKey(projectId: string) { return `senlin:ai:prompt:${projectId}` } function readPromptDraft(projectId: string) { try { return localStorage.getItem(promptDraftKey(projectId)) ?? '' } catch { return '' } }