From 81096a5a62c367b9320fdf1a041144b6c049f0e4 Mon Sep 17 00:00:00 2001 From: Color2333 <1552429809@qq.com> Date: Thu, 19 Mar 2026 23:40:23 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E5=85=A8=E7=AB=99=E6=80=A7=E8=83=BD?= =?UTF-8?q?=E4=BC=98=E5=8C=96=20+=20=E5=85=A8=E5=B1=80=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 性能优化: - KaTeX 字体 + PDF Worker 改为 CDN,首屏 -2.7MB - ForceGraph2D/react-pdf/react-markdown 懒加载,首屏 -800KB+ - topic_stats N+1 查询改为批量聚合 (401次→4次) - Citation 字段加索引 - graph_service 全量加载改为 list_lightweight (内存 -90%) - AgentSession Set→Array 减少重渲染 - HTTP 客户端复用 + nginx gzip + LLM 指数退避重试 - 50+ 处 index-as-key 修复 任务系统重构: - TaskTracker 统一进度回调签名 (msg, cur, tot) - TaskManager 合并到 global_tracker - fetch/cs_feed/weekly/figure_analysis 进度回调粒度增强 - GlobalTaskBar 改为右下角悬浮面板 (分类图标/颜色/历史) - Sidebar 顶部精简为任务状态入口 - ActiveTask 增加 category/created_at 字段 --- apps/api/routers/content.py | 9 +- apps/api/routers/cs_feeds.py | 18 +- apps/api/routers/graph.py | 33 +++- apps/api/routers/jobs.py | 67 ++++++- apps/api/routers/papers.py | 76 +++++--- apps/api/routers/pipelines.py | 6 +- apps/api/routers/topics.py | 18 +- frontend/index.html | 3 + frontend/nginx.conf | 1 + frontend/src/components/GlobalTaskBar.tsx | 184 ++++++++++++------ frontend/src/components/Layout.tsx | 6 +- frontend/src/components/Markdown.tsx | 1 - frontend/src/components/PdfReader.tsx | 7 +- frontend/src/components/Sidebar.tsx | 40 ++-- .../src/components/graph/CitationPanel.tsx | 120 ++++++------ .../src/components/graph/InsightPanel.tsx | 12 +- .../src/components/graph/OverviewPanel.tsx | 49 ++--- frontend/src/contexts/AgentSessionContext.tsx | 39 ++-- frontend/src/contexts/GlobalTaskContext.tsx | 21 +- frontend/src/hooks/useAsync.ts | 40 +++- frontend/src/pages/Agent.tsx | 10 +- frontend/src/pages/AgentMessages.tsx | 13 +- frontend/src/pages/AgentSteps.tsx | 50 ++--- frontend/src/pages/Collect.tsx | 5 +- frontend/src/pages/Dashboard.tsx | 8 +- frontend/src/pages/PaperDetail.tsx | 14 +- frontend/src/pages/Wiki.tsx | 62 +++--- frontend/src/pages/Writing.tsx | 19 +- frontend/src/types/index.ts | 2 + packages/ai/daily_runner.py | 31 ++- packages/ai/graph_service.py | 42 ++-- packages/ai/pipelines.py | 169 ++++++++++------ packages/ai/task_manager.py | 136 +++---------- packages/domain/task_tracker.py | 27 ++- packages/integrations/llm_client.py | 152 +++++++++++---- packages/storage/db.py | 3 + packages/storage/repositories.py | 137 ++++++++----- 37 files changed, 1009 insertions(+), 621 deletions(-) diff --git a/apps/api/routers/content.py b/apps/api/routers/content.py index 519f2a3..d1fa1cc 100644 --- a/apps/api/routers/content.py +++ b/apps/api/routers/content.py @@ -98,6 +98,7 @@ def start_topic_wiki_task( fn=_run_topic_wiki_task, keyword=keyword, limit=limit, + category="generation", ) return {"task_id": task_id, "status": "pending"} @@ -181,13 +182,19 @@ def daily_brief(req: DailyBriefRequest) -> dict: def _generate_fn(progress_callback=None): # publish() 内部已写入 generated_content 表,无需重复 - return brief_service.publish(recipient=recipient) + if progress_callback: + progress_callback("正在生成每日简报...", 20, 100) + result = brief_service.publish(recipient=recipient) + if progress_callback: + progress_callback("简报生成完成", 95, 100) + return result task_id = global_tracker.submit( task_type="daily_brief", title="📰 生成每日简报", fn=_generate_fn, total=100, + category="generation", ) return { "task_id": task_id, diff --git a/apps/api/routers/cs_feeds.py b/apps/api/routers/cs_feeds.py index 7fe94bd..de33b1a 100644 --- a/apps/api/routers/cs_feeds.py +++ b/apps/api/routers/cs_feeds.py @@ -151,19 +151,35 @@ def _fetch_fn(progress_callback=None): from packages.storage.db import session_scope from packages.storage.repositories import PaperRepository + if progress_callback: + progress_callback("正在获取论文列表...", 10, 100) client = ArxivClient() papers = client.fetch_latest( query=f"cat:{category_code}", max_results=sub.daily_limit, days_back=7, ) + + total_papers = len(papers) + if progress_callback: + progress_callback(f"开始入库 ({total_papers} 篇)...", 50, 100) + count = 0 with session_scope() as session: paper_repo = PaperRepository(session) - for p in papers: + for i, p in enumerate(papers): paper_repo.upsert_paper(p) count += 1 + if progress_callback: + progress_callback( + f"入库中 ({i + 1}/{total_papers})...", + 50 + int((i + 1) / total_papers * 40), + 100, + ) repo.update_run_status(category_code, count) + + if progress_callback: + progress_callback("抓取完成", 95, 100) return {"fetched": count} task_id = global_tracker.submit( diff --git a/apps/api/routers/graph.py b/apps/api/routers/graph.py index 6b953a6..85cd856 100644 --- a/apps/api/routers/graph.py +++ b/apps/api/routers/graph.py @@ -26,12 +26,17 @@ def sync_citations_incremental( """增量同步引用(后台执行)""" def _fn(progress_callback=None): - return graph_service.sync_incremental( + if progress_callback: + progress_callback("正在同步增量引用...", 20, 100) + result = graph_service.sync_incremental( paper_limit=paper_limit, edge_limit_per_paper=edge_limit_per_paper, ) + if progress_callback: + progress_callback("增量引用同步完成", 90, 100) + return result - task_id = global_tracker.submit("citation_sync", "📊 增量引用同步", _fn) + task_id = global_tracker.submit("citation_sync", "📊 增量引用同步", _fn, category="sync") return {"task_id": task_id, "message": "增量引用同步已启动", "status": "running"} @@ -52,13 +57,20 @@ def sync_citations_for_topic( pass def _fn(progress_callback=None): - return graph_service.sync_citations_for_topic( + if progress_callback: + progress_callback("正在同步主题引用...", 20, 100) + result = graph_service.sync_citations_for_topic( topic_id=topic_id, paper_limit=paper_limit, edge_limit_per_paper=edge_limit_per_paper, ) + if progress_callback: + progress_callback("主题引用同步完成", 90, 100) + return result - task_id = global_tracker.submit("citation_sync", f"📊 主题引用同步: {topic_name}", _fn) + task_id = global_tracker.submit( + "citation_sync", f"📊 主题引用同步:{topic_name}", _fn, category="sync" + ) return {"task_id": task_id, "message": f"主题引用同步已启动: {topic_name}", "status": "running"} @@ -71,9 +83,16 @@ def sync_citations( paper_title = get_paper_title(UUID(paper_id)) or paper_id[:8] def _fn(progress_callback=None): - return graph_service.sync_citations_for_paper(paper_id=paper_id, limit=limit) - - task_id = global_tracker.submit("citation_sync", f"📄 引用同步: {paper_title[:30]}", _fn) + if progress_callback: + progress_callback("正在同步论文引用...", 20, 100) + result = graph_service.sync_citations_for_paper(paper_id=paper_id, limit=limit) + if progress_callback: + progress_callback("论文引用同步完成", 90, 100) + return result + + task_id = global_tracker.submit( + "citation_sync", f"📄 引用同步:{paper_title[:30]}", _fn, category="sync" + ) return {"task_id": task_id, "message": "论文引用同步已启动", "status": "running"} diff --git a/apps/api/routers/jobs.py b/apps/api/routers/jobs.py index 5c1dd0e..a0b63a9 100644 --- a/apps/api/routers/jobs.py +++ b/apps/api/routers/jobs.py @@ -7,7 +7,7 @@ from fastapi import APIRouter, BackgroundTasks, HTTPException, Query -from packages.ai.daily_runner import run_daily_brief, run_daily_ingest, run_weekly_graph_maintenance +from packages.ai.daily_runner import run_daily_brief, run_daily_ingest from packages.domain.enums import ReadStatus from packages.domain.task_tracker import global_tracker from packages.storage.db import session_scope @@ -31,7 +31,7 @@ def _fn(progress_callback=None): brief = run_daily_brief() return {"ingest": ingest, "brief": brief} - task_id = global_tracker.submit("daily_job", "📅 每日任务执行", _fn) + task_id = global_tracker.submit("daily_job", "📅 每日任务执行", _fn, category="report") return {"task_id": task_id, "message": "每日任务已启动", "status": "running"} @@ -40,9 +40,54 @@ def run_weekly_graph_once() -> dict: """每周图维护任务 - 后台执行""" def _fn(progress_callback=None): - return run_weekly_graph_maintenance() + from packages.ai.graph_service import GraphService + from packages.storage.db import session_scope + from packages.storage.repositories import TopicRepository - task_id = global_tracker.submit("weekly_maintenance", "🔄 每周图维护", _fn) + if progress_callback: + progress_callback("正在获取主题列表...", 10, 100) + + with session_scope() as session: + topics = TopicRepository(session).list_topics(enabled_only=True) + + total_topics = len(topics) + graph = GraphService() + topic_results = [] + + for i, t in enumerate(topics): + if progress_callback: + progress_callback( + f"处理主题 {i + 1}/{total_topics}: {t.name[:20]}...", + 20 + int((i + 1) / total_topics * 40), + 100, + ) + try: + topic_results.append( + graph.sync_citations_for_topic( + topic_id=t.id, + paper_limit=20, + edge_limit_per_paper=6, + ) + ) + except Exception: + logger.exception( + "Failed to sync citations for topic %s", + t.id, + ) + continue + + if progress_callback: + progress_callback("正在执行增量同步...", 70, 100) + incremental = graph.sync_incremental(paper_limit=50, edge_limit_per_paper=6) + + if progress_callback: + progress_callback("图维护完成", 95, 100) + return { + "topic_sync": topic_results, + "incremental": incremental, + } + + task_id = global_tracker.submit("weekly_maintenance", "🔄 每周图维护", _fn, category="sync") return {"task_id": task_id, "message": "每周图维护已启动", "status": "running"} @@ -79,7 +124,11 @@ def _run_batch(): failed = 0 try: global_tracker.start( - task_id, "batch_process", f"📚 批量处理未读论文 ({total} 篇)", total=total + task_id, + "batch_process", + f"📚 批量处理未读论文 ({total} 篇)", + total=total, + category="analysis", ) with ThreadPoolExecutor(max_workers=PAPER_CONCURRENCY) as pool: @@ -204,7 +253,9 @@ async def run_daily_report_once(background_tasks: BackgroundTasks): def _run_workflow_bg(): task_id = f"daily_report_{_uuid.uuid4().hex[:8]}" - global_tracker.start(task_id, "daily_report", "📊 每日报告工作流", total=100) + global_tracker.start( + task_id, "daily_report", "📊 每日报告工作流", total=100, category="report" + ) def _progress(msg: str, cur: int, tot: int): global_tracker.update(task_id, cur, msg, total=100) @@ -235,7 +286,9 @@ async def run_daily_report_send_only( def _run_send_only_bg(): task_id = f"report_send_{_uuid.uuid4().hex[:8]}" - global_tracker.start(task_id, "report_send", "📧 快速发送简报", total=100) + global_tracker.start( + task_id, "report_send", "📧 快速发送简报", total=100, category="report" + ) def _progress(msg: str, cur: int, tot: int): global_tracker.update(task_id, cur, msg, total=100) diff --git a/apps/api/routers/papers.py b/apps/api/routers/papers.py index 0dfddb8..c14d640 100644 --- a/apps/api/routers/papers.py +++ b/apps/api/routers/papers.py @@ -5,6 +5,7 @@ from pathlib import Path from uuid import UUID +import httpx from fastapi import APIRouter, HTTPException, Query from fastapi.responses import FileResponse @@ -14,6 +15,18 @@ from packages.storage.db import session_scope from packages.storage.repositories import PaperRepository +# 全局 HTTP 客户端复用(避免每次请求创建新客户端) +_http_client: httpx.AsyncClient | None = None + + +def _get_http_client() -> httpx.AsyncClient: + """获取或创建全局 HTTP 客户端""" + global _http_client + if _http_client is None or _http_client.is_closed: + _http_client = httpx.AsyncClient(timeout=60.0, follow_redirects=True) + return _http_client + + router = APIRouter() @@ -78,7 +91,6 @@ def recommended_papers(top_k: int = Query(default=10, ge=1, le=50)) -> dict: @router.get("/papers/proxy-arxiv-pdf/{arxiv_id:path}") async def proxy_arxiv_pdf(arxiv_id: str): """代理访问 arXiv PDF(解决 CORS 问题)""" - import httpx # 清理 arxiv_id(移除版本号) clean_id = arxiv_id.split("v")[0] @@ -86,29 +98,27 @@ async def proxy_arxiv_pdf(arxiv_id: str): try: # 使用后端服务器访问 arXiv(绕过 CORS) - async with httpx.AsyncClient(timeout=60.0) as client: - response = await client.get(arxiv_url, follow_redirects=True) - - if response.status_code == 404: - raise HTTPException(status_code=404, detail=f"arXiv 论文不存在:{clean_id}") - - if response.status_code != 200: - raise HTTPException( - status_code=500, detail=f"arXiv 访问失败:{response.status_code}" - ) - - # 返回 PDF 内容 - from fastapi.responses import Response - - return Response( - content=response.content, - media_type="application/pdf", - headers={ - "Access-Control-Allow-Origin": "*", - "Content-Disposition": f'inline; filename="{clean_id}.pdf"', - "Cache-Control": "public, max-age=3600", - }, - ) + client = _get_http_client() + response = await client.get(arxiv_url, follow_redirects=True) + + if response.status_code == 404: + raise HTTPException(status_code=404, detail=f"arXiv 论文不存在:{clean_id}") + + if response.status_code != 200: + raise HTTPException(status_code=500, detail=f"arXiv 访问失败:{response.status_code}") + + # 返回 PDF 内容 + from fastapi.responses import Response + + return Response( + content=response.content, + media_type="application/pdf", + headers={ + "Access-Control-Allow-Origin": "*", + "Content-Disposition": f'inline; filename="{clean_id}.pdf"', + "Cache-Control": "public, max-age=3600", + }, + ) except httpx.TimeoutException: raise HTTPException(status_code=504, detail="arXiv 请求超时") except httpx.RequestError as exc: @@ -328,17 +338,33 @@ def analyze_paper_figures( def _analyze_fn(progress_callback=None): from packages.ai.figure_service import FigureService + if progress_callback: + progress_callback("正在提取图表...", 10, 100) svc = FigureService() results = svc.analyze_paper_figures(paper_id, pdf_path, max_figures) + + total_figures = len(results) + if progress_callback and total_figures > 0: + progress_callback(f"正在生成解读 ({total_figures} 个图表)...", 50, 100) + # 分析完成后,从 DB 获取带 id 的完整结果 from packages.ai.figure_service import FigureService as FS2 items = FS2.get_paper_analyses(paper_id) - for item in items: + for i, item in enumerate(items): if item.get("has_image"): item["image_url"] = f"/papers/{paper_id}/figures/{item['id']}/image" else: item["image_url"] = None + if progress_callback: + progress_callback( + f"解读中 ({i + 1}/{total_figures})...", + 50 + int((i + 1) / total_figures * 45), + 100, + ) + + if progress_callback: + progress_callback("图表分析完成", 95, 100) return {"paper_id": str(paper_id), "count": len(items), "items": items} task_id = global_tracker.submit( diff --git a/apps/api/routers/pipelines.py b/apps/api/routers/pipelines.py index 93ad9da..421883c 100644 --- a/apps/api/routers/pipelines.py +++ b/apps/api/routers/pipelines.py @@ -26,7 +26,7 @@ def run_skim(paper_id: UUID) -> dict: tid = f"skim_{paper_id.hex[:8]}" title = get_paper_title(paper_id) or str(paper_id)[:8] - global_tracker.start(tid, "skim", f"粗读: {title[:30]}", total=1) + global_tracker.start(tid, "skim", f"粗读:{title[:30]}", total=1, category="analysis") try: skim = pipelines.skim(paper_id) global_tracker.finish(tid, success=True) @@ -40,7 +40,7 @@ def run_skim(paper_id: UUID) -> dict: def run_deep(paper_id: UUID) -> dict: tid = f"deep_{paper_id.hex[:8]}" title = get_paper_title(paper_id) or str(paper_id)[:8] - global_tracker.start(tid, "deep_read", f"精读: {title[:30]}", total=1) + global_tracker.start(tid, "deep_read", f"精读:{title[:30]}", total=1, category="analysis") try: deep = pipelines.deep_dive(paper_id) global_tracker.finish(tid, success=True) @@ -54,7 +54,7 @@ def run_deep(paper_id: UUID) -> dict: def run_embed(paper_id: UUID) -> dict: tid = f"embed_{paper_id.hex[:8]}" title = get_paper_title(paper_id) or str(paper_id)[:8] - global_tracker.start(tid, "embed", f"嵌入: {title[:30]}", total=1) + global_tracker.start(tid, "embed", f"嵌入:{title[:30]}", total=1, category="analysis") try: pipelines.embed_paper(paper_id) global_tracker.finish(tid, success=True) diff --git a/apps/api/routers/topics.py b/apps/api/routers/topics.py index 23c49de..e46c36e 100644 --- a/apps/api/routers/topics.py +++ b/apps/api/routers/topics.py @@ -136,12 +136,22 @@ def manual_fetch_topic(topic_id: str) -> dict: topic_name = topic.name def _fetch_fn(progress_callback=None): - return run_topic_ingest(topic_id) + # 分阶段报告进度:抓取 (0-50%) -> 处理 (50-100%) + def _stage_callback(msg, cur, tot): + # 将内部进度映射到 0-50% 范围 + progress_callback(f"抓取:{msg}", int(cur / tot * 50), 100) + + result = run_topic_ingest(topic_id, progress_callback=_stage_callback) + + if progress_callback: + progress_callback("处理完成", 100, 100) + return result task_id = global_tracker.submit( task_type="fetch", - title=f"抓取: {topic_name[:30]}", + title=f"抓取:{topic_name[:30]}", fn=_fetch_fn, + category="collection", ) return { "status": "started", @@ -232,9 +242,9 @@ def ingest_references(body: ReferenceImportReq) -> dict: @router.get("/ingest/references/status/{task_id}") def ingest_references_status(task_id: str) -> dict: """查询参考文献导入任务进度""" - from packages.ai.pipelines import get_import_task + from packages.domain.task_tracker import global_tracker - task = get_import_task(task_id) + task = global_tracker.get_task(task_id) if not task: raise HTTPException(404, "Task not found") return task diff --git a/frontend/index.html b/frontend/index.html index c746dcf..226b7ca 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -19,6 +19,9 @@ + + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf index ff9d80f..650eb59 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -26,6 +26,7 @@ server { # Gzip 压缩(级别 6 兼顾压缩率和 CPU) gzip on; + gzip_static on; gzip_comp_level 6; gzip_min_length 1000; gzip_proxied any; diff --git a/frontend/src/components/GlobalTaskBar.tsx b/frontend/src/components/GlobalTaskBar.tsx index 30a3a9d..1598890 100644 --- a/frontend/src/components/GlobalTaskBar.tsx +++ b/frontend/src/components/GlobalTaskBar.tsx @@ -1,91 +1,165 @@ -/** - * 全局任务进度条 — 固定在页面底部 - * @author Color2333 - */ -import { useGlobalTasks, type ActiveTask } from "@/contexts/GlobalTaskContext"; -import { Loader2, CheckCircle2, XCircle, ChevronUp, ChevronDown } from "lucide-react"; -import { useState } from "react"; +import { useGlobalTasks, TASK_CATEGORY_CONFIG, type ActiveTask } from "@/contexts/GlobalTaskContext"; +import { Loader2, CheckCircle2, XCircle, ChevronRight, X, Clock, RotateCcw } from "lucide-react"; +import { useState, useEffect } from "react"; import { cn } from "@/lib/utils"; +function getTaskConfig(task: ActiveTask) { + const cat = task.category || "general"; + return TASK_CATEGORY_CONFIG[cat as keyof typeof TASK_CATEGORY_CONFIG] || TASK_CATEGORY_CONFIG.general; +} + function TaskItem({ task }: { task: ActiveTask }) { + const cfg = getTaskConfig(task); const pct = task.progress_pct; + return ( -
- {task.finished ? ( - task.success ? ( - - ) : ( - - ) - ) : ( - - )} +
+ {cfg.icon}
-
- {task.title} - +
+ {task.title} +
+ {task.finished ? ( + task.success + ? + : + ) : ( + + )} +
+
+ +
+ {cfg.label} + {task.total > 0 ? `${task.current}/${task.total}` : ""} - {task.finished ? "" : ` · ${task.elapsed_seconds}s`} + {task.elapsed_seconds > 0 && ( + + {task.elapsed_seconds}s + + )}
+ {task.message && ( -

{task.message}

+

{task.message}

)} + {!task.finished && task.total > 0 && ( -
+
80 ? "bg-success" : "bg-primary")} style={{ width: `${pct}%` }} />
)} + + {task.finished && !task.success && task.error && ( +

{task.error}

+ )}
); } export default function GlobalTaskBar() { - const { tasks, hasRunning } = useGlobalTasks(); - const [expanded, setExpanded] = useState(true); // 默认展开 + const { tasks, activeTasks, hasRunning } = useGlobalTasks(); + const [expanded, setExpanded] = useState(false); + const [visible, setVisible] = useState(false); - if (tasks.length === 0) return null; + useEffect(() => { + if (tasks.length > 0) setVisible(true); + }, [tasks.length]); - const running = tasks.filter((t) => !t.finished); - const recent = tasks.filter((t) => t.finished).slice(0, 3); - const displayTasks = expanded ? [...running, ...recent] : running.slice(0, 1); + const runningCount = activeTasks.length; + const finishedCount = tasks.filter(t => t.finished).length; - if (running.length === 0 && !expanded) return null; + if (tasks.length === 0 || !visible) return null; return ( -
+ <> - {displayTasks.length > 0 && ( -
- {displayTasks.map((t) => ( - - ))} + + {expanded && ( +
+
+
+ 任务中心 + {runningCount > 0 && ( + + {runningCount} + + )} +
+ +
+ +
+ {activeTasks.length > 0 && ( +
+

+ 进行中 ({activeTasks.length}) +

+ {activeTasks.map((task) => ( + + ))} +
+ )} + + {tasks.filter(t => t.finished).length > 0 && ( +
+

+ + 最近完成 ({tasks.filter(t => t.finished).length}) +

+ {tasks.filter(t => t.finished).slice(0, 5).map((task) => ( + + ))} +
+ )} +
)} -
+ ); } diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 1ccae4c..178c25f 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -1,12 +1,9 @@ -/** - * 主布局组件 - * @author Color2333 - */ import { Outlet, useLocation } from "react-router-dom"; import Sidebar from "./Sidebar"; import { ConversationProvider } from "@/contexts/ConversationContext"; import { AgentSessionProvider } from "@/contexts/AgentSessionContext"; import { GlobalTaskProvider } from "@/contexts/GlobalTaskContext"; +import GlobalTaskBar from "./GlobalTaskBar"; export default function Layout() { const { pathname } = useLocation(); @@ -29,6 +26,7 @@ export default function Layout() {
)} +
diff --git a/frontend/src/components/Markdown.tsx b/frontend/src/components/Markdown.tsx index f24366e..584edcf 100644 --- a/frontend/src/components/Markdown.tsx +++ b/frontend/src/components/Markdown.tsx @@ -7,7 +7,6 @@ import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import rehypeKatex from "rehype-katex"; -import "katex/dist/katex.min.css"; interface Props { children: string; diff --git a/frontend/src/components/PdfReader.tsx b/frontend/src/components/PdfReader.tsx index 187dc76..b6a9eb7 100644 --- a/frontend/src/components/PdfReader.tsx +++ b/frontend/src/components/PdfReader.tsx @@ -26,10 +26,7 @@ import { Check, } from "lucide-react"; -pdfjs.GlobalWorkerOptions.workerSrc = new URL( - "pdfjs-dist/build/pdf.worker.min.mjs", - import.meta.url, -).toString(); +pdfjs.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.min.mjs`; interface PdfReaderProps { paperId: string; @@ -473,7 +470,7 @@ export default function PdfReader({ paperId, paperTitle, paperArxivId, onClose } const cfg = actionLabels[r.action]; return (
{/* 卡片头部 */} diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 289d333..e4e05a3 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -158,33 +158,23 @@ export default function Sidebar() {
- {/* 活跃任务进度条 */} {hasRunning && activeTasks.length > 0 && ( -
-
- - 运行中 -
- {activeTasks.slice(0, 2).map((task) => ( -
-
- {task.title} - {task.progress_pct}% -
-
-
-
-

{task.message}

+
+
+ +
+

+ {activeTasks.length} 个任务进行中 +

+

+ {activeTasks[0]?.title || ""} + {activeTasks.length > 1 ? ` 等${activeTasks.length}个` : ""} +

- ))} - {activeTasks.length > 2 && ( -

- 还有 {activeTasks.length - 2} 个任务正在运行... -

- )} +
+ {activeTasks[0]?.progress_pct || 0}% +
+
)} diff --git a/frontend/src/components/graph/CitationPanel.tsx b/frontend/src/components/graph/CitationPanel.tsx index b3b02d3..e97c068 100644 --- a/frontend/src/components/graph/CitationPanel.tsx +++ b/frontend/src/components/graph/CitationPanel.tsx @@ -2,7 +2,7 @@ * 引文分析面板 — 单篇引用详情 / 主题网络 / 深度溯源 * @author Color2333 */ -import { useState, useCallback, useEffect, useRef, useMemo } from "react"; +import { useState, useCallback, useEffect, useRef, useMemo, lazy, Suspense } from "react"; import { Link } from "react-router-dom"; import { Button, Badge } from "@/components/ui"; import { useToast } from "@/contexts/ToastContext"; @@ -10,7 +10,7 @@ import { graphApi, paperApi, topicApi, actionApi, ingestApi, type CollectionAction, type ImportTaskStatus, type ReferenceImportEntry, } from "@/services/api"; -import ForceGraph2D from "react-force-graph-2d"; +const ForceGraph2D = lazy(() => import("react-force-graph-2d")); import { Search, Network, FileText, Rss, Clock, Loader2, ArrowDown, ArrowUp, ChevronDown, ChevronRight, Star, @@ -585,7 +585,7 @@ function RichCitationListView({ data }: { data: CitationDetail }) {
{data.references.map((entry, i) => ( 0 && (
{task.results.map((r, i) => ( -
+
{r.status === "imported" && } {r.status === "skipped" && } {r.status === "failed" && } @@ -901,31 +901,33 @@ function PaperCitationGraphView({ detail }: { detail: CitationDetail }) { 已入库
- "rgba(148,163,184,0.3)"} - cooldownTicks={80} - nodeCanvasObjectMode={() => "after"} - nodeCanvasObject={(node: any, ctx: CanvasRenderingContext2D, globalScale: number) => { - if (!node.x || !node.y) return; - const label = node.label || ""; - const fontSize = Math.max(10 / globalScale, 2); - ctx.font = `${fontSize}px Sans-serif`; - ctx.textAlign = "center"; - ctx.textBaseline = "top"; - ctx.fillStyle = "rgba(30,41,59,0.8)"; - if (globalScale > 1.5 || node.group === "center") { - ctx.fillText(label.slice(0, 30), node.x, node.y + 6); - } - }} - /> +
}> + "rgba(148,163,184,0.3)"} + cooldownTicks={80} + nodeCanvasObjectMode={() => "after"} + nodeCanvasObject={(node: any, ctx: CanvasRenderingContext2D, globalScale: number) => { + if (!node.x || !node.y) return; + const label = node.label || ""; + const fontSize = Math.max(10 / globalScale, 2); + ctx.font = `${fontSize}px Sans-serif`; + ctx.textAlign = "center"; + ctx.textBaseline = "top"; + ctx.fillStyle = "rgba(30,41,59,0.8)"; + if (globalScale > 1.5 || node.group === "center") { + ctx.fillText(label.slice(0, 30), node.x, node.y + 6); + } + }} + /> +
); @@ -1056,32 +1058,34 @@ function TopicNetworkGraphView({ data }: { data: TopicCitationNetwork }) { 外部
- "rgba(148,163,184,0.25)"} - cooldownTicks={100} - nodeCanvasObjectMode={() => "after"} - nodeCanvasObject={(node: any, ctx: CanvasRenderingContext2D, globalScale: number) => { - if (!node.x || !node.y) return; - const label = node.label || ""; - const isHub = node.group === "hub"; - const fontSize = Math.max((isHub ? 12 : 10) / globalScale, 2); - ctx.font = `${isHub ? "bold " : ""}${fontSize}px Sans-serif`; - ctx.textAlign = "center"; - ctx.textBaseline = "top"; - ctx.fillStyle = isHub ? "#92400e" : "rgba(30,41,59,0.7)"; - if (globalScale > 1.2 || isHub) { - ctx.fillText(label, node.x, node.y + 6); - } - }} - /> +
}> + "rgba(148,163,184,0.25)"} + cooldownTicks={100} + nodeCanvasObjectMode={() => "after"} + nodeCanvasObject={(node: any, ctx: CanvasRenderingContext2D, globalScale: number) => { + if (!node.x || !node.y) return; + const label = node.label || ""; + const isHub = node.group === "hub"; + const fontSize = Math.max((isHub ? 12 : 10) / globalScale, 2); + ctx.font = `${isHub ? "bold " : ""}${fontSize}px Sans-serif`; + ctx.textAlign = "center"; + ctx.textBaseline = "top"; + ctx.fillStyle = isHub ? "#92400e" : "rgba(30,41,59,0.7)"; + if (globalScale > 1.2 || isHub) { + ctx.fillText(label, node.x, node.y + 6); + } + }} + /> +
{data.key_external_papers && data.key_external_papers.length > 0 && ( @@ -1120,7 +1124,7 @@ function CitationTreeView({ data }: { data: CitationTree }) { {data.ancestors.map((edge, i) => { const node = data.nodes.find((n) => n.id === edge.source); return ( -
+
L{edge.depth} {node?.year && {node.year}} @@ -1145,7 +1149,7 @@ function CitationTreeView({ data }: { data: CitationTree }) { {data.descendants.map((edge, i) => { const node = data.nodes.find((n) => n.id === edge.target); return ( -
+
L{edge.depth} {node?.year && {node.year}} diff --git a/frontend/src/components/graph/InsightPanel.tsx b/frontend/src/components/graph/InsightPanel.tsx index 6dc8624..4398b91 100644 --- a/frontend/src/components/graph/InsightPanel.tsx +++ b/frontend/src/components/graph/InsightPanel.tsx @@ -421,7 +421,7 @@ function ResearchGapsContent({ data }: { data: ResearchGapsResponse }) { 识别到 {research_gaps.length} 个研究空白

- {research_gaps.map((gap, i) => )} + {research_gaps.map((gap, i) => )}
)} @@ -445,7 +445,7 @@ function ResearchGapsContent({ data }: { data: ResearchGapsResponse }) { {method_comparison.methods.map((m, i) => ( - + {m.name} {method_comparison.dimensions.map((dim) => ( @@ -463,7 +463,7 @@ function ResearchGapsContent({ data }: { data: ResearchGapsResponse }) {

未被探索的方法组合

    {method_comparison.underexplored_combinations.map((c, i) => ( -
  • +
  • {c}
  • ))} @@ -482,7 +482,7 @@ function ResearchGapsContent({ data }: { data: ResearchGapsResponse }) {

      {trend_analysis.hot_directions.map((d, i) => ( -
    • • {d}
    • +
    • • {d}
    • ))}
@@ -494,7 +494,7 @@ function ResearchGapsContent({ data }: { data: ResearchGapsResponse }) {

    {trend_analysis.declining_areas.map((d, i) => ( -
  • • {d}
  • +
  • • {d}
  • ))}
@@ -506,7 +506,7 @@ function ResearchGapsContent({ data }: { data: ResearchGapsResponse }) {

    {trend_analysis.emerging_opportunities.map((d, i) => ( -
  • • {d}
  • +
  • • {d}
  • ))}
diff --git a/frontend/src/components/graph/OverviewPanel.tsx b/frontend/src/components/graph/OverviewPanel.tsx index 7e116a8..687e9ac 100644 --- a/frontend/src/components/graph/OverviewPanel.tsx +++ b/frontend/src/components/graph/OverviewPanel.tsx @@ -2,15 +2,15 @@ * 全局概览面板 — 统计 / 力导向图 / PageRank / 前沿 / 桥接 / 共引 * @author Color2333 */ -import { useEffect, useRef, useState, useMemo, useCallback } from "react"; +import { useEffect, useRef, useState, useMemo, useCallback, lazy, Suspense } from "react"; import { Link } from "react-router-dom"; import { Badge, Spinner } from "@/components/ui"; import { graphApi } from "@/services/api"; import { useToast } from "@/contexts/ToastContext"; -import ForceGraph2D from "react-force-graph-2d"; +const ForceGraph2D = lazy(() => import("react-force-graph-2d")); import { FileText, Network, Layers, Tag, Share2, Star, Zap, - Compass, RotateCw, + Compass, RotateCw, Loader2, } from "lucide-react"; import type { LibraryOverview, OverviewNode, BridgesResponse, @@ -113,25 +113,28 @@ function OverviewContent({ {/* 全局力导向图 */}
} desc="节点大小 = PageRank 影响力,颜色 = 主题">
- `${n.title}\nPageRank: ${n.pagerank.toFixed(4)}\n引用: ${n.in_degree} 被引: ${n.out_degree}`} - nodeColor={(n: OverviewNode) => { - const t = n.topics[0]; - if (!t) return "#94a3b8"; - const hash = [...t].reduce((a, c) => a + c.charCodeAt(0), 0); - const hues = [210, 150, 30, 330, 270, 60, 0, 180]; - return `hsl(${hues[hash % hues.length]}, 65%, 55%)`; - }} - linkColor={() => "rgba(148,163,184,0.15)"} - linkWidth={0.5} - onNodeClick={(node: OverviewNode) => { window.location.href = `/papers/${node.id}`; }} - cooldownTicks={80} - enableZoomInteraction - /> +
}> + { const node = n as OverviewNode & { val: number }; return `${node.title}\nPageRank: ${node.pagerank.toFixed(4)}\n引用: ${node.in_degree} 被引: ${node.out_degree}`; }} + nodeColor={(n) => { + const node = n as OverviewNode; + const t = node.topics[0]; + if (!t) return "#94a3b8"; + const hash = [...t].reduce((a, c) => a + c.charCodeAt(0), 0); + const hues = [210, 150, 30, 330, 270, 60, 0, 180]; + return `hsl(${hues[hash % hues.length]}, 65%, 55%)`; + }} + linkColor={() => "rgba(148,163,184,0.15)"} + linkWidth={0.5} + onNodeClick={(node) => { window.location.href = `/papers/${(node as OverviewNode).id}`; }} + cooldownTicks={80} + enableZoomInteraction + /> + @@ -215,7 +218,7 @@ function OverviewContent({
} desc="被相同论文引用的研究会自动聚在一起">
{cocitation.clusters.slice(0, 8).map((cl, i) => ( -
+
聚类 {i + 1} {cl.size} 篇论文 diff --git a/frontend/src/contexts/AgentSessionContext.tsx b/frontend/src/contexts/AgentSessionContext.tsx index 0269da3..b5a8de9 100644 --- a/frontend/src/contexts/AgentSessionContext.tsx +++ b/frontend/src/contexts/AgentSessionContext.tsx @@ -79,10 +79,14 @@ const Ctx = createContext(null); export function AgentSessionProvider({ children }: { children: React.ReactNode }) { const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); - const [pendingActions, setPendingActions] = useState>(new Set()); - const [confirmingActions, setConfirmingActions] = useState>(new Set()); + const [pendingActionIds, setPendingActionIds] = useState([]); + const [confirmingActionIds, setConfirmingActionIds] = useState([]); const [canvas, setCanvas] = useState(null); + // 从数组派生 Set,避免每次渲染都创建新对象 + const pendingActions = useMemo(() => new Set(pendingActionIds), [pendingActionIds]); + const confirmingActions = useMemo(() => new Set(confirmingActionIds), [confirmingActionIds]); + const { activeId, createConversation, saveMessages } = useConversationCtx(); const justCreatedRef = useRef(false); const activeIdRef = useRef(activeId); @@ -176,7 +180,7 @@ export function AgentSessionProvider({ children }: { children: React.ReactNode } } else { setItems([]); } - setPendingActions(new Set()); + setPendingActionIds([]); setCanvas(null); }, [activeId]); @@ -227,7 +231,7 @@ export function AgentSessionProvider({ children }: { children: React.ReactNode } }, [items, buildSavePayload, saveMessages]); /* ---- 工具函数 ---- */ - const applyPendingText = (copy: ChatItem[], pendingText: string): void => { + const applyPendingText = useCallback((copy: ChatItem[], pendingText: string): void => { const lastIdx = copy.length - 1; if (lastIdx < 0) { if (pendingText) { @@ -253,7 +257,7 @@ export function AgentSessionProvider({ children }: { children: React.ReactNode } timestamp: new Date(), }); } - }; + }, []); /* ---- SSE 事件处理 ---- */ const processSSE = useCallback( @@ -393,7 +397,7 @@ export function AgentSessionProvider({ children }: { children: React.ReactNode } case "action_confirm": { const pending = drainBuffer(); const actionId = data.id as string; - setPendingActions((prev) => new Set(prev).add(actionId)); + setPendingActionIds((prev) => [...prev, actionId]); setItems((prev) => { const copy = [...prev]; applyPendingText(copy, pending); @@ -554,7 +558,7 @@ export function AgentSessionProvider({ children }: { children: React.ReactNode } } } }, - [scheduleFlush, drainBuffer] + [scheduleFlush, drainBuffer, applyPendingText] ); /** @@ -694,12 +698,8 @@ export function AgentSessionProvider({ children }: { children: React.ReactNode } /* ---- 确认/拒绝操作 ---- */ const handleConfirm = useCallback( async (actionId: string) => { - setConfirmingActions((prev) => new Set(prev).add(actionId)); - setPendingActions((prev) => { - const n = new Set(prev); - n.delete(actionId); - return n; - }); + setConfirmingActionIds((prev) => [...prev, actionId]); + setPendingActionIds((prev) => prev.filter((id) => id !== actionId)); cancelStream(); setLoading(true); try { @@ -720,11 +720,7 @@ export function AgentSessionProvider({ children }: { children: React.ReactNode } ]); setLoading(false); } finally { - setConfirmingActions((prev) => { - const n = new Set(prev); - n.delete(actionId); - return n; - }); + setConfirmingActionIds((prev) => prev.filter((id) => id !== actionId)); } }, [startStream, cancelStream] @@ -732,11 +728,7 @@ export function AgentSessionProvider({ children }: { children: React.ReactNode } const handleReject = useCallback( async (actionId: string) => { - setPendingActions((prev) => { - const n = new Set(prev); - n.delete(actionId); - return n; - }); + setPendingActionIds((prev) => prev.filter((id) => id !== actionId)); cancelStream(); setLoading(true); try { @@ -800,7 +792,6 @@ export function AgentSessionProvider({ children }: { children: React.ReactNode } confirmingActions, canvas, hasPendingConfirm, - setCanvas, sendMessage, handleConfirm, handleReject, diff --git a/frontend/src/contexts/GlobalTaskContext.tsx b/frontend/src/contexts/GlobalTaskContext.tsx index c38dfd2..4b00a16 100644 --- a/frontend/src/contexts/GlobalTaskContext.tsx +++ b/frontend/src/contexts/GlobalTaskContext.tsx @@ -2,7 +2,7 @@ * 全局任务追踪 — 跨页面可见的实时任务进度 * @author Color2333 */ -import { createContext, useContext, useState, useEffect, useCallback, useRef } from "react"; +import { createContext, useContext, useState, useEffect, useCallback, useRef, useMemo } from "react"; import { useToast } from "@/contexts/ToastContext"; export interface ActiveTask { @@ -17,8 +17,21 @@ export interface ActiveTask { finished: boolean; success: boolean; error: string | null; + category?: string; + created_at?: number; } +export const TASK_CATEGORY_CONFIG = { + collection: { icon: "📥", color: "text-blue-500", bg: "bg-blue-500/10", label: "收集" }, + analysis: { icon: "🔬", color: "text-purple-500", bg: "bg-purple-500/10", label: "分析" }, + generation: { icon: "✨", color: "text-amber-500", bg: "bg-amber-500/10", label: "生成" }, + sync: { icon: "🔄", color: "text-green-500", bg: "bg-green-500/10", label: "同步" }, + report: { icon: "📊", color: "text-pink-500", bg: "bg-pink-500/10", label: "报告" }, + general: { icon: "⚙️", color: "text-gray-500", bg: "bg-gray-500/10", label: "通用" }, +} as const; + +export type TaskCategory = keyof typeof TASK_CATEGORY_CONFIG; + interface GlobalTaskCtx { tasks: ActiveTask[]; activeTasks: ActiveTask[]; @@ -85,11 +98,13 @@ export function GlobalTaskProvider({ children }: { children: React.ReactNode }) }; }, [fetchTasks]); - const activeTasks = tasks.filter((t) => !t.finished); + const activeTasks = useMemo(() => tasks.filter((t) => !t.finished), [tasks]); const hasRunning = activeTasks.length > 0; + const value = useMemo(() => ({ tasks, activeTasks, hasRunning }), [tasks, activeTasks, hasRunning]); + return ( - + {children} ); diff --git a/frontend/src/hooks/useAsync.ts b/frontend/src/hooks/useAsync.ts index 23c5eb2..64e4625 100644 --- a/frontend/src/hooks/useAsync.ts +++ b/frontend/src/hooks/useAsync.ts @@ -2,7 +2,7 @@ * 通用异步请求 Hook * @author Color2333 */ -import { useState, useCallback } from "react"; +import { useState, useCallback, useRef, useEffect } from "react"; interface AsyncState { data: T | null; @@ -17,15 +17,27 @@ export function useAsync() { error: null, }); + const mountedRef = useRef(true); + + useEffect(() => { + return () => { + mountedRef.current = false; + }; + }, []); + const execute = useCallback(async (asyncFn: () => Promise) => { setState({ data: null, loading: true, error: null }); try { const result = await asyncFn(); - setState({ data: result, loading: false, error: null }); + if (mountedRef.current) { + setState({ data: result, loading: false, error: null }); + } return result; } catch (err) { const message = err instanceof Error ? err.message : "未知错误"; - setState({ data: null, loading: false, error: message }); + if (mountedRef.current) { + setState({ data: null, loading: false, error: message }); + } throw err; } }, []); @@ -47,17 +59,29 @@ export function useAutoLoad(asyncFn: () => Promise, deps: unknown[] = []) error: null, }); + const mountedRef = useRef(true); + + useEffect(() => { + return () => { + mountedRef.current = false; + }; + }, []); + const reload = useCallback(async () => { - setState(prev => ({ ...prev, loading: true, error: null })); + setState((prev) => ({ ...prev, loading: true, error: null })); try { const result = await asyncFn(); - setState({ data: result, loading: false, error: null }); + if (mountedRef.current) { + setState({ data: result, loading: false, error: null }); + } } catch (err) { const message = err instanceof Error ? err.message : "未知错误"; - setState({ data: null, loading: false, error: message }); + if (mountedRef.current) { + setState({ data: null, loading: false, error: message }); + } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, deps); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [asyncFn, ...deps]); return { ...state, reload }; } diff --git a/frontend/src/pages/Agent.tsx b/frontend/src/pages/Agent.tsx index 462ca40..b7f1745 100644 --- a/frontend/src/pages/Agent.tsx +++ b/frontend/src/pages/Agent.tsx @@ -649,7 +649,7 @@ const PaperListView = memo(function PaperListView({
{papers.slice(0, 30).map((p, i) => (
@@ -757,8 +757,8 @@ const IngestResultView = memo(function IngestResultView({

已入库 ({ingested.length})

- {ingested.map((p, i) => ( -
+ {ingested.map((p) => ( +
{String(p.title ?? p.arxiv_id ?? "")}
@@ -772,9 +772,9 @@ const IngestResultView = memo(function IngestResultView({

失败 ({failed.length})

- {failed.map((p, i) => ( + {failed.map((p) => (
diff --git a/frontend/src/pages/AgentMessages.tsx b/frontend/src/pages/AgentMessages.tsx index e1464ce..497b442 100644 --- a/frontend/src/pages/AgentMessages.tsx +++ b/frontend/src/pages/AgentMessages.tsx @@ -1,12 +1,11 @@ -import { memo, useState, useCallback, useRef, useEffect, useMemo } from "react"; +import { memo, useState, useCallback, useRef, useEffect, useMemo, lazy, Suspense } from "react"; import { useNavigate } from "react-router-dom"; import { cn } from "@/lib/utils"; import { CheckCircle2, XCircle, Loader2, Play, ChevronDown, ChevronRight } from "lucide-react"; -import ReactMarkdown from "react-markdown"; +const ReactMarkdown = lazy(() => import("react-markdown")); import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import rehypeKatex from "rehype-katex"; -import "katex/dist/katex.min.css"; import { type StepItem } from "@/contexts/AgentSessionContext"; import { getToolMeta, StepDataView } from "./AgentSteps"; @@ -53,9 +52,11 @@ const AssistantMessage = memo(function AssistantMessage({ return (
- - {markdownContent} - +
}> + + {markdownContent} + +
{streaming && ( diff --git a/frontend/src/pages/AgentSteps.tsx b/frontend/src/pages/AgentSteps.tsx index 3b33b85..067b667 100644 --- a/frontend/src/pages/AgentSteps.tsx +++ b/frontend/src/pages/AgentSteps.tsx @@ -1,4 +1,4 @@ -import { memo, useState, Suspense } from "react"; +import { memo, useState, Suspense, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { cn } from "@/lib/utils"; import { @@ -53,10 +53,10 @@ function PaperListView({ papers, label }: { papers: Array

{label}

- {papers.slice(0, 5).map((p, i) => ( + {papers.slice(0, 5).map((p) => (
{Array.isArray(data.papers) && (data.papers as Array>).length > 0 && (
- {(data.papers as Array>).slice(0, 3).map((p, i) => ( + {(data.papers as Array>).slice(0, 3).map((p) => ( ))} @@ -343,8 +345,8 @@ const StepDataView = memo(function StepDataView({ data, toolName }: { data: Reco const topics = data.topics as Array>; return (
- {topics.map((t, i) => ( -
+ {topics.map((t) => ( +
{String(t.name ?? "")} {t.paper_count !== undefined && {String(t.paper_count)} 篇} @@ -362,8 +364,8 @@ const StepDataView = memo(function StepDataView({ data, toolName }: { data: Reco const items = data.timeline as Array>; return (
- {items.map((p, i) => ( - @@ -377,8 +379,8 @@ const StepDataView = memo(function StepDataView({ data, toolName }: { data: Reco if (items.length > 0) { return (
- {items.map((p, i) => ( - @@ -402,8 +404,8 @@ const StepDataView = memo(function StepDataView({ data, toolName }: { data: Reco const suggestions = data.suggestions as Array>; return (
- {suggestions.map((s, i) => ( -
+ {suggestions.map((s) => ( +

{String(s.name ?? "")}

{String(s.query ?? "")}

{s.reason !== undefined &&

{String(s.reason)}

} @@ -425,9 +427,9 @@ const StepDataView = memo(function StepDataView({ data, toolName }: { data: Reco const steps = Array.isArray(data.reasoning_steps) ? (data.reasoning_steps as Array>) : []; return (
- {steps.slice(0, 6).map((s, i) => ( -
-

{String(s.step_name ?? s.claim ?? `步骤 ${i + 1}`)}

+ {steps.slice(0, 6).map((s) => ( +
+

{String(s.step_name ?? s.claim ?? "步骤")}

{s.evidence !== undefined &&

{String(s.evidence)}

}
))} @@ -438,8 +440,8 @@ const StepDataView = memo(function StepDataView({ data, toolName }: { data: Reco const figs = data.figures as Array>; return (
- {figs.map((f, i) => ( -
+ {figs.map((f) => ( +

{String(f.figure_type ?? "图表")} — p.{String(f.page ?? "?")}

{String(f.description ?? f.analysis ?? "")}

@@ -452,9 +454,9 @@ const StepDataView = memo(function StepDataView({ data, toolName }: { data: Reco const gaps = Array.isArray(analysis.research_gaps) ? (analysis.research_gaps as Array>) : []; return (
- {gaps.slice(0, 5).map((g, i) => ( -
-

{String(g.gap_title ?? g.title ?? `空白 ${i + 1}`)}

+ {gaps.slice(0, 5).map((g) => ( +
+

{String(g.gap_title ?? g.title ?? "空白")}

{String(g.description ?? g.evidence ?? "")}

))} @@ -482,7 +484,7 @@ const StepDataView = memo(function StepDataView({ data, toolName }: { data: Reco
); } - return
{JSON.stringify(data, null, 2)}
; + return
{jsonPreview}
; }); export { getToolMeta, ArxivCandidateSelector, StepDataView, ActionConfirmCard }; diff --git a/frontend/src/pages/Collect.tsx b/frontend/src/pages/Collect.tsx index b8e4737..dbbfcf5 100644 --- a/frontend/src/pages/Collect.tsx +++ b/frontend/src/pages/Collect.tsx @@ -527,7 +527,7 @@ export default function Collect() {
{results.map((r, i) => ( setResults((prev) => @@ -684,7 +684,8 @@ export default function Collect() {
{suggestions.map((s, i) => (
@@ -1341,7 +1341,7 @@ function ReasoningPanel({ reasoning }: { reasoning: ReasoningChainResult }) {
    {strengths.map((s, i) => (
  • @@ -1359,7 +1359,7 @@ function ReasoningPanel({ reasoning }: { reasoning: ReasoningChainResult }) {
      {weaknesses.map((w, i) => (
    • @@ -1379,7 +1379,7 @@ function ReasoningPanel({ reasoning }: { reasoning: ReasoningChainResult }) {
        {suggestions.map((f, i) => (
      • diff --git a/frontend/src/pages/Wiki.tsx b/frontend/src/pages/Wiki.tsx index 6520db5..6d58174 100644 --- a/frontend/src/pages/Wiki.tsx +++ b/frontend/src/pages/Wiki.tsx @@ -2,7 +2,7 @@ * Wiki - Manus 风格结构化知识百科 * @author Color2333 */ -import { useState, useEffect, useCallback, useRef } from "react"; +import { useState, useEffect, useCallback, useRef, lazy, Suspense } from "react"; import { Card, CardHeader, Button, Tabs, Spinner, Empty } from "@/components/ui"; import { wikiApi, generatedApi, tasksApi } from "@/services/api"; import type { @@ -19,7 +19,7 @@ import type { GeneratedContent, TaskStatus, } from "@/types"; -import Markdown from "@/components/Markdown"; +const Markdown = lazy(() => import("@/components/Markdown")); import { Search, BookOpen, @@ -39,6 +39,7 @@ import { Link2, ExternalLink, Quote, + Loader2, } from "lucide-react"; const wikiTabs = [ @@ -432,21 +433,21 @@ function TopicWikiView({ } />
        - {content.overview} +
        }>{content.overview}
)} {/* 章节 */} {content.sections?.length > 0 && - content.sections.map((sec, idx) => )} + content.sections.map((sec, idx) => )} {/* 方法论演化 */} {content.methodology_evolution && ( } />
- {content.methodology_evolution} +
}>{content.methodology_evolution}
)} @@ -462,7 +463,7 @@ function TopicWikiView({ } />
{content.key_findings.map((finding, i) => ( -
+
{i + 1} @@ -492,7 +493,7 @@ function TopicWikiView({
{timeline.seminal.slice(0, 8).map((s, i) => (
@@ -522,7 +523,7 @@ function TopicWikiView({ const name = typeof stage === "string" ? stage : stage.name; const desc = typeof stage === "string" ? "" : stage.description; return ( -
+
{i + 1}
@@ -552,7 +553,7 @@ function TopicWikiView({ />
{content.future_directions.map((dir, i) => ( -
+

{dir}

@@ -567,7 +568,7 @@ function TopicWikiView({ } />
{survey.summary.open_questions.map((q: string, i: number) => ( -
+
Q{i + 1}

{q}

@@ -582,7 +583,7 @@ function TopicWikiView({ } />
{content.reading_list.map((item, i) => ( - + ))}
@@ -623,7 +624,7 @@ function PaperWikiView({ } />
- {content.summary} +
}>{content.summary}
)} @@ -634,7 +635,7 @@ function PaperWikiView({ } />
{content.contributions.map((c, i) => ( -
+
{i + 1} @@ -650,7 +651,7 @@ function PaperWikiView({ } />
- {content.methodology} +
}>{content.methodology}
)} @@ -663,7 +664,7 @@ function PaperWikiView({ action={} />
- {content.significance} +
}>{content.significance}
)} @@ -699,7 +700,7 @@ function PaperWikiView({ } />
{content.limitations.map((lim, i) => ( -
+

{lim}

@@ -713,7 +714,7 @@ function PaperWikiView({
- {content.related_work_analysis} +
}>{content.related_work_analysis}
)} @@ -724,7 +725,7 @@ function PaperWikiView({ } />
{content.reading_suggestions.map((item, i) => ( - + ))}
@@ -753,7 +754,7 @@ function SectionCard({ section, index }: { section: WikiSection; index: number }
)}
- {section.content} +
}>{section.content}
); @@ -768,14 +769,14 @@ function CitationContextsCard({ contexts }: { contexts: string[] }) { description="论文之间的引用语境" action={} /> -
- {contexts.slice(0, 15).map((ctx, i) => ( -
- -

{ctx}

+
+ {contexts.slice(0, 15).map((ctx, i) => ( +
+ +

{ctx}

+
+ ))}
- ))} -
); } @@ -792,10 +793,11 @@ function PdfExcerptsCard({ excerpts }: { excerpts: PdfExcerpt[] }) { />
{excerpts.map((ex, i) => ( -
+

{ex.title}