@@ -1,13 +1,14 @@
import { useEffect , useMemo , useState } from 'react'
import { useEffect , useMemo , useRef , useState } from 'react'
import { Alert , Avatar , Button , Dropdown , Empty , Form , Input , Menu , Modal , Select , Spin , Switch , Tag , Typography } from '@arco-design/web-react'
import {
IconApps , IconBook , IconCheckCircle , IconCheckCircleFill , IconClockCircle , IconCompass , IconDown , IconFile ,
IconFolder , IconLeft , IconList , IconMoon , IconPlus , IconRefresh , IconRight , IconRobot ,
IconSun , IconUser ,
IconSun , IconUpload , IconUser ,
} from '@arco-design/web-react/icon'
import {
ApiError , createProject , fetchProjectWorkspace , fetchProjects , getCurrentUser ,
ApiError , 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 ,
} from './api'
@@ -114,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 } / > : < 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 ( ) } } / > : < 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 > ) }
@@ -142,10 +143,10 @@ export function App() {
)
}
function Content ( { view , title , workspaces , tasks , onEditTask , onLoadAI , experts } : { view : View ; title : string ; workspaces : Workspace [ ] ; tasks : TaskWithProject [ ] ; onEditTask : ( task : TaskWithProject ) = > void ; onLoadAI : ( ) = > void ; experts : Expert [ ] } ) {
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 > } ) {
let panel : React.ReactNode
if ( view === 'tasks' ) panel = < TaskBoard title = "工作计划" subtitle = "" tasks = { tasks } onEditTask = { onEditTask } / >
else if ( view === 'documents' ) panel = < DocumentPanel title = "" workspaces= { workspaces } / >
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 panel = < CronPanel title = "" workspaces = { workspaces } / >
return < section className = "project-content" > < header className = "project-page-header" > < Title heading = { 3 } > { title } < / Title > < / header > { panel } < / section >
@@ -206,9 +207,31 @@ function ExplorePanel({ session, projectName }: { session: ApiSession; projectNa
return < section className = "content-page explore-page" > < div className = "content-heading" > < div > < Title heading = { 4 } > 探 索 < / Title > < Text type = "secondary" > 集 中 阅 读 数 据 源 内 容 , 筛 选 后 沉 淀 到 项 目 资 料 。 < / Text > < / div > < Button type = "text" shape = "circle" aria-label = "刷新探索内容" loading = { loading } icon = { < IconRefresh / > } onClick = { ( ) = > { void refreshSources ( ) ; void refreshItems ( ) } } / > < / div > { error ? < Alert type = "error" content = { error } / > : null } < div className = "explore-source-strip" > < button className = { sourceId === 'all' ? 'active' : '' } onClick = { ( ) = > setSourceId ( 'all' ) } > < IconCompass / > < span > 全 部 < / span > < small > { allCount } < / small > < / button > { sources . map ( ( source ) = > < button className = { sourceId === source . id ? 'active' : '' } key = { source . id } onClick = { ( ) = > setSourceId ( source . id ) } > { source . iconUrl ? < img src = { source . iconUrl } alt = "" / > : < IconBook / > } < span > { source . name } < / span > < small > { source . itemCount } < / small > < / button > ) } < / div > < div className = "explore-reader" > { items . length ? < > < div className = "explore-item-list" > { items . map ( ( item ) = > < button className = { item . id === selected ? . id ? 'active' : '' } key = { item . id } onClick = { ( ) = > setSelectedId ( item . id ) } > < span > < b > { item . title } < / b > < small > { item . summary || '暂无摘要' } < / small > < / span > < i > { item . starred ? '★' : formatDate ( item . publishedAt || item . createdAt ) } < / i > < / button > ) } < / div > < article className = "explore-detail" > < div className = "explore-detail-meta" > < Tag color = "arcoblue" > { selectedSource ? . name || '全部' } < / Tag > < small > { formatDate ( selected ? . publishedAt || selected ? . createdAt || '' ) } < / small > < / div > < Title heading = { 5 } > { selected ? . title } < / Title > { selected ? . imageUrl ? < img src = { selected . imageUrl } alt = "" / > : null } < p > { selected ? . content || selected ? . summary || '暂无正文' } < / p > < div className = "explore-detail-actions" > < Button type = "text" icon = { < IconBook / > } disabled = { ! projectName } > 沉 淀 到 { projectName || '项目' } < / Button > { selected ? . url ? < Button type = "text" href = { selected . url } target = "_blank" > 打 开 原 文 < / Button > : null } < / div > < / article > < / > : < Empty description = "暂无探索内容,请先在 Web 工作台添加或同步数据源" / > } < / div > < / section >
}
function DocumentPanel ( { title , workspaces } : { title : string ; workspaces : Workspace [ ] } ) {
function DocumentPanel ( { workspaces , onUpload , onCreateMarkdown } : { workspaces : Workspace [ ] ; onUpload : ( file : File ) = > Promise < void > ; onCreateMarkdown : ( input : { name : string ; markdown : string } ) = > Promise < void > } ) {
const documents = workspaces . flatMap ( ( workspace ) = > workspace . documents . map ( ( item ) = > ( { . . . item , projectName : workspace.project.name } ) ) )
return < Panel title = "笔记资料" subtitle = { title } > { 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 = "暂无笔记或资料" / > } < / Pane l>
const inputRef = useRef < HTMLInputElement > ( nul l)
const [ creating , setCreating ] = useState ( false )
const [ name , setName ] = useState ( '未命名笔记.md' )
const [ markdown , setMarkdown ] = useState ( '' )
const [ saving , setSaving ] = useState ( false )
const [ uploading , setUploading ] = useState ( false )
const [ error , setError ] = useState ( '' )
async function saveMarkdown() {
if ( ! name . trim ( ) ) return
setSaving ( true ) ; setError ( '' )
try { await onCreateMarkdown ( { name : name.trim ( ) , markdown } ) ; setCreating ( false ) ; setName ( '未命名笔记.md' ) ; setMarkdown ( '' ) } catch ( requestError ) { setError ( errorMessage ( requestError ) ) } finally { setSaving ( false ) }
}
async function uploadFiles ( files : FileList | null ) {
if ( ! files ? . length ) return
setUploading ( true ) ; setError ( '' )
try { for ( const file of Array . from ( files ) ) await onUpload ( file ) } catch ( requestError ) { setError ( errorMessage ( requestError ) ) } finally { setUploading ( false ) ; if ( inputRef . current ) inputRef . current . value = '' }
}
if ( creating ) return < section className = "content-page markdown-editor-page" > < div className = "content-heading" > < div > < Text type = "secondary" > 项 目 资 料 < / Text > < Title heading = { 4 } > 新 建 Markdown < / Title > < / div > < Button type = "text" onClick = { ( ) = > setCreating ( false ) } > 返 回 资 料 < / Button > < / div > { error ? < Alert type = "error" content = { error } / > : null } < Form layout = "vertical" > < Form.Item label = "文件名称" required > < Input value = { name } onChange = { setName } placeholder = "例如:会议记录.md" / > < / Form.Item > < Form.Item label = "Markdown 内容" > < Input.TextArea value = { markdown } onChange = { setMarkdown } placeholder = { '# 标题\n\n开始记录…' } autoSize = { { minRows : 14 , maxRows : 24 } } / > < / Form.Item > < div className = "markdown-editor-actions" > < Button onClick = { ( ) = > setCreating ( false ) } > 取 消 < / Button > < Button type = "primary" loading = { saving } onClick = { ( ) = > { void saveMarkdown ( ) } } > 创 建 文 件 < / Button > < / div > < / Form > < / section >
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 } ) {