Files
agent/apps/web_v1/src/pages/workspace-explore.tsx

224 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useMemo, useState } from 'react'
import type { ReactNode } from 'react'
import { Button, Card, Empty, Grid, Space, Typography } from '@arco-design/web-react'
import {
IconBook,
IconCheckCircle,
IconCompass,
IconDelete,
IconEdit,
IconFile,
IconLink,
IconRefresh,
IconStar,
IconStorage,
} from '@arco-design/web-react/icon'
import type { InboxItem, Project, ProjectWorkspace } from './projects/project-types'
const { Row, Col } = Grid
const { Title, Text, Paragraph } = Typography
type DataSourceID = 'all' | 'manual' | 'requirements' | 'architecture'
type ExploreArticle = InboxItem & {
project: Project
sourceID: DataSourceID
}
type DataSourceCard = {
id: DataSourceID
name: string
count: number
icon: ReactNode
color: string
}
const SOURCE_META: Record<DataSourceID, { name: string; icon: ReactNode; color: string }> = {
all: { name: '全部', icon: <IconStorage />, color: 'blue' },
manual: { name: '手动收集', icon: <IconCompass />, color: 'green' },
requirements: { name: '需求文档', icon: <IconFile />, color: 'orange' },
architecture: { name: '架构讨论', icon: <IconBook />, color: 'purple' },
}
export function WorkspaceExplorePage({
workspaces,
onSelectItem,
}: {
workspaces: ProjectWorkspace[]
onSelectItem: (title: string) => void
}) {
const articles = useMemo(
() =>
workspaces.flatMap((workspace) =>
workspace.inbox.map((item) => ({
...item,
project: workspace.project,
sourceID: detectSource(item),
})),
),
[workspaces],
)
const [activeSourceID, setActiveSourceID] = useState<DataSourceID>('all')
const filteredArticles = useMemo(
() => (activeSourceID === 'all' ? articles : articles.filter((article) => article.sourceID === activeSourceID)),
[activeSourceID, articles],
)
const sources = useMemo(() => dataSources(articles), [articles])
const [activeArticleID, setActiveArticleID] = useState<string | null>(filteredArticles[0]?.id ?? null)
useEffect(() => {
if (filteredArticles.length === 0) {
setActiveArticleID(null)
return
}
if (!activeArticleID || !filteredArticles.some((article) => article.id === activeArticleID)) {
setActiveArticleID(filteredArticles[0].id)
}
}, [activeArticleID, filteredArticles])
const selected = filteredArticles.find((article) => article.id === activeArticleID) ?? filteredArticles[0]
const activeSource = SOURCE_META[activeSourceID]
return (
<div className="workspace-explore-page overview-page">
<div className="overview-head">
<div>
<Title heading={4}></Title>
<Text type="secondary"></Text>
</div>
<Space>
<Button icon={<IconRefresh />}></Button>
<Button type="primary" icon={<IconLink />}>
</Button>
</Space>
</div>
<Row gutter={10} className="explore-source-row">
{sources.map((source) => (
<Col span={6} key={source.id}>
<Card
className={activeSourceID === source.id ? 'compact-card explore-source-card active' : 'compact-card explore-source-card'}
bordered
onClick={() => setActiveSourceID(source.id)}
>
<span className={`explore-source-icon ${source.color}`}>{source.icon}</span>
<span className="explore-source-copy">
<Text className="explore-source-name">{source.name}</Text>
<Text type="secondary">{source.count} </Text>
</span>
{source.id !== 'all' ? (
<span className="explore-source-actions" onClick={(event) => event.stopPropagation()}>
<Button type="text" size="mini" icon={<IconEdit />} />
<Button type="text" size="mini" status="danger" icon={<IconDelete />} />
</span>
) : null}
</Card>
</Col>
))}
</Row>
{selected ? (
<section className="explore-reader-layout">
<Card className="explore-article-list queue-section" bordered>
<div className="explore-list-header">
<Title heading={5}>
<Space size={6}>
<span className={`explore-source-icon mini ${activeSource.color}`}>{activeSource.icon}</span>
<span>{activeSource.name}</span>
</Space>
</Title>
<Button type="text" icon={<IconRefresh />} />
</div>
<div className="explore-list-body">
{filteredArticles.map((article) => (
<button
key={`${article.project.id}-${article.id}`}
className={article.id === selected.id ? 'explore-article-item active' : 'explore-article-item'}
onClick={() => {
setActiveArticleID(article.id)
onSelectItem(article.title)
}}
>
<span className={`explore-source-logo ${SOURCE_META[article.sourceID].color}`}>
{sourceInitial(SOURCE_META[article.sourceID].name)}
</span>
<span className="explore-article-copy">
<Text type="secondary">
{SOURCE_META[article.sourceID].name} · {article.time}
</Text>
<Text className="explore-article-title">{article.title}</Text>
<Text className="explore-article-summary" type="secondary" ellipsis={{ showTooltip: true }}>
{article.summary || '暂无正文'}
</Text>
</span>
</button>
))}
</div>
</Card>
<Card className="explore-article-detail queue-section" bordered>
<div className="explore-detail-toolbar">
<Title heading={5}>{selected.title}</Title>
<Space>
<Button type="text" icon={<IconCheckCircle />} />
<Button type="text" icon={<IconStar />} />
<Button type="text" icon={<IconBook />} />
</Space>
</div>
<article className="explore-article-body">
<Space className="explore-article-meta" wrap>
<span className={`explore-source-logo small ${SOURCE_META[selected.sourceID].color}`}>
{sourceInitial(SOURCE_META[selected.sourceID].name)}
</span>
<Text type="secondary">{SOURCE_META[selected.sourceID].name}</Text>
<Text type="secondary">{selected.project.name}</Text>
<Text type="secondary">{selected.time}</Text>
</Space>
<Paragraph className="explore-article-content">{selected.summary || '暂无正文'}</Paragraph>
<blockquote>
线
</blockquote>
</article>
</Card>
</section>
) : (
<Card className="queue-section" bordered>
<Empty description="暂无数据源文章" />
</Card>
)}
</div>
)
}
function dataSources(articles: ExploreArticle[]): DataSourceCard[] {
const counts = new Map<DataSourceID, number>([
['all', articles.length],
['manual', 0],
['requirements', 0],
['architecture', 0],
])
articles.forEach((article) => counts.set(article.sourceID, (counts.get(article.sourceID) ?? 0) + 1))
return (Object.keys(SOURCE_META) as DataSourceID[]).map((id) => ({
id,
...SOURCE_META[id],
count: counts.get(id) ?? 0,
}))
}
function detectSource(article: InboxItem): DataSourceID {
const text = `${article.title} ${article.meta} ${article.tag} ${article.summary}`
if (/架构|技术方案|系统设计|architecture/i.test(text)) {
return 'architecture'
}
if (/需求|PRD|产品文档|requirement/i.test(text)) {
return 'requirements'
}
return 'manual'
}
function sourceInitial(name: string) {
return name.trim().slice(0, 1) || '源'
}