/** * Deep Research V2.0 主页面 — 瀑布流布局 * * Phase 0: Landing(全屏居中搜索) * Phase 1+: 配置 -> 策略 -> 执行 -> 结果,依次累积展示 * * 支持从 URL 参数 :taskId 恢复历史任务。 */ import { useState, useCallback, useRef, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Card, Button, Typography, message } from 'antd'; import { CheckCircleFilled, DownloadOutlined, PlusOutlined, ClockCircleOutlined, CloseCircleFilled } from '@ant-design/icons'; import { getAccessToken } from '../../../framework/auth/api'; import { aslApi } from '../api'; import { useDeepResearchTask } from '../hooks/useDeepResearchTask'; import { useASLLayout } from '../components/ASLLayout'; import LandingView from '../components/deep-research/LandingView'; import SetupPanel from '../components/deep-research/SetupPanel'; import StrategyConfirm from '../components/deep-research/StrategyConfirm'; import AgentTerminal from '../components/deep-research/AgentTerminal'; import ResultsView from '../components/deep-research/ResultsView'; import type { IntentSummary, GenerateRequirementResponse } from '../types/deepResearch'; type Phase = 0 | 1 | 2 | 3 | 4; const DeepResearchPage = () => { const { taskId: urlTaskId } = useParams<{ taskId?: string }>(); const navigate = useNavigate(); const { refreshHistory } = useASLLayout(); const [phase, setPhase] = useState(0); const [query, setQuery] = useState(''); const [taskId, setTaskId] = useState(null); const [generatedRequirement, setGeneratedRequirement] = useState(''); const [intentSummary, setIntentSummary] = useState(null); const [generating, setGenerating] = useState(false); const [restoredFromUrl, setRestoredFromUrl] = useState(false); const strategyRef = useRef(null); const terminalRef = useRef(null); const resultsRef = useRef(null); // 轮询 hook(phase >= 3 或从 URL 恢复的任务) const shouldPoll = phase >= 3 || (restoredFromUrl && !!taskId); const { task, isRunning, isCompleted, isFailed } = useDeepResearchTask({ taskId, enabled: shouldPoll, }); // 从 URL :taskId 恢复历史任务,或 URL 失去 taskId 时重置 useEffect(() => { if (urlTaskId && urlTaskId !== taskId) { setTaskId(urlTaskId); setRestoredFromUrl(true); } else if (!urlTaskId && taskId) { setPhase(0); setTaskId(null); setQuery(''); setGeneratedRequirement(''); setIntentSummary(null); setRestoredFromUrl(false); } }, [urlTaskId]); // eslint-disable-line react-hooks/exhaustive-deps // 恢复的任务加载完成后设置正确的 phase useEffect(() => { if (!restoredFromUrl || !task) return; setQuery(task.query || ''); if (task.confirmedRequirement) { setGeneratedRequirement(task.confirmedRequirement); } if (task.aiIntentSummary) { setIntentSummary(task.aiIntentSummary); } if (task.status === 'completed') { setPhase(4); } else if (task.status === 'running' || task.status === 'pending') { setPhase(3); } else if (task.status === 'failed') { setPhase(4); } else if (task.status === 'draft') { setPhase(2); } }, [restoredFromUrl, task]); // 正常流程中:执行完成自动进入结果页 useEffect(() => { if (isCompleted && phase === 3 && !restoredFromUrl) { setPhase(4); setTimeout(() => resultsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 150); } }, [isCompleted, phase, restoredFromUrl]); const scrollTo = (ref: React.RefObject) => { setTimeout(() => ref.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 150); }; const handleLandingSubmit = useCallback((q: string) => { setQuery(q); setPhase(1); setGeneratedRequirement(''); setIntentSummary(null); setTaskId(null); setRestoredFromUrl(false); }, []); const handleSetupSubmit = useCallback(async ( originalQuery: string, selectedSources: string[], filters: { yearRange?: string; targetCount?: string } ) => { setGenerating(true); try { const res = await aslApi.generateRequirement({ originalQuery, targetSources: selectedSources, filters, }); const data = res.data as GenerateRequirementResponse; setTaskId(data.taskId); setGeneratedRequirement(data.generatedRequirement); setIntentSummary(data.intentSummary); setPhase(2); scrollTo(strategyRef); } catch (err: any) { message.error(err.message || '需求扩写失败'); } finally { setGenerating(false); } }, []); const handleStrategyConfirm = useCallback(async (confirmedReq: string) => { if (!taskId) return; try { await aslApi.executeDeepResearchTask(taskId, confirmedReq); setPhase(3); scrollTo(terminalRef); message.success('Deep Research 已启动'); // 任务进入 pending 后刷新侧边栏历史列表 navigate(`/literature/research/deep/${taskId}`, { replace: true }); refreshHistory(); } catch (err: any) { message.error(err.message || '启动失败'); } }, [taskId, navigate, refreshHistory]); const handleNewSearch = useCallback(() => { setPhase(0); setQuery(''); setTaskId(null); setGeneratedRequirement(''); setIntentSummary(null); setRestoredFromUrl(false); navigate('/literature/research/deep', { replace: true }); window.scrollTo({ top: 0, behavior: 'smooth' }); }, [navigate]); // ── 从 URL 恢复的任务:直接展示结果或执行中状态 ── if (restoredFromUrl && task) { const isFinished = task.status === 'completed' || task.status === 'failed'; return (
{/* 顶部横幅:完成/失败状态 + 导出按钮 */} {isFinished && ( )} {!isFinished && (
「{task.query}」 — 深度检索执行中...
)} {/* 检索需求书(PICOS + 完整指令) */} {task.confirmedRequirement && task.aiIntentSummary && (
{}} readOnly />
)} {/* 执行日志 */}
{/* 结果区(报告 + 文献表格),横幅已在顶部展示 */} {isFinished && (
)}
); } // 恢复中但 task 还未加载 — loading if (restoredFromUrl && !task) { return (
加载中...
); } // ── 正常新建流程 ── if (phase === 0) { return (
); } return (
{/* Section 1: Setup */} setPhase(0)} loading={generating} collapsed={phase >= 2} onExpand={phase === 2 ? () => setPhase(1) : undefined} /> {/* Section 2: Strategy */} {phase >= 2 && (
= 3} />
)} {/* Section 3: Executing */} {phase >= 3 && (
)} {/* Section 4: Results */} {phase >= 4 && task && (
)}
); }; // ── 历史记录顶部横幅(完成/失败 + 导出) ── const { Text } = Typography; function HistoryBanner({ task, onNewSearch }: { task: any; onNewSearch: () => void }) { const [exporting, setExporting] = useState(false); const isCompleted = task.status === 'completed'; const handleExportWord = async () => { setExporting(true); try { const token = getAccessToken(); const res = await fetch(`/api/v1/asl/research/tasks/${task.taskId}/export-word`, { headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const blob = await res.blob(); const disposition = res.headers.get('Content-Disposition') || ''; const filenameMatch = disposition.match(/filename\*?=(?:UTF-8'')?([^;]+)/); const filename = filenameMatch ? decodeURIComponent(filenameMatch[1]) : 'DeepResearch.docx'; const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); message.success('导出成功'); } catch (err: any) { message.error(err.message || '导出失败'); } finally { setExporting(false); } }; if (!isCompleted) { return (
检索失败 「{task.query}」
); } return (
Deep Research 完成 「{task.query}」 — {task.resultCount || 0} 篇文献 {task.completedAt && ` · ${new Date(task.completedAt).toLocaleString()}`}
); } export default DeepResearchPage;