import { useState, useEffect, useCallback } from 'react' import { Database, RefreshCw, BookOpen, } from 'lucide-react' import { useTranslation } from 'react-i18next' import { ipc } from '../../services/ipc-client' import { Button } from '../ui/Button' import { EmptyState } from '../ui/EmptyState' import { useProjectStore } from '../../stores/project-store' import { globalEventBus } from '../../shared/event-bus' import { loadKBData, type KBDocument } from '../../services/knowledge-service' /** 知识库管理面板(侧栏)— 纯只读展示 + 搜索,数据由定稿自动驱动 */ export default function KnowledgePanel() { const { t } = useTranslation('panels') const [documents, setDocuments] = useState([]) const [stats, setStats] = useState({ documentCount: 0, totalChunks: 0 }) const [currentPage, setCurrentPage] = useState(1) const pageSize = 20 const [titleMap, setTitleMap] = useState>({}) /** 加载文档列表 + 统计(通过 Service 层) */ const loadData = useCallback(async () => { try { const { documents: docs, stats: s } = await loadKBData() setDocuments(docs) setStats(s) } catch { /* 忽略 */ } }, []) useEffect(() => { let mounted = true Promise.resolve().then(() => { if (mounted) loadData() }) return () => { mounted = false } }, [loadData]) // 通过 EventBus 监听资源刷新和定稿完成事件 useEffect(() => { const unsub1 = globalEventBus.on('REFRESH_RESOURCE', () => { loadData() }) const unsub2 = globalEventBus.on('FINALIZE_COMPLETE', () => { loadData() }) return () => { unsub1(); unsub2() } }, [loadData]) useEffect(() => { let cancelled = false const loadTitles = async () => { if (documents.length === 0) return const missing = documents.filter(d => d.filePath && !titleMap[d.id]) if (missing.length === 0) return const newTitles: Record = {} await Promise.all( missing.map(async (doc) => { let title = doc.fileName const rawName = doc.fileName.replace(/\.[^.]+$/, '') const chMatch = rawName.match(/^(?:chapter_(\d+)|第(\d+)章)\s*(.*)$/) if (chMatch) { const num = chMatch[1] ? parseInt(chMatch[1], 10) : parseInt(chMatch[2], 10) const rest = (chMatch[3] || '').trim() title = rest ? t('knowledge.chapterTitle', { chapter: num, title: rest }) : t('knowledge.chapterOnly', { chapter: num }) } try { const res = await ipc.invoke('fs:read-file', doc.filePath) if (res.success && res.content) { const firstLine = res.content.split('\n').find((l: string) => l.trim()) if (firstLine) { title = firstLine.replace(/^#+\s*/, '').trim() || title } } } catch { /* 忽略 */ } newTitles[doc.id] = title }) ) if (!cancelled) setTitleMap(prev => ({ ...prev, ...newTitles })) } loadTitles() return () => { cancelled = true } }, [documents]) // eslint-disable-line react-hooks/exhaustive-deps -- titleMap 不需要作为依赖:内部通过 prev => 合并即可获取最新值 const currentProject = useProjectStore(s => s.currentProject) if (!currentProject) { return ( } message={t('knowledge.openProjectFirst')} className="pb-[15vh]" opacity={0.4} /> ) } return (
{/* 标题栏 */}
{t('knowledge.title')} {t('knowledge.docsChunks', { docs: stats.documentCount, chunks: stats.totalChunks })}
{/* 已入库章节列表 */}
{t('knowledge.indexedChapters')}
{documents.length === 0 ? (
{t('knowledge.noManuscript')} {t('knowledge.importDocumentAuto')}
) : (
{documents .sort((a, b) => new Date(b.importedAt).getTime() - new Date(a.importedAt).getTime()) .slice((currentPage - 1) * pageSize, currentPage * pageSize) .map((doc) => (
{titleMap[doc.id] || doc.fileName}
{t('knowledge.chunks', { count: doc.chunkCount })} {new Date(doc.importedAt).toLocaleDateString('zh-CN')}
))} {Math.ceil(documents.length / pageSize) > 1 && (
{currentPage} / {Math.ceil(documents.length / pageSize)}
)}
)}
) }