From 5db4cc599892a10bcd38deea0885e5ce946a6535 Mon Sep 17 00:00:00 2001 From: Asfmq <2696428814@qq.com> Date: Thu, 25 Jun 2026 23:45:37 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=85=A8=E6=A0=88=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E9=87=8D=E6=9E=84=E4=B8=8E=E8=B4=A8=E9=87=8F=E7=A1=AC?= =?UTF-8?q?=E5=8C=96=E2=80=94=E2=80=94API=20=E9=94=99=E8=AF=AF=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E3=80=81=E5=B7=A5=E5=85=B7=E5=9F=9F=E9=87=8D=E7=BB=84?= =?UTF-8?q?=E3=80=81=E5=AE=89=E5=85=A8=E5=8A=A0=E5=9B=BA=E3=80=81=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E7=BB=84=E4=BB=B6=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端核心变更: - API 层: 新增 AppError 枚举统一错误类型,替代散落的 (StatusCode, String) - Agent 工具域: 重组为 astro/system/ 和 astro/research/ 两级域,新增 ProcessPaperTool 流水线工具 - 安全: 新增 SSRF 双层防护 (同步字符串级 + 异步 DNS 解析级),覆盖 IPv4/IPv6 私网段 - 弱密码检测: 扩展弱密码列表并增加最小长度检查 - LLM 客户端: 新增 ChatCompleter/Embedder trait,支持依赖注入与批量向量化 embed_batch - 批量处理: AssetBatch 从串行改为 Semaphore 并发池 (BATCH_CONCURRENCY=3) - 分块器: 重写为三阶段结构化管线 (章节解析→短节合并→带标题路径子块) - RAG: embedding 计算移出事务,RetrievalResult 新增 headings/section_index 字段 - 检索: ADS/arXiv 并行检索 (tokio::join!),去重改用 HashSet,本地库回填批量 IN 查询 - 天体查询: Sesame API 升级到 v4,新增视差误差/自行/视向速度/多波段测光字段 - 迁移: 14 个增量文件合并为单一 init.sql,支持 sqlx::migrate! 内存库集成测试 - 测试: circuit_breaker/hooks/task_board/session/memory/streaming_executor 新增修正 15+ 测试 前端架构重构: - 目录重组: features/ → pages/ + components/ + hooks/ 三层分离 - App.tsx 从 1181 行压缩至 ~174 行 (逻辑抽入 9 个自定义 Hook) - Agent 面板拆分为 AgentSessionSidebar/AgentMessageList/AgentInputArea 子组件 - 新增 GlobalDialog/PaperDetailModal/UncachedPaperModal 通用对话框组件 - 工具函数抽取: celestial.ts (天体坐标格式), paper.tsx (文献信息渲染) --- .env.example | 11 + CLAUDE.md | 15 +- Cargo.lock | 1 + Cargo.toml | 1 + README.md | 4 +- dashboard/README.md | 11 +- dashboard/src/App.tsx | 1181 ++---------- .../AIAssistantPanel.tsx | 4 +- .../src/components/agent/AgentInputArea.tsx | 195 ++ .../src/components/agent/AgentMessageList.tsx | 445 +++++ .../agent/AgentMetricsPanel.tsx | 0 .../components/agent/AgentSessionSidebar.tsx | 209 +++ .../agent/AskUserQuestionCard.tsx | 0 .../agent/AuditLogViewer.tsx | 0 .../agent/PermissionRequestCard.tsx | 0 .../src/components/dialogs/GlobalDialog.tsx | 71 + .../components/dialogs/PaperDetailModal.tsx | 360 ++++ .../components/dialogs/UncachedPaperModal.tsx | 73 + .../src/components/reader/BilingualViewer.tsx | 448 +++++ .../components/reader/ReaderNotesSidebar.tsx | 285 +++ .../src/components/reader/ReaderToolbar.tsx | 314 ++++ .../src/components/sync/BatchPipelineCard.tsx | 280 +++ .../components/sync/BookmarkletToolCard.tsx | 74 + .../src/components/sync/MetadataSyncCard.tsx | 270 +++ .../src/features/agent/ResearchAgentPanel.tsx | 1617 ---------------- dashboard/src/features/reader/ReaderPanel.tsx | 1106 ----------- dashboard/src/features/sync/SyncPanel.tsx | 921 --------- dashboard/src/hooks/useAuth.ts | 67 + dashboard/src/hooks/useCitations.ts | 46 + dashboard/src/hooks/useLibrary.ts | 216 +++ dashboard/src/hooks/useNotes.ts | 69 + dashboard/src/hooks/useReaderState.ts | 317 ++++ dashboard/src/hooks/useResearchAgent.ts | 971 ++++++++++ dashboard/src/hooks/useSearch.ts | 123 ++ dashboard/src/hooks/useSyncScroll.ts | 210 +++ dashboard/src/hooks/useSyncState.ts | 375 ++++ .../citation => pages}/CitationPanel.tsx | 4 +- .../library => pages}/LibraryPanel.tsx | 6 +- dashboard/src/pages/ReaderPanel.tsx | 174 ++ dashboard/src/pages/ResearchAgentPanel.tsx | 93 + .../search => pages}/SearchPanel.tsx | 36 +- .../settings => pages}/SettingsPanel.tsx | 0 dashboard/src/pages/SyncPanel.tsx | 137 ++ dashboard/src/{types.ts => types/index.ts} | 0 dashboard/src/utils/celestial.ts | 131 ++ dashboard/src/utils/paper.tsx | 152 ++ dashboard/前端开发规范-React篇.md | 580 ------ docs/architecture/core-modules.md | 18 +- docs/database.md | 178 +- migrations/00000000000000_init.sql | 356 ++++ migrations/20260608000000_init.sql | 32 - migrations/20260608000001_notes.sql | 15 - migrations/20260608000002_add_doctype.sql | 3 - migrations/20260608000003_sync_features.sql | 12 - .../20260613000000_paper_chunks_metadata.sql | 29 - migrations/20260615000000_agent_sessions.sql | 36 - migrations/20260616000000_agent_tasks.sql | 31 - migrations/20260617000000_agent_audit_log.sql | 19 - migrations/20260618000000_agent_identity.sql | 36 - migrations/20260622000000_session_rewind.sql | 20 - migrations/20260622000001_session_branch.sql | 13 - migrations/20260623000000_fts5_search.sql | 73 - migrations/20260623000001_fix_fts5_rowid.sql | 30 - .../20260624000000_add_session_mode.sql | 11 - src/agent/background.rs | 3 +- src/agent/enums.rs | 221 +++ src/agent/hooks/builtins.rs | 8 +- src/agent/hooks/mod.rs | 61 +- src/agent/memory/mod.rs | 131 ++ src/agent/memory/selection.rs | 156 +- src/agent/mod.rs | 1 + src/agent/modes/deep_research.rs | 4 +- src/agent/modes/literature_reader.rs | 7 +- src/agent/modes/mod.rs | 8 +- src/agent/runtime/checkpoint.rs | 33 +- src/agent/runtime/circuit_breaker.rs | 75 +- src/agent/runtime/context.rs | 9 +- src/agent/runtime/file_cache.rs | 33 +- src/agent/runtime/permission_profile.rs | 14 +- src/agent/runtime/session.rs | 79 + src/agent/runtime/streaming_executor.rs | 428 ++++- src/agent/runtime/system_prompt.rs | 42 +- src/agent/task_board.rs | 177 +- src/agent/tools/astro/analyze_image.rs | 48 +- src/agent/tools/astro/mod.rs | 36 +- src/agent/tools/astro/paper.rs | 259 --- src/agent/tools/astro/rag.rs | 108 -- .../tools/astro/research/citation_network.rs | 445 +++++ .../tools/astro/research/library_search.rs | 131 ++ src/agent/tools/astro/research/metadata.rs | 73 + src/agent/tools/astro/research/mod.rs | 20 + src/agent/tools/astro/{ => research}/note.rs | 51 +- .../tools/astro/research/paper_content.rs | 166 ++ .../tools/astro/research/paper_outline.rs | 135 ++ src/agent/tools/astro/research/rag.rs | 121 ++ src/agent/tools/astro/research/target.rs | 104 + src/agent/tools/astro/system/mod.rs | 8 + src/agent/tools/astro/system/process.rs | 261 +++ src/agent/tools/astro/{ => system}/search.rs | 84 +- src/agent/tools/astro/target.rs | 93 - src/agent/tools/background.rs | 34 +- src/agent/tools/filesystem/bash.rs | 106 +- src/agent/tools/mod.rs | 97 +- src/agent/tools/todo.rs | 244 ++- src/api/agent.rs | 145 +- src/api/auth.rs | 48 +- src/api/error.rs | 187 ++ src/api/helpers.rs | 4 + src/api/mod.rs | 7 +- src/api/notes.rs | 18 +- src/api/papers.rs | 314 ++-- src/api/search.rs | 25 +- src/api/sync.rs | 67 +- src/api/targets.rs | 95 +- src/bin/cli.rs | 8 +- src/clients/llm.rs | 115 ++ src/lib.rs | 6 +- src/main.rs | 24 +- src/services/batch/asset.rs | 1669 +++++++++-------- src/services/chunker.rs | 917 +++++++-- src/services/download.rs | 33 +- src/services/mod.rs | 5 +- src/services/parser/common.rs | 3 + src/services/parser/mod.rs | 17 +- src/services/rag.rs | 273 ++- src/services/search.rs | 303 ++- src/services/section_parser.rs | 292 +++ src/services/target.rs | 387 +++- src/services/translation.rs | 30 + src/utils/mod.rs | 1 + src/utils/ssrf.rs | 199 ++ 131 files changed, 13934 insertions(+), 7887 deletions(-) rename dashboard/src/{features/reader => components}/AIAssistantPanel.tsx (99%) create mode 100644 dashboard/src/components/agent/AgentInputArea.tsx create mode 100644 dashboard/src/components/agent/AgentMessageList.tsx rename dashboard/src/{features => components}/agent/AgentMetricsPanel.tsx (100%) create mode 100644 dashboard/src/components/agent/AgentSessionSidebar.tsx rename dashboard/src/{features => components}/agent/AskUserQuestionCard.tsx (100%) rename dashboard/src/{features => components}/agent/AuditLogViewer.tsx (100%) rename dashboard/src/{features => components}/agent/PermissionRequestCard.tsx (100%) create mode 100644 dashboard/src/components/dialogs/GlobalDialog.tsx create mode 100644 dashboard/src/components/dialogs/PaperDetailModal.tsx create mode 100644 dashboard/src/components/dialogs/UncachedPaperModal.tsx create mode 100644 dashboard/src/components/reader/BilingualViewer.tsx create mode 100644 dashboard/src/components/reader/ReaderNotesSidebar.tsx create mode 100644 dashboard/src/components/reader/ReaderToolbar.tsx create mode 100644 dashboard/src/components/sync/BatchPipelineCard.tsx create mode 100644 dashboard/src/components/sync/BookmarkletToolCard.tsx create mode 100644 dashboard/src/components/sync/MetadataSyncCard.tsx delete mode 100644 dashboard/src/features/agent/ResearchAgentPanel.tsx delete mode 100644 dashboard/src/features/reader/ReaderPanel.tsx delete mode 100644 dashboard/src/features/sync/SyncPanel.tsx create mode 100644 dashboard/src/hooks/useAuth.ts create mode 100644 dashboard/src/hooks/useCitations.ts create mode 100644 dashboard/src/hooks/useLibrary.ts create mode 100644 dashboard/src/hooks/useNotes.ts create mode 100644 dashboard/src/hooks/useReaderState.ts create mode 100644 dashboard/src/hooks/useResearchAgent.ts create mode 100644 dashboard/src/hooks/useSearch.ts create mode 100644 dashboard/src/hooks/useSyncScroll.ts create mode 100644 dashboard/src/hooks/useSyncState.ts rename dashboard/src/{features/citation => pages}/CitationPanel.tsx (98%) rename dashboard/src/{features/library => pages}/LibraryPanel.tsx (99%) create mode 100644 dashboard/src/pages/ReaderPanel.tsx create mode 100644 dashboard/src/pages/ResearchAgentPanel.tsx rename dashboard/src/{features/search => pages}/SearchPanel.tsx (90%) rename dashboard/src/{features/settings => pages}/SettingsPanel.tsx (100%) create mode 100644 dashboard/src/pages/SyncPanel.tsx rename dashboard/src/{types.ts => types/index.ts} (100%) create mode 100644 dashboard/src/utils/celestial.ts create mode 100644 dashboard/src/utils/paper.tsx delete mode 100644 dashboard/前端开发规范-React篇.md create mode 100644 migrations/00000000000000_init.sql delete mode 100644 migrations/20260608000000_init.sql delete mode 100644 migrations/20260608000001_notes.sql delete mode 100644 migrations/20260608000002_add_doctype.sql delete mode 100644 migrations/20260608000003_sync_features.sql delete mode 100644 migrations/20260613000000_paper_chunks_metadata.sql delete mode 100644 migrations/20260615000000_agent_sessions.sql delete mode 100644 migrations/20260616000000_agent_tasks.sql delete mode 100644 migrations/20260617000000_agent_audit_log.sql delete mode 100644 migrations/20260618000000_agent_identity.sql delete mode 100644 migrations/20260622000000_session_rewind.sql delete mode 100644 migrations/20260622000001_session_branch.sql delete mode 100644 migrations/20260623000000_fts5_search.sql delete mode 100644 migrations/20260623000001_fix_fts5_rowid.sql delete mode 100644 migrations/20260624000000_add_session_mode.sql create mode 100644 src/agent/enums.rs delete mode 100644 src/agent/tools/astro/paper.rs delete mode 100644 src/agent/tools/astro/rag.rs create mode 100644 src/agent/tools/astro/research/citation_network.rs create mode 100644 src/agent/tools/astro/research/library_search.rs create mode 100644 src/agent/tools/astro/research/metadata.rs create mode 100644 src/agent/tools/astro/research/mod.rs rename src/agent/tools/astro/{ => research}/note.rs (61%) create mode 100644 src/agent/tools/astro/research/paper_content.rs create mode 100644 src/agent/tools/astro/research/paper_outline.rs create mode 100644 src/agent/tools/astro/research/rag.rs create mode 100644 src/agent/tools/astro/research/target.rs create mode 100644 src/agent/tools/astro/system/mod.rs create mode 100644 src/agent/tools/astro/system/process.rs rename src/agent/tools/astro/{ => system}/search.rs (63%) delete mode 100644 src/agent/tools/astro/target.rs create mode 100644 src/api/error.rs create mode 100644 src/services/section_parser.rs create mode 100644 src/utils/mod.rs create mode 100644 src/utils/ssrf.rs 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 + +
+ )} +