feat(asl): Complete Deep Research V2.0 core development
Backend: - Add SSE streaming client (unifuncsSseClient) replacing async polling - Add paragraph-based reasoning parser with mergeConsecutiveThinking - Add requirement expansion service (DeepSeek-V3 PICOS+MeSH) - Add Word export service with Pandoc, inline hyperlinks, reference link expansion - Add deep research V2 worker with 2s log flush and Chinese source prompt - Add 5 curated data sources config (PubMed/ClinicalTrials/Cochrane/CNKI/MedJournals) - Add 4 API endpoints (generate-requirement/tasks/task-status/export-word) - Update Prisma schema with 6 new V2.0 fields on AslResearchTask - Add DB migration for V2.0 fields - Simplify ASL_DEEP_RESEARCH_EXPANSION prompt (remove strategy section) Frontend: - Add waterfall-flow DeepResearchPage (phase 0-4 progressive reveal) - Add LandingView, SetupPanel, StrategyConfirm, AgentTerminal, ResultsView - Add react-markdown + remark-gfm for report rendering - Add custom link component showing visible URLs after references - Add useDeepResearchTask polling hook - Add deep research TypeScript types Tests: - Add E2E test, smoke test, and Chinese data source test scripts Docs: - Update ASL module status (v2.0 - core features complete) - Update system status (v6.1 - ASL V2.0 milestone) - Update Unifuncs DeepSearch API guide (v2.0 - SSE mode + Chinese source results) - Update module auth specification (test script guidelines) - Update V2.0 development plan Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
161
frontend-v2/src/modules/asl/pages/DeepResearchPage.tsx
Normal file
161
frontend-v2/src/modules/asl/pages/DeepResearchPage.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Deep Research V2.0 主页面 — 瀑布流布局
|
||||
*
|
||||
* Phase 0: Landing(全屏居中搜索)
|
||||
* Phase 1+: 配置 → 策略 → 执行 → 结果,依次累积展示
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { message } from 'antd';
|
||||
import { aslApi } from '../api';
|
||||
import { useDeepResearchTask } from '../hooks/useDeepResearchTask';
|
||||
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 [phase, setPhase] = useState<Phase>(0);
|
||||
const [query, setQuery] = useState('');
|
||||
const [taskId, setTaskId] = useState<string | null>(null);
|
||||
const [generatedRequirement, setGeneratedRequirement] = useState('');
|
||||
const [intentSummary, setIntentSummary] = useState<IntentSummary | null>(null);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
|
||||
const strategyRef = useRef<HTMLDivElement>(null);
|
||||
const terminalRef = useRef<HTMLDivElement>(null);
|
||||
const resultsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { task, isRunning, isCompleted, isFailed } = useDeepResearchTask({
|
||||
taskId,
|
||||
enabled: phase >= 3,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isCompleted && phase === 3) {
|
||||
setPhase(4);
|
||||
setTimeout(() => resultsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 150);
|
||||
}
|
||||
}, [isCompleted, phase]);
|
||||
|
||||
const scrollTo = (ref: React.RefObject<HTMLDivElement | null>) => {
|
||||
setTimeout(() => ref.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 150);
|
||||
};
|
||||
|
||||
const handleLandingSubmit = useCallback((q: string) => {
|
||||
setQuery(q);
|
||||
setPhase(1);
|
||||
setGeneratedRequirement('');
|
||||
setIntentSummary(null);
|
||||
setTaskId(null);
|
||||
}, []);
|
||||
|
||||
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 已启动');
|
||||
} catch (err: any) {
|
||||
message.error(err.message || '启动失败');
|
||||
}
|
||||
}, [taskId]);
|
||||
|
||||
const handleNewSearch = useCallback(() => {
|
||||
setPhase(0);
|
||||
setQuery('');
|
||||
setTaskId(null);
|
||||
setGeneratedRequirement('');
|
||||
setIntentSummary(null);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}, []);
|
||||
|
||||
if (phase === 0) {
|
||||
return (
|
||||
<div className="h-full overflow-auto bg-gray-50">
|
||||
<LandingView onSubmit={handleLandingSubmit} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto bg-gray-50 pb-16">
|
||||
<div className="max-w-5xl mx-auto px-6 pt-6">
|
||||
{/* Section 1: Setup */}
|
||||
<SetupPanel
|
||||
initialQuery={query}
|
||||
onSubmit={handleSetupSubmit}
|
||||
onBack={() => setPhase(0)}
|
||||
loading={generating}
|
||||
collapsed={phase >= 2}
|
||||
onExpand={phase === 2 ? () => setPhase(1) : undefined}
|
||||
/>
|
||||
|
||||
{/* Section 2: Strategy */}
|
||||
{phase >= 2 && (
|
||||
<div ref={strategyRef} className="mt-6">
|
||||
<StrategyConfirm
|
||||
generatedRequirement={generatedRequirement}
|
||||
intentSummary={intentSummary}
|
||||
onConfirm={handleStrategyConfirm}
|
||||
collapsed={phase >= 3}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Section 3: Executing */}
|
||||
{phase >= 3 && (
|
||||
<div ref={terminalRef} className="mt-6">
|
||||
<AgentTerminal
|
||||
task={task}
|
||||
isRunning={isRunning}
|
||||
isFailed={isFailed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Section 4: Results */}
|
||||
{phase >= 4 && task && (
|
||||
<div ref={resultsRef} className="mt-6">
|
||||
<ResultsView
|
||||
task={task}
|
||||
onNewSearch={handleNewSearch}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeepResearchPage;
|
||||
Reference in New Issue
Block a user