feat(desktop): add AI session creation and history
This commit is contained in:
@@ -6,7 +6,7 @@ import {
|
||||
IconSun, IconUpload, IconUser,
|
||||
} from '@arco-design/web-react/icon'
|
||||
import {
|
||||
ApiError, createMarkdownDocument, createProject, fetchProjectWorkspace, fetchProjects, getCurrentUser,
|
||||
ApiError, createAISession, createMarkdownDocument, createProject, fetchProjectWorkspace, fetchProjects, getCurrentUser,
|
||||
listAIExperts, listDatasetItems, listDatasetSources, login, setApiBaseUrl, updateCurrentUser, updateTask,
|
||||
uploadDocument,
|
||||
type ApiSession, type CurrentUser, type DatasetItem, type DatasetSource, type Expert, type Project, type Workspace, type WorkspaceTask,
|
||||
@@ -115,7 +115,7 @@ export function App() {
|
||||
</section>
|
||||
|
||||
{error ? <Alert className="app-alert" type="error" content={error} closable onClose={() => setError('')} /> : null}
|
||||
<main className="app-main"><Spin loading={loading} block>{workspaceMode === 'explore' ? <ExplorePanel session={session} projectName={activeProject?.name ?? ''} /> : !projectId ? <AllProjectsHome projects={projects} workspaces={visibleWorkspaces} tasks={tasks} onEditTask={setTaskEditor} onCreateProject={() => setProjectModalOpen(true)} /> : visibleWorkspaces.length ? <Content view={view} title={title} workspaces={visibleWorkspaces} tasks={tasks} onEditTask={setTaskEditor} onLoadAI={loadAI} experts={experts} onUploadDocument={async (file) => { await uploadDocument(session, projectId, file); await loadWorkspaces() }} onCreateMarkdown={async (input) => { await createMarkdownDocument(session, projectId, input); await loadWorkspaces() }} /> : <Empty description="项目加载失败,请刷新后重试" />}</Spin></main>
|
||||
<main className="app-main"><Spin loading={loading} block>{workspaceMode === 'explore' ? <ExplorePanel session={session} projectName={activeProject?.name ?? ''} /> : !projectId ? <AllProjectsHome projects={projects} workspaces={visibleWorkspaces} tasks={tasks} onEditTask={setTaskEditor} onCreateProject={() => setProjectModalOpen(true)} /> : visibleWorkspaces.length ? <Content view={view} title={title} workspaces={visibleWorkspaces} tasks={tasks} onEditTask={setTaskEditor} onLoadAI={loadAI} experts={experts} onUploadDocument={async (file) => { await uploadDocument(session, projectId, file); await loadWorkspaces() }} onCreateMarkdown={async (input) => { await createMarkdownDocument(session, projectId, input); await loadWorkspaces() }} onCreateAISession={async (input) => { await createAISession(session, projectId, input); await loadWorkspaces() }} /> : <Empty description="项目加载失败,请刷新后重试" />}</Spin></main>
|
||||
|
||||
{projectId && workspaceMode === 'workbench' ? <aside className="channel-rail" aria-label="项目子菜单">
|
||||
{navItems.map((item) => <button key={item.id} title={item.label} className={view === item.id ? 'active' : ''} onClick={() => { setView(item.id); if (item.id === 'ai') void loadAI() }}>{item.icon}<span>{item.label}</span></button>)}
|
||||
@@ -143,11 +143,11 @@ export function App() {
|
||||
)
|
||||
}
|
||||
|
||||
function Content({ view, title, workspaces, tasks, onEditTask, onLoadAI, experts, onUploadDocument, onCreateMarkdown }: { view: View; title: string; workspaces: Workspace[]; tasks: TaskWithProject[]; onEditTask: (task: TaskWithProject) => void; onLoadAI: () => void; experts: Expert[]; onUploadDocument: (file: File) => Promise<void>; onCreateMarkdown: (input: { name: string; markdown: string }) => Promise<void> }) {
|
||||
function Content({ view, title, workspaces, tasks, onEditTask, onLoadAI, experts, onUploadDocument, onCreateMarkdown, onCreateAISession }: { view: View; title: string; workspaces: Workspace[]; tasks: TaskWithProject[]; onEditTask: (task: TaskWithProject) => void; onLoadAI: () => void; experts: Expert[]; onUploadDocument: (file: File) => Promise<void>; onCreateMarkdown: (input: { name: string; markdown: string }) => Promise<void>; onCreateAISession: (input: { title: string; context: string; expertId?: string }) => Promise<void> }) {
|
||||
let panel: React.ReactNode
|
||||
if (view === 'tasks') panel = <TaskBoard title="工作计划" subtitle="" tasks={tasks} onEditTask={onEditTask} />
|
||||
else if (view === 'documents') panel = <DocumentPanel workspaces={workspaces} onUpload={onUploadDocument} onCreateMarkdown={onCreateMarkdown} />
|
||||
else if (view === 'ai') panel = <AIPanel title="" workspaces={workspaces} experts={experts} onLoad={onLoadAI} />
|
||||
else if (view === 'ai') panel = <AIPanel workspaces={workspaces} experts={experts} onLoad={onLoadAI} onCreateSession={onCreateAISession} />
|
||||
else panel = <CronPanel title="" workspaces={workspaces} />
|
||||
return <section className="project-content"><header className="project-page-header"><Title heading={3}>{title}</Title></header>{panel}</section>
|
||||
}
|
||||
@@ -234,10 +234,23 @@ function DocumentPanel({ workspaces, onUpload, onCreateMarkdown }: { workspaces:
|
||||
return <section className="content-page documents-page"><div className="content-heading"><div><Title heading={4}>笔记资料</Title></div><div className="document-actions"><input ref={inputRef} className="file-picker" type="file" multiple onChange={(event) => { void uploadFiles(event.target.files) }} /><Button icon={<IconUpload />} loading={uploading} onClick={() => inputRef.current?.click()}>上传文件</Button><Button type="primary" icon={<IconPlus />} onClick={() => setCreating(true)}>新建 Markdown</Button></div></div>{error ? <Alert type="error" content={error} /> : null}<div className="detail-list">{documents.map((item) => <article className="detail-row" key={item.id}><IconFile /><div><strong>{item.name}</strong><p>{item.extension || item.mimeType || item.kind} · 更新于 {formatDate(item.updatedAt)}</p></div><Tag>{item.projectName}</Tag></article>)}{!documents.length && <Empty description="暂无笔记或资料" />}</div></section>
|
||||
}
|
||||
|
||||
function AIPanel({ title, workspaces, experts, onLoad }: { title: string; workspaces: Workspace[]; experts: Expert[]; onLoad: () => void }) {
|
||||
function AIPanel({ workspaces, experts, onLoad, onCreateSession }: { workspaces: Workspace[]; experts: Expert[]; onLoad: () => void; onCreateSession: (input: { title: string; context: string; expertId?: string }) => Promise<void> }) {
|
||||
useEffect(() => { onLoad() }, [onLoad])
|
||||
const [page, setPage] = useState<'new' | 'history'>('new')
|
||||
const [title, setTitle] = useState('')
|
||||
const [context, setContext] = useState('')
|
||||
const [expertId, setExpertId] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const sessions = workspaces.flatMap((workspace) => workspace.aiSessions.map((item) => ({ ...item, projectName: workspace.project.name })))
|
||||
return <Panel title="AI 会话" subtitle={title}><div className="expert-chips">{experts.slice(0, 6).map((expert) => <span key={expert.id}><i style={{ background: expert.color }}>{expert.emoji}</i>{expert.name}</span>)}</div>{sessions.map((item) => <article className="detail-row" key={item.id}><IconRobot /><div><strong>{item.title}</strong><p>{item.summary || 'AI 会话'}</p></div><Tag>{item.projectName}</Tag></article>)}{!sessions.length && <Empty description="暂无 AI 会话" />}</Panel>
|
||||
useEffect(() => { setExpertId((current) => experts.some((expert) => expert.id === current) ? current : experts[0]?.id ?? '') }, [experts])
|
||||
|
||||
async function createSession() {
|
||||
setSaving(true); setError('')
|
||||
try { await onCreateSession({ title: title.trim() || '新会话', context: context.trim(), expertId: expertId || undefined }); setTitle(''); setContext(''); setPage('history') } catch (requestError) { setError(errorMessage(requestError)) } finally { setSaving(false) }
|
||||
}
|
||||
|
||||
return <section className="content-page ai-session-page"><div className="content-heading"><div><Title heading={4}>{page === 'new' ? '新建会话' : '历史会话'}</Title></div><div className="ai-page-switch"><Button type={page === 'new' ? 'primary' : 'text'} icon={<IconPlus />} onClick={() => setPage('new')}>新建会话</Button><Button type={page === 'history' ? 'primary' : 'text'} icon={<IconClockCircle />} onClick={() => setPage('history')}>历史会话</Button></div></div>{error ? <Alert type="error" content={error} /> : null}{page === 'new' ? <Form className="ai-session-form" layout="vertical"><Form.Item label="会话标题"><Input value={title} onChange={setTitle} placeholder="例如:梳理本周项目计划" /></Form.Item><Form.Item label="选择专家"><div className="expert-chips">{experts.map((expert) => <button className={expert.id === expertId ? 'active' : ''} key={expert.id} type="button" onClick={() => setExpertId(expert.id)}><i style={{ background: expert.color }}>{expert.emoji}</i>{expert.name}</button>)}{!experts.length && <Text type="secondary">暂无可用专家</Text>}</div></Form.Item><Form.Item label="上下文或问题"><Input.TextArea value={context} onChange={setContext} placeholder="描述你希望 AI 协助完成的内容…" autoSize={{ minRows: 8, maxRows: 16 }} /></Form.Item><div className="ai-session-actions"><Button type="primary" loading={saving} icon={<IconRobot />} onClick={() => { void createSession() }}>创建会话</Button></div></Form> : <div className="ai-history-page detail-list">{sessions.map((item) => <article className="detail-row" key={item.id}><IconRobot /><div><strong>{item.title}</strong><p>{item.summary || 'AI 会话'} · {formatDate(item.updatedAt)}</p></div><Tag>{item.projectName}</Tag></article>)}{!sessions.length && <Empty description="暂无历史会话,先新建一个会话吧" />}</div>}</section>
|
||||
}
|
||||
|
||||
function CronPanel({ title, workspaces }: { title: string; workspaces: Workspace[] }) {
|
||||
|
||||
Reference in New Issue
Block a user