@@ -6,10 +6,10 @@ import {
IconSun , IconUpload , IconUser ,
} from '@arco-design/web-react/icon'
import {
ApiError , createAISession , createMarkdownDocument , createProject , fetchProjectWorkspace , fetchProjects , getCurrentUser ,
listAIExperts , listDatasetItems , listDatasetSources , login , setApiBaseUrl , updateCurrentUser , updateTask ,
ApiError , createAISession , createMarkdownDocument , createProject , fetchDocumentBlob , fetchProjectWorkspace , fetchProjects , getCurrentUser , getDocument ,
listAIExperts , listDatasetItems , listDatasetSources , login , setApiBaseUrl , updateCurrentUser , updateDocument , updateTask ,
uploadDocument ,
type ApiSession , type CurrentUser , type DatasetItem , type DatasetSource , type Expert , type Project , type Workspace , type WorkspaceTask ,
type ApiSession , type CurrentUser , type DatasetItem , type DatasetSource , type DocumentDetail , type Expert , type Project , type Workspace , type WorkspaceDocument , type WorkspaceTask ,
} from './api'
const { Text , Title } = Typography
@@ -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 ( ) } } onCreateAISession = { async ( input ) = > { await createAISession ( 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 ( ) } } onLoadMarkdown = { ( documentId ) = > getDocument ( session , projectId , documentId ) } onUpdateMarkdown = { async ( input ) = > { await updateDocument ( session , projectId , input . id , input ) ; await loadWorkspaces ( ) } } onOpenExternalDocument = { async ( documentId ) = > { const preview = window . open ( '' , '_blank' ) ; const blob = await fetchDocumentBlob ( session , projectId , documentId ) ; const url = URL . createObjectURL ( blob ) ; if ( preview ) { preview . opener = null ; preview . location . href = url } else { const link = document . createElement ( 'a' ) ; link . href = url ; link . target = '_blank' ; link . rel = 'noopener noreferrer' ; link . click ( ) } window . setTimeout ( ( ) = > URL . revokeObjectURL ( url ) , 60 _000 ) } } 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,10 +143,10 @@ export function App() {
)
}
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 > } ) {
function Content ( { view , title , workspaces , tasks , onEditTask , onLoadAI , experts , onUploadDocument , onCreateMarkdown , onLoadMarkdown , onUpdateMarkdown , onOpenExternalDocument , 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 > ; onLoadMarkdown : ( documentId : string ) = > Promise < DocumentDetail > ; onUpdateMarkdown : ( input : { id : string ; name : string ; markdown : string ; revision : number } ) = > Promise < void > ; onOpenExternalDocument : ( documentId : 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 === 'documents' ) panel = < DocumentPanel workspaces = { workspaces } onUpload = { onUploadDocument } onCreateMarkdown = { onCreateMarkdown } onLoadMarkdown = { onLoadMarkdown } onUpdateMarkdown = { onUpdateMarkdown } onOpenExternalDocument = { onOpenExternalDocument } / >
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 >
@@ -207,10 +207,11 @@ 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 ( { workspaces , onUpload , onCreateMarkdown } : { workspaces : Workspace [ ] ; onUpload : ( file : File ) = > Promise < void > ; onCreateMarkdown : ( input : { name : string ; markdown : string } ) = > Promise < void > } ) {
function DocumentPanel ( { workspaces , onUpload , onCreateMarkdown , onLoadMarkdown , onUpdateMarkdown , onOpenExternalDocument } : { workspaces : Workspace [ ] ; onUpload : ( file : File ) = > Promise < void > ; onCreateMarkdown : ( input : { name : string ; markdown : string } ) = > Promise < void > ; onLoadMarkdown : ( documentId : string ) = > Promise < DocumentDetail > ; onUpdateMarkdown : ( input : { id : string ; name : string ; markdown : string ; revision : number } ) = > Promise < void > ; onOpenExternalDocument : ( documentId : string ) = > Promise < void > } ) {
const documents = workspaces . flatMap ( ( workspace ) = > workspace . documents . map ( ( item ) = > ( { . . . item , projectName : workspace.project.name } ) ) )
const inputRef = useRef < HTMLInputElement > ( null )
const [ creating , setCreating ] = useState ( false )
const [ editing , setEditing ] = useState < DocumentDetail | null > ( null )
const [ name , setName ] = useState ( '未命名笔记.md' )
const [ markdown , setMarkdown ] = useState ( '' )
const [ saving , setSaving ] = useState ( false )
@@ -220,7 +221,7 @@ function DocumentPanel({ workspaces, onUpload, onCreateMarkdown }: { workspaces:
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 ) }
try { if ( editing ) await onUpdateMarkdown ( { id : editing.id , name : name.trim ( ) , markdown , revision : editing.revision } ) ; else await onCreateMarkdown ( { name : name.trim ( ) , markdown } ) ; setCreating ( false ) ; setEditing ( null ) ; setName ( '未命名笔记.md' ) ; setMarkdown ( '' ) } catch ( requestError ) { setError ( errorMessage ( requestError ) ) } finally { setSaving ( false ) }
}
async function uploadFiles ( files : FileList | null ) {
@@ -229,9 +230,17 @@ function DocumentPanel({ workspaces, onUpload, onCreateMarkdown }: { workspaces:
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 >
async function openDocument ( document : WorkspaceDocument ) {
setError ( '' )
try {
if ( document . extension . toLowerCase ( ) === '.md' || document . mimeType . includes ( 'markdown' ) ) { const detail = await onLoadMarkdown ( document . id ) ; setEditing ( detail ) ; setName ( detail . name ) ; setMarkdown ( detail . markdown ? ? '' ) }
else await onOpenExternalDocument ( document . id )
} catch ( requestError ) { setError ( errorMessage ( requestError ) ) }
}
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 . fi les ) } } / > < 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 c lassName = "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 >
if ( creating || editing ) return < section className = "content-page markdown-editor -page" > < div className = "content-heading" > < div > < Text type = "secondary" > 项 目 资 料 < / Text > < Title heading = { 4 } > { editing ? '编辑 Markdown' : '新建 Markdown' } < / Tit le > < / div > < Button type = "text" onClick = { ( ) = > { setCreating ( false ) ; setEditing ( null ) ; setError ( '' ) } } > 返 回 资 料 < / 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 ) ; setEditing ( null ) } } >取 消 < / Button > < Button type = "primary" loading = { saving } onClick = { ( ) = > { void saveMarkdown ( ) } } > { editing ? '保存修改' : '创建文件' } < / 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 ) = > < button className = "detail-row document-row" key = { item . id } onClick = { ( ) = > { void openDocument ( item ) } } > < IconFile / > < div > < strong > { item . name } < / strong > < p > { item . extension || item . mimeType || item . kind } · 更 新 于 { formatDate ( item . updatedAt ) } < / p > < / div > < Tag > { item . extension . toLowerCase ( ) === '.md' ? '编辑' : '浏览器打开' } < / Tag > < / button > ) } { ! documents . length && < Empty description = "暂无笔记或资料" / > } < / div > < / section >
}
function AIPanel ( { workspaces , experts , onLoad , onCreateSession } : { workspaces : Workspace [ ] ; experts : Expert [ ] ; onLoad : ( ) = > void ; onCreateSession : ( input : { title : string ; context : string ; expertId? : string } ) = > Promise < void > } ) {