diff --git a/.env.example b/.env.example index 29c505f..afd2dd2 100644 --- a/.env.example +++ b/.env.example @@ -45,6 +45,17 @@ LLM_MODEL=gpt-4o-mini # 向量维度(需与所选模型输出维度一致,默认 1536) # EMBEDDING_DIM=1536 +# ───────────────────────────────────────────────────────────────────────────── +# Chunker 结构化切片参数(影响向量化粒度) +# ───────────────────────────────────────────────────────────────────────────── + +# 子章节最大字符数(超出则递归切分),默认 2000 +# CHUNKER_MAX_CHILD_CHARS=2000 +# 短章节合并阈值(字符数),默认 200 +# CHUNKER_SHORT_THRESHOLD=200 +# 上下文重叠字符数,默认 100 +# CHUNKER_OVERLAP_CHARS=100 + # ───────────────────────────────────────────────────────────────────────────── # 2. 七牛云对象存储(PDF 解析后的配图托管) # ───────────────────────────────────────────────────────────────────────────── diff --git a/CLAUDE.md b/CLAUDE.md index bfd3b91..bb83685 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -144,15 +144,14 @@ The embedding dimension is controlled by `EMBEDDING_DIM` env var (default 1536). ### Frontend (dashboard/) -React 19 + TypeScript + Vite + Tailwind CSS 4. Features are organized by domain: +React 19 + TypeScript + Vite + Tailwind CSS 4. Organized by technical layer (Type-Based): -- `features/search/` — Cross-source paper search panel -- `features/library/` — Local library management -- `features/reader/` — Bilingual reader with highlight annotations (KaTeX for math) -- `features/citation/` — Canvas-based force-directed citation graph -- `features/sync/` — Batch sync control panel -- `features/agent/` — Agent chat: ResearchAgentPanel (timeline view with thought/tool_call/answer/subagent), AgentMetricsPanel (tool stats), AskUserQuestionCard (interactive Q&A), AuditLogViewer -- `features/settings/` — System configuration +- `pages/` — Page-level panel view components (SearchPanel, LibraryPanel, ReaderPanel, CitationPanel, SyncPanel, ResearchAgentPanel, SettingsPanel) +- `components/` — Reusable and layout components (sub-folders: agent, reader, sync, layout, dialogs) +- `hooks/` — Global and feature-specific custom stateful React Hooks (e.g., useLibrary, useSearch, useNotes) +- `types/` — Global TypeScript type definitions (types/index.ts) +- `utils/` — Common utility helper functions +- `assets/` — Static assets and global stylesheet styles Dependencies: `react-markdown` + `rehype-katex` + `remark-math` for Markdown/LaTeX rendering, `framer-motion` for animations, `lucide-react` for icons. diff --git a/Cargo.lock b/Cargo.lock index e3c34bd..5416c62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,6 +160,7 @@ dependencies = [ "sha1 0.10.6", "sqlite-vec", "sqlx", + "subtle", "tempfile", "thiserror 1.0.69", "tokio", diff --git a/Cargo.toml b/Cargo.toml index e76fb44..011168a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ regex = "1.10" chrono = { version = "0.4", features = ["serde"] } sha1 = "0.10" hmac = "0.12" +subtle = "2" # 恒定时间密码比较,防时序侧信道攻击 base64 = "0.22" urlencoding = "2.1" url = "2.5" diff --git a/README.md b/README.md index e2515a1..d135792 100644 --- a/README.md +++ b/README.md @@ -128,8 +128,8 @@ AstroResearch/ │ │ ├── translation.rs # LLM 翻译 + Trie 天文词典 │ │ └── rag.rs # 向量检索增强生成 │ └── bin/ # 独立二进制 (cli, health_check, reparse) -├── dashboard/ # React 19 + Vite + TypeScript 前端 -│ └── src/features/ # search/ library/ reader/ citation/ agent/ sync/ settings/ +├── dashboard/ # React 19 + Vite + TypeScript 前端 (按技术类型划分) +│ └── src/ # pages/ components/ hooks/ types/ utils/ assets/ ├── skills/ # Agent Skills (Markdown 知识模块) ├── migrations/ # SQLite 迁移脚本 ├── library/ # 本地文献存储 (PDF/HTML/Markdown/Translation) diff --git a/dashboard/README.md b/dashboard/README.md index 7a9c9d1..91b3a36 100644 --- a/dashboard/README.md +++ b/dashboard/README.md @@ -22,13 +22,10 @@ npm run dev ## 2. 核心功能及文件结构 (Core Modules) -- `src/components/layout/`:侧边栏与基本布局组件。 -- `src/components/CitationGalaxyCanvas.tsx`:自研的 Canvas 力导向引文星系图渲染器。 -- `src/features/search/`:统一检索面板,支持跨源搜索与收藏。 -- `src/features/library/`:馆藏管理卡片,提供下载状态实时监测及重新下载操作。 -- `src/features/reader/`:左右对齐的双分栏阅读器,内置划词高亮笔记及 LLM 重新翻译触发。 -- `src/features/sync/`:批量同步面板,支持后台元数据大批量采集、过滤及文献资源批量下载/解析流水线任务管理。 -- `src/types.ts`:全局 TypeScript 静态类型定义。 +- `src/pages/`:页面级大组件视图(如统一检索、馆藏管理、对照阅读、引文星系、批量同步、智能科研、系统设置)。 +- `src/components/`:通用与专有 UI 组件(如力导向引文星系图、文献阅读器子组件、流水线任务终端等)。 +- `src/hooks/`:抽取出的全局共享及业务数据逻辑 Hooks(如 `useLibrary`、`useReaderState`、`useSyncState` 等)。 +- `src/types/`:全局 TypeScript 静态类型定义(`types/index.ts`)。 --- diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index 61d4bc9..6d32b1f 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -1,80 +1,28 @@ // dashboard/src/App.tsx import { useState, useEffect, useCallback } from 'react'; import axios from 'axios'; -import { Loader, Download, BookOpen, GitFork, RefreshCw, AlertTriangle, Lock } from 'lucide-react'; +import { Loader, BookOpen, GitFork, Lock } from 'lucide-react'; import { Sidebar } from './components/layout/Sidebar'; -import { SearchPanel, getDoctypeBadge } from './features/search/SearchPanel'; -import { LibraryPanel } from './features/library/LibraryPanel'; -import { ReaderPanel } from './features/reader/ReaderPanel'; -import { CitationPanel } from './features/citation/CitationPanel'; -import { SyncPanel } from './features/sync/SyncPanel'; -import { ResearchAgentPanel } from './features/agent/ResearchAgentPanel'; -import type { StandardPaper, CitationNetwork, NoteRecord } from './types'; +import { SearchPanel } from './pages/SearchPanel'; +import { LibraryPanel } from './pages/LibraryPanel'; +import { ReaderPanel } from './pages/ReaderPanel'; +import { CitationPanel } from './pages/CitationPanel'; +import { SyncPanel } from './pages/SyncPanel'; +import { ResearchAgentPanel } from './pages/ResearchAgentPanel'; +import type { StandardPaper, NoteRecord } from './types'; +import { GlobalDialog } from './components/dialogs/GlobalDialog'; +import { UncachedPaperModal } from './components/dialogs/UncachedPaperModal'; +import { PaperDetailModal } from './components/dialogs/PaperDetailModal'; + +// 引入拆分出的业务逻辑 Hooks +import { useAuth } from './hooks/useAuth'; +import { useLibrary } from './hooks/useLibrary'; +import { useSearch } from './hooks/useSearch'; +import { useNotes } from './hooks/useNotes'; +import { useCitations } from './hooks/useCitations'; export default function App() { - // 登录与鉴权相关状态 - const [isAuthenticated, setIsAuthenticated] = useState(null); - const [password, setPassword] = useState(''); - const [loginError, setLoginError] = useState(null); - const [loggingIn, setLoggingIn] = useState(false); - - const [activeTab, setActiveTab] = useState<'search' | 'library' | 'reader' | 'citation' | 'sync' | 'agent'>(() => { - const saved = localStorage.getItem('astro_active_tab'); - return (saved as any) || 'search'; - }); - - useEffect(() => { - localStorage.setItem('astro_active_tab', activeTab); - }, [activeTab]); - - // 全局启用 Axios 跨域 Cookie 传输凭证 - useEffect(() => { - axios.defaults.withCredentials = true; - }, []); - - // 初始化校验登录凭证状态(由浏览器自动带上 Cookie) - useEffect(() => { - axios.get('/api/auth/check') - .then(() => { - setIsAuthenticated(true); - }) - .catch(() => { - setIsAuthenticated(false); - }); - }, []); - - const handleLogin = async (e: React.FormEvent) => { - e.preventDefault(); - if (!password.trim()) { - setLoginError('请输入访问密码!'); - return; - } - setLoggingIn(true); - setLoginError(null); - try { - await axios.post('/api/auth/login', { password }); - setIsAuthenticated(true); - setLoginError(null); - } catch (err: any) { - console.error('登录校验失败:', err); - const errMsg = err.response?.data || '密码错误,请重试。'; - setLoginError(errMsg); - } finally { - setLoggingIn(false); - } - }; - - const handleLogout = async () => { - try { - await axios.post('/api/auth/logout'); - } catch (e) { - console.error('登出失败:', e); - } - setIsAuthenticated(false); - setPassword(''); - }; - - // 全局对话框弹窗状态 + // 1. 全局自定义 Dialog 弹窗管理 (Alert / Confirm) const [dialog, setDialog] = useState<{ type: 'alert' | 'confirm'; title: string; @@ -100,356 +48,44 @@ export default function App() { onConfirm, }); }, []); - - // 共享数据状态 - const [library, setLibrary] = useState([]); - const [selectedPaper, setSelectedPaper] = useState(null); - const [detailBibcode, setDetailBibcode] = useState(null); - const [recentlySelected, setRecentlySelected] = useState(() => { - try { - const saved = localStorage.getItem('astro_recently_selected'); - return saved ? JSON.parse(saved) : []; - } catch (e) { - console.error('Failed to parse recently selected papers', e); - return []; - } + // 2. 局部状态定义 (多组件共用或全局网络进度状态) + const [activeTab, setActiveTab] = useState<'search' | 'library' | 'reader' | 'citation' | 'sync' | 'agent'>(() => { + const saved = localStorage.getItem('astro_active_tab'); + return (saved as any) || 'search'; }); - useEffect(() => { - localStorage.setItem('astro_recently_selected', JSON.stringify(recentlySelected)); - }, [recentlySelected]); - - const addPaperToRecent = (paper: StandardPaper) => { - setRecentlySelected(prev => { - const filtered = prev.filter(p => p.bibcode !== paper.bibcode); - return [paper, ...filtered].slice(0, 5); - }); - }; - - const openCitation = (paper: StandardPaper) => { - setSelectedPaper(paper); - setActiveTab('citation'); - loadCitations(paper.bibcode, true); - addPaperToRecent(paper); - }; - - // 检索页状态 - const [searchQuery, setSearchQuery] = useState(''); - const [searchSource, setSearchSource] = useState<'all' | 'ads' | 'arxiv'>('all'); - const [searchResults, setSearchResults] = useState([]); - - // 衍生状态计算 - const detailPaper = library.find(p => p.bibcode === detailBibcode) || searchResults.find(p => p.bibcode === detailBibcode); - const [searching, setSearching] = useState(false); - const [exportingList, setExportingList] = useState([]); - const [bibtexContent, setBibtexContent] = useState(null); - const [exporting, setExporting] = useState(false); - const [searchRows, setSearchRows] = useState(15); - const [searchStart, setSearchStart] = useState(0); - const [searchSort, setSearchSort] = useState('relevance'); - const [searchCache, setSearchCache] = useState>({}); - - // 读者页状态 const [englishText, setEnglishText] = useState(''); const [chineseText, setChineseText] = useState(''); const [parsing, setParsing] = useState(false); const [translating, setTranslating] = useState(false); const [vectorizing, setVectorizing] = useState(false); - - // 引用星系数据状态 - const [citationNetwork, setCitationNetwork] = useState(null); - const [loadingCitations, setLoadingCitations] = useState(false); - const [citationHistory, setCitationHistory] = useState([]); // 多跳历史 - const [uncachedBibcode, setUncachedBibcode] = useState(null); - - // 笔记系统状态 - const [notes, setNotes] = useState([]); - const [showNotesPanel, setShowNotesPanel] = useState(false); - const [newNoteText, setNewNoteText] = useState(''); - const [newNoteColor, setNewNoteColor] = useState('yellow'); - const [selectedParagraphIdx, setSelectedParagraphIdx] = useState(null); - const [selectedText, setSelectedText] = useState(''); - - // 下载进度状态 - const [downloadingBibcodes, setDownloadingBibcodes] = useState>({}); - const [uploadingBibcode, setUploadingBibcode] = useState(null); - // 1. 初始化时加载本地文献 useEffect(() => { - if (isAuthenticated === true) { - fetchLibrary(); - } - }, [isAuthenticated]); + localStorage.setItem('astro_active_tab', activeTab); + }, [activeTab]); - const fetchLibrary = async () => { - try { - const res = await axios.get('/api/library'); - setLibrary(res.data); + // 3. 调用拆分后的业务 Hook + const auth = useAuth(); + const notes = useNotes(); + const citations = useCitations(); - const lastReadBibcode = localStorage.getItem('last_read_bibcode'); - if (lastReadBibcode) { - const lastReadPaper = res.data.find(p => p.bibcode === lastReadBibcode); - if (lastReadPaper) { - const initialTab = localStorage.getItem('astro_active_tab') || 'search'; - openReader(lastReadPaper, initialTab !== 'reader'); - } - } - } catch (e) { - console.error('加载本地文献库失败', e); - } - }; - - // 2. 检索文献 (统一执行逻辑,包含前端缓存) - const executeSearch = async (start: number, rows: number, sort: string) => { - if (!searchQuery.trim()) return; - - // 构造缓存 Key - const cacheKey = `${searchQuery.trim()}_${searchSource}_${start}_${rows}_${sort}`; - if (searchCache[cacheKey]) { - setSearchResults(searchCache[cacheKey]); - return; - } - - setSearching(true); - setBibtexContent(null); - try { - const res = await axios.get('/api/search', { - params: { q: searchQuery, source: searchSource, rows, start, sort } - }); - setSearchResults(res.data); - // 写入缓存 - setSearchCache(prev => ({ ...prev, [cacheKey]: res.data })); - } catch (e) { - console.error('检索文献失败', e); - showAlert('检索失败,请确认后端连接及 API 密钥配置。', '检索出错'); - } finally { - setSearching(false); - } - }; - - const handleSearch = async (e: React.FormEvent) => { - e.preventDefault(); - setSearchStart(0); - executeSearch(0, searchRows, searchSort); - }; - - const handlePageChange = (newStart: number) => { - setSearchStart(newStart); - executeSearch(newStart, searchRows, searchSort); - }; - - const handleSortChange = (newSort: string) => { - setSearchSort(newSort); - setSearchStart(0); - executeSearch(0, searchRows, newSort); - }; - - const handleRowsChange = (newRows: number) => { - setSearchRows(newRows); - setSearchStart(0); - executeSearch(0, newRows, searchSort); - }; - - // 3. 触发文献双格式下载 - const handleDownload = async (bibcode: string, force = false) => { - setDownloadingBibcodes(prev => ({ ...prev, [bibcode]: true })); - try { - const res = await axios.post('/api/download', { bibcode, force }); - // 更新库及检索列表中的状态 - setSearchResults(prev => prev.map(p => p.bibcode === bibcode ? res.data : p)); - setLibrary(prev => { - if (prev.some(p => p.bibcode === bibcode)) { - return prev.map(p => p.bibcode === bibcode ? res.data : p); - } else { - return [res.data, ...prev]; - } - }); - if (selectedPaper?.bibcode === bibcode) { - setSelectedPaper(res.data); - } - } catch (e) { - console.error('下载文献失败', e); - showAlert('文献下载失败,请检查 ADS 网络限制与网络代理!', '下载失败'); - } finally { - setDownloadingBibcodes(prev => ({ ...prev, [bibcode]: false })); - } - }; - - // 3b. 手动上传文献文件以绕过反爬/人机验证 - const handleManualUpload = async (bibcode: string, type: 'pdf' | 'html', file: File) => { - setUploadingBibcode(bibcode); - const formData = new FormData(); - formData.append('bibcode', bibcode); - formData.append('type', type); - formData.append('file', file); - - try { - const res = await axios.post('/api/upload', formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); - // 更新前端库及检索列表状态 - setSearchResults(prev => prev.map(p => p.bibcode === bibcode ? res.data : p)); - setLibrary(prev => { - if (prev.some(p => p.bibcode === bibcode)) { - return prev.map(p => p.bibcode === bibcode ? res.data : p); - } else { - return [res.data, ...prev]; - } - }); - if (selectedPaper?.bibcode === bibcode) { - setSelectedPaper(res.data); - } - showAlert('手动文献文件上传导入成功!', '上传成功'); - } catch (e: any) { - console.error('手动文件上传失败', e); - const errMsg = e.response?.data || '请确保上传的是合法且完整的文件。'; - showAlert(`文件上传失败: ${errMsg}`, '上传出错'); - } finally { - setUploadingBibcode(null); - } - }; - - // 3c. 手动标记文献为“无有效全文资源” - const handleMarkNoResource = async (bibcode: string, clear = false) => { - const performMark = async () => { - try { - const res = await axios.post('/api/no_resource', { bibcode, clear }); - // 更新前端库及检索列表状态 - setSearchResults(prev => prev.map(p => p.bibcode === bibcode ? res.data : p)); - setLibrary(prev => { - if (prev.some(p => p.bibcode === bibcode)) { - return prev.map(p => p.bibcode === bibcode ? res.data : p); - } else { - return [res.data, ...prev]; - } - }); - if (selectedPaper?.bibcode === bibcode) { - setSelectedPaper(res.data); - } - showAlert( - clear - ? '已成功清除“无全文资源”标记,该文献已被重新允许重载!' - : '已成功将文献标记为“无全文资源”,后续批量任务将自动跳过!', - '标记更新' - ); - } catch (e: any) { - console.error('标记更新失败', e); - const errMsg = e.response?.data || '请稍后重试。'; - showAlert(`标记失败: ${errMsg}`, '操作出错'); - } - }; - - if (clear) { - performMark(); - } else { - showConfirm( - '确认将此文献标记为“无有效全文资源”吗?\n标记后,未来的批量下载/解析重试任务将自动跳过此文献。', - performMark, - '标记无有效全文资源' - ); - } - }; - - // 4. 文献解析成 Markdown (优先 HTML, 其次 PDF MinerU) - const handleParse = async (bibcode: string, force = false) => { - setParsing(true); - try { - const res = await axios.post<{ markdown: string }>('/api/parse', { bibcode, force }); - setEnglishText(res.data.markdown); - - // 更新文献状态 - setLibrary(prev => prev.map(p => p.bibcode === bibcode ? { ...p, has_markdown: true } : p)); - if (selectedPaper?.bibcode === bibcode) { - setSelectedPaper(prev => prev ? { ...prev, has_markdown: true } : null); - } - } catch (e) { - console.error('文献解析失败', e); - showAlert('文献排版解析失败,请检查是否已完成 HTML/PDF 下载,并配置了 MinerU API 节点。', '解析失败'); - } finally { - setParsing(false); - } - }; - - // 5. 对比翻译文本 (带天文学术语修正) - const handleTranslate = async (bibcode: string, force = false) => { - setTranslating(true); - try { - const res = await axios.post<{ translation: string }>('/api/translate', { bibcode, force }); - setChineseText(res.data.translation); - - setLibrary(prev => prev.map(p => p.bibcode === bibcode ? { ...p, has_translation: true } : p)); - if (selectedPaper?.bibcode === bibcode) { - setSelectedPaper(prev => prev ? { ...prev, has_translation: true } : null); - } - } catch (e) { - console.error('文献翻译失败', e); - showAlert('翻译失败,请检查 .env 中的大模型 API 密钥与端点配置。', '翻译失败'); - } finally { - setTranslating(false); - } - }; - - // 5.5. 对文献进行知识入库 (独立任务) - const handleVectorize = async (bibcode: string) => { - setVectorizing(true); - try { - const res = await axios.post<{ chunk_count: number }>('/api/embed', { bibcode }); - - setLibrary(prev => prev.map(p => p.bibcode === bibcode ? { ...p, has_vector: true } : p)); - if (selectedPaper?.bibcode === bibcode) { - setSelectedPaper(prev => prev ? { ...prev, has_vector: true } : null); - } - showAlert(`文献已成功录入馆藏智能库,共提炼并录入 ${res.data.chunk_count} 个核心知识节点。`, '知识入库成功'); - } catch (e) { - console.error('知识入库失败', e); - showAlert('知识入库失败,请检查 .env 中的 Embedding API 配置。', '知识入库失败'); - } finally { - setVectorizing(false); - } - }; - - // 6. 加载文献引用关系网络 - const loadCitations = useCallback(async (bibcode: string, reset = false) => { - setLoadingCitations(true); - try { - const res = await axios.get('/api/citations', { - params: { bibcode } - }); - setCitationNetwork(res.data); - setCitationHistory(prev => { - if (reset) { - return [res.data]; - } - if (prev.some(net => net.bibcode === res.data.bibcode)) { - return prev; - } - return [...prev, res.data]; - }); - } catch (e) { - console.error('加载引用拓扑失败', e); - } finally { - setLoadingCitations(false); - } - }, []); - - // 7. 进入阅读器 - const openReader = async (paper: StandardPaper, skipTabSwitch = false) => { - setSelectedPaper(paper); + // 协调:进入阅读器方法 (需要拉取正文和笔记) + const openReader = useCallback(async (paper: StandardPaper, skipTabSwitch = false) => { + library.setSelectedPaper(paper); localStorage.setItem('last_read_bibcode', paper.bibcode); - addPaperToRecent(paper); + library.addPaperToRecent(paper); setEnglishText(''); setChineseText(''); - setNotes([]); - setShowNotesPanel(false); + notes.setNotes([]); + notes.setShowNotesPanel(false); + if (!skipTabSwitch) { setActiveTab('reader'); } - // 获取详情 (包含已有原文及翻译) + // 异步加载文献详情正文 try { const res = await axios.get<{ paper: StandardPaper, english_content?: string, translation_content?: string }>('/api/paper', { params: { bibcode: paper.bibcode } @@ -464,33 +100,41 @@ export default function App() { console.error('加载文献详情失败', e); } - // 加载该文献的所有笔记 + // 异步加载文献已存笔记 try { const nRes = await axios.get('/api/notes', { params: { bibcode: paper.bibcode } }); - setNotes(nRes.data); + notes.setNotes(nRes.data); } catch (e) { console.error('加载笔记失败', e); } - }; + }, [notes]); - // 7.5. 从 RAG 问答结果跳转至目标文献段落并高亮 - const handleJumpToSource = async (bibcode: string, paragraphIndex: number) => { + const library = useLibrary({ + isAuthenticated: auth.isAuthenticated, + showAlert, + showConfirm, + openReader, + }); + + const search = useSearch({ showAlert }); + + // 协调:从 RAG 问答结果跳转至目标文献段落并高亮 + const handleJumpToSource = useCallback(async (bibcode: string, paragraphIndex: number) => { setActiveTab('reader'); // 如果当前选中的不是目标文献,则先加载它 - if (!selectedPaper || selectedPaper.bibcode !== bibcode) { + if (!library.selectedPaper || library.selectedPaper.bibcode !== bibcode) { setEnglishText(''); setChineseText(''); - setNotes([]); - setShowNotesPanel(false); + notes.setNotes([]); + notes.setShowNotesPanel(false); try { - // 获取 StandardPaper 基础信息与正文 const paperRes = await axios.get<{ paper: StandardPaper, english_content?: string, translation_content?: string }>('/api/paper', { params: { bibcode } }); - setSelectedPaper(paperRes.data.paper); + library.setSelectedPaper(paperRes.data.paper); if (paperRes.data.english_content) { setEnglishText(paperRes.data.english_content); } @@ -498,9 +142,8 @@ export default function App() { setChineseText(paperRes.data.translation_content); } - // 加载该文献的所有笔记 const nRes = await axios.get('/api/notes', { params: { bibcode } }); - setNotes(nRes.data); + notes.setNotes(nRes.data); } catch (e) { console.error('加载跳转文献失败:', e); return; @@ -508,9 +151,9 @@ export default function App() { } // 强制开启侧边栏 - setShowNotesPanel(true); + notes.setShowNotesPanel(true); - // 延迟等 DOM 渲染完成进行滚动高亮 + // 延迟等 DOM 渲染完成进行平滑滚动与闪烁高亮 setTimeout(() => { const element = document.getElementById(`paragraph-block-${paragraphIndex}`); if (element) { @@ -521,70 +164,70 @@ export default function App() { }, 2500); } }, 400); - }; + }, [library.selectedPaper, notes]); - // 8. 批量导出 BibTeX - const handleExportBibtex = async () => { - if (exportingList.length === 0) return; - setExporting(true); + // 协调:触发文献解析 + const handleParse = async (bibcode: string, force = false) => { + setParsing(true); try { - const res = await axios.post<{ bibtex: string }>('/api/export', { bibcodes: exportingList }); - setBibtexContent(res.data.bibtex); + const res = await axios.post<{ markdown: string }>('/api/parse', { bibcode, force }); + setEnglishText(res.data.markdown); + + library.setLibrary(prev => prev.map(p => p.bibcode === bibcode ? { ...p, has_markdown: true } : p)); + if (library.selectedPaper?.bibcode === bibcode) { + library.setSelectedPaper(prev => prev ? { ...prev, has_markdown: true } : null); + } } catch (e) { - console.error('导出 BibTeX 失败', e); - showAlert('导出 BibTeX 失败,请检查 ADS Token。', '导出失败'); + console.error('文献解析失败', e); + showAlert('文献排版解析失败,请检查是否已完成 HTML/PDF 下载,并配置了 MinerU API 节点。', '解析失败'); } finally { - setExporting(false); + setParsing(false); } }; - // 批量选择引文 - const toggleExportItem = (bibcode: string) => { - setExportingList(prev => - prev.includes(bibcode) ? prev.filter(b => b !== bibcode) : [...prev, bibcode] - ); - }; - - // 笔记相关操作 - const handleCreateNote = async () => { - if (!selectedPaper || selectedParagraphIdx === null || !newNoteText.trim()) return; + // 协调:触发对比翻译 + const handleTranslate = async (bibcode: string, force = false) => { + setTranslating(true); try { - const res = await axios.post('/api/notes', { - bibcode: selectedPaper.bibcode, - paragraph_index: selectedParagraphIdx, - note_text: newNoteText.trim(), - highlight_color: newNoteColor, - selected_text: selectedText, - }); - setNotes(prev => [...prev, res.data]); - setNewNoteText(''); - setSelectedParagraphIdx(null); - setSelectedText(''); + const res = await axios.post<{ translation: string }>('/api/translate', { bibcode, force }); + setChineseText(res.data.translation); + + library.setLibrary(prev => prev.map(p => p.bibcode === bibcode ? { ...p, has_translation: true } : p)); + if (library.selectedPaper?.bibcode === bibcode) { + library.setSelectedPaper(prev => prev ? { ...prev, has_translation: true } : null); + } } catch (e) { - console.error('保存笔记失败', e); + console.error('文献翻译失败', e); + showAlert('翻译失败,请检查 .env 中的大模型 API 密钥与端点配置。', '翻译失败'); + } finally { + setTranslating(false); } }; - const handleDeleteNote = async (id: number) => { + // 协调:触发知识入库 + const handleVectorize = async (bibcode: string) => { + setVectorizing(true); try { - await axios.delete('/api/notes', { params: { id } }); - setNotes(prev => prev.filter(n => n.id !== id)); + const res = await axios.post<{ chunk_count: number }>('/api/embed', { bibcode }); + + library.setLibrary(prev => prev.map(p => p.bibcode === bibcode ? { ...p, has_vector: true } : p)); + if (library.selectedPaper?.bibcode === bibcode) { + library.setSelectedPaper(prev => prev ? { ...prev, has_vector: true } : null); + } + showAlert(`文献已成功录入馆藏智能库,共提炼并录入 ${res.data.chunk_count} 个核心知识节点。`, '知识入库成功'); } catch (e) { - console.error('删除笔记失败', e); + console.error('知识入库失败', e); + showAlert('知识入库失败,请检查 .env 中的 Embedding API 配置。', '知识入库失败'); + } finally { + setVectorizing(false); } }; - // 选中文本时弹出笔记添加面板 - const handleTextSelection = (paragraphIdx: number) => { - const sel = window.getSelection(); - if (sel && sel.toString().trim().length > 3) { - setSelectedText(sel.toString().trim()); - setSelectedParagraphIdx(paragraphIdx); - setShowNotesPanel(true); - } - }; + // 派生状态计算 + const detailPaper = library.getDetailPaper(search.searchResults); - if (isAuthenticated === null) { + // 4. 渲染顶层登录页面 + if (auth.isAuthenticated === null) { return (
@@ -595,16 +238,13 @@ export default function App() { ); } - if (isAuthenticated === false) { + if (auth.isAuthenticated === false) { return (
- {/* 背景点缀装饰 */}
- {/* 登录卡片 */}
- {/* Logo & 头部 */}
@@ -626,18 +266,17 @@ export default function App() {

天文学科研辅助系统 · 安全登录

- {/* 登录表单 */} -
+
setPassword(e.target.value)} + value={auth.password} + onChange={e => auth.setPassword(e.target.value)} placeholder="请输入系统访问密码" autoFocus - disabled={loggingIn} + disabled={auth.loggingIn} className="w-full pl-9 pr-4 py-2 rounded-lg bg-slate-50 border border-[#d2d8e2] text-slate-900 placeholder-slate-400 focus:outline-none focus:border-[#106ba3] focus:bg-white transition-all text-xs font-medium" />
@@ -646,18 +285,18 @@ export default function App() {
- {loginError && ( + {auth.loginError && (
- {loginError} + {auth.loginError}
)} -
-
-

{dialog.message}

-
-
- - {dialog.type === 'confirm' && ( - - )} -
-
-
- )} + {/* 全局统一 Alert / Confirm 弹窗 */} + setDialog(null)} + /> - {/* 未入库文献操作弹窗 */} - {uncachedBibcode && ( -
setUncachedBibcode(null)} - className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/40 backdrop-blur-xs transition-all animate-fade-in" - > -
e.stopPropagation()} - className="bg-[var(--bg-card)] rounded-xl border border-[var(--border-precision)] shadow-md max-w-sm w-full p-6 space-y-4" - > -
-

- - 文献尚未入库 -

- -
-
-

- 文献 {uncachedBibcode} 尚未收录在本地数据库中。 -

-

- 您可以选择在线拉取该文献元数据并入库,或是直接跳转至 NASA ADS 平台查看其原始页面。 -

-
-
- - - -
-
-
- )} + {/* 未入库文献操作引导弹窗 */} + citations.setUncachedBibcode(null)} + loadCitations={citations.loadCitations} + /> {/* 文献详情弹窗 */} - {detailPaper && ( -
setDetailBibcode(null)} - className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/40 backdrop-blur-xs transition-all animate-fade-in cursor-pointer" - > -
e.stopPropagation()} - className="bg-[var(--bg-card)] rounded-xl border border-[var(--border-precision)] shadow-md max-w-lg w-full p-6 space-y-4 cursor-default animate-fade-in" - > - {/* 标题 & 关闭 */} -
-
- 文献详情元数据 -

- {getDoctypeBadge(detailPaper.doctype)} - {detailPaper.title} -

-
- -
- - {/* 详情内容 */} -
- {/* 作者 */} -
- 作者列表 -

{detailPaper.authors.join(', ')}

-
- - {/* 期刊 & 年份 */} -
-
- 发表期刊 - - {detailPaper.pub_journal || '未标注'} - -
-
- 发表年份 - {detailPaper.year} -
-
- - {/* 摘要 */} -
- 摘要 -

- {detailPaper.abstract_text || '该文献暂无摘要数据。'} -

-
- - {/* 关键字 */} -
- 关键词 - {detailPaper.keywords && detailPaper.keywords.length > 0 ? ( -
- {detailPaper.keywords.map(kw => ( - - {kw} - - ))} -
- ) : ( -
- 暂无关键词 -
- )} -
- - {/* 标识符 */} - - - {/* 手动上传文件(应对防爬阻断) */} -
-
- 手动离线上传文献 - 防爬/人机验证备用 -
-

- 若自动下载受阻,可在浏览器中打开上方链接,手动保存 PDF 或 HTML 后在此处上传覆盖。 -

-
-
- { - const file = e.target.files?.[0]; - if (file) handleManualUpload(detailPaper.bibcode, 'pdf', file); - }} - className="absolute inset-0 opacity-0 cursor-pointer w-full h-full" - disabled={uploadingBibcode === detailPaper.bibcode} - /> - {detailPaper.has_pdf && ( - - 已下载 - - )} - - {uploadingBibcode === detailPaper.bibcode ? '上传中...' : '上传 PDF 文献'} - - 支持 .pdf 格式 -
- -
- { - const file = e.target.files?.[0]; - if (file) handleManualUpload(detailPaper.bibcode, 'html', file); - }} - className="absolute inset-0 opacity-0 cursor-pointer w-full h-full" - disabled={uploadingBibcode === detailPaper.bibcode} - /> - {detailPaper.has_html && ( - - 已下载 - - )} - - {uploadingBibcode === detailPaper.bibcode ? '上传中...' : '上传 HTML 文献'} - - 支持 .html 格式 -
-
- - {!detailPaper.is_downloaded && ( -
- {detailPaper.pdf_error === 'no_resource' && detailPaper.html_error === 'no_resource' ? ( - - ) : ( - - )} -
- )} -
- - {/* 自动下载失败诊断 */} - {!detailPaper.is_downloaded && (detailPaper.pdf_error || detailPaper.html_error) && ( -
- {detailPaper.pdf_error === 'no_resource' && detailPaper.html_error === 'no_resource' ? ( -
-
- - 已手动标记为【无有效全文资源】 -
- 该文献已从物理文献下载重试队列中排除,后续批量下载时系统将自动跳过此文献。 -
- ) : ( - <> -
- - 自动下载失败诊断信息: -
- {detailPaper.pdf_error && ( -
- PDF 错误: - {detailPaper.pdf_error} -
- )} - {detailPaper.html_error && ( -
- HTML 错误: - {detailPaper.html_error} -
- )} - - )} -
- )} -
- - {/* 底部操作:整合所有动作(阅读、图谱、下载) */} -
- {detailPaper.is_downloaded ? ( - <> - - - - - ) : ( - <> - - - - )} -
-
-
- )} + library.setDetailBibcode(null)} + uploadingBibcode={library.uploadingBibcode} + downloadingBibcodes={library.downloadingBibcodes} + handleManualUpload={(bibcode, type, file) => library.handleManualUpload(bibcode, type, file, search.setSearchResults)} + handleMarkNoResource={(bibcode, clear) => library.handleMarkNoResource(bibcode, clear, search.setSearchResults)} + handleDownload={(bibcode, force) => library.handleDownload(bibcode, force, search.setSearchResults)} + openReader={openReader} + setActiveTab={setActiveTab} + setSelectedPaper={library.setSelectedPaper} + loadCitations={citations.loadCitations} + showConfirm={showConfirm} + />
); } diff --git a/dashboard/src/features/reader/AIAssistantPanel.tsx b/dashboard/src/components/AIAssistantPanel.tsx similarity index 99% rename from dashboard/src/features/reader/AIAssistantPanel.tsx rename to dashboard/src/components/AIAssistantPanel.tsx index 32e205b..3e6dea6 100644 --- a/dashboard/src/features/reader/AIAssistantPanel.tsx +++ b/dashboard/src/components/AIAssistantPanel.tsx @@ -11,8 +11,8 @@ import { ThoughtCard, ToolCallCard, SubAgentContainer, PARENT_COLORS, routeThought, routeToolCall, routeToolResult, routeTextDelta, -} from '../../components/agent'; -import type { SSEEventHandlers, TimelineItem } from '../../components/agent'; +} from './agent'; +import type { SSEEventHandlers, TimelineItem } from './agent'; interface RetrievalSource { bibcode: string; diff --git a/dashboard/src/components/agent/AgentInputArea.tsx b/dashboard/src/components/agent/AgentInputArea.tsx new file mode 100644 index 0000000..5f6b295 --- /dev/null +++ b/dashboard/src/components/agent/AgentInputArea.tsx @@ -0,0 +1,195 @@ +import React from 'react'; +import { + Brain, Compass, BookOpen, Send, Square, Paperclip, X +} from 'lucide-react'; +import type { AgentMode } from '../../hooks/useResearchAgent'; + +interface AgentInputAreaProps { + input: string; + setInput: (val: string) => void; + streaming: boolean; + thinking: boolean; + setThinking: (val: boolean) => void; + agentMode: string; + setAgentMode: (val: string) => void; + agentModes: AgentMode[]; + pendingImage: { data?: string; path?: string; mime_type: string; name: string } | null; + setPendingImage: (val: { data?: string; path?: string; mime_type: string; name: string } | null) => void; + onSend: (text: string) => void; + onStop: () => void; + fileInputRef: React.RefObject; + attachImage: (file: File) => void; + handlePaste: (e: React.ClipboardEvent) => void; +} + +const ICON_MAP: Record> = { + Brain, + Compass, + BookOpen +}; + +function getIconComponent(iconName: string): React.ComponentType<{ className?: string }> { + return ICON_MAP[iconName] || Brain; +} + +export function AgentInputArea({ + input, + setInput, + streaming, + thinking, + setThinking, + agentMode, + setAgentMode, + agentModes, + pendingImage, + setPendingImage, + onSend, + onStop, + fileInputRef, + attachImage, + handlePaste, +}: AgentInputAreaProps) { + return ( +
+ { + e.preventDefault(); + onSend(input); + }} + className="max-w-4xl mx-auto w-full pointer-events-auto" + > +
+ {/* 图片预览 */} + {pendingImage && ( +
+ Preview + +
+ )} +