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:
1315
frontend-v2/package-lock.json
generated
1315
frontend-v2/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,9 @@
|
||||
"prismjs": "^1.30.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^7.9.5",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"xlsx": "^0.18.5",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
|
||||
@@ -991,9 +991,7 @@
|
||||
.message-bubble .markdown-content ol {
|
||||
margin: 8px 0 12px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.message-bubble .markdown-content ul {
|
||||
}.message-bubble .markdown-content ul {
|
||||
list-style-type: disc;
|
||||
}.message-bubble .markdown-content ol {
|
||||
list-style-type: decimal;
|
||||
@@ -1020,4 +1018,4 @@
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,6 +478,56 @@ export async function getResearchTaskStatus(
|
||||
return request(`/research/tasks/${taskId}/status`);
|
||||
}
|
||||
|
||||
// ==================== Deep Research V2.0 API ====================
|
||||
|
||||
import type {
|
||||
DataSourceConfig,
|
||||
GenerateRequirementRequest,
|
||||
GenerateRequirementResponse,
|
||||
DeepResearchTask,
|
||||
} from '../types/deepResearch';
|
||||
|
||||
/**
|
||||
* 获取数据源配置列表
|
||||
*/
|
||||
export async function getDeepResearchDataSources(): Promise<ApiResponse<DataSourceConfig[]>> {
|
||||
return request('/research/data-sources');
|
||||
}
|
||||
|
||||
/**
|
||||
* 需求扩写(PICOS + MeSH)
|
||||
*/
|
||||
export async function generateRequirement(
|
||||
data: GenerateRequirementRequest
|
||||
): Promise<ApiResponse<GenerateRequirementResponse>> {
|
||||
return request('/research/generate-requirement', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动异步执行
|
||||
*/
|
||||
export async function executeDeepResearchTask(
|
||||
taskId: string,
|
||||
confirmedRequirement: string
|
||||
): Promise<ApiResponse<void>> {
|
||||
return request(`/research/tasks/${taskId}/execute`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ confirmedRequirement }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 V2.0 任务详情(状态 + 日志 + 结果)
|
||||
*/
|
||||
export async function getDeepResearchTask(
|
||||
taskId: string
|
||||
): Promise<ApiResponse<DeepResearchTask>> {
|
||||
return request(`/research/tasks/${taskId}`);
|
||||
}
|
||||
|
||||
// ==================== 统一导出API对象 ====================
|
||||
|
||||
/**
|
||||
@@ -525,7 +575,13 @@ export const aslApi = {
|
||||
// 健康检查
|
||||
healthCheck,
|
||||
|
||||
// 智能文献检索 (DeepSearch)
|
||||
// 智能文献检索 (DeepSearch V1.x)
|
||||
createResearchTask,
|
||||
getResearchTaskStatus,
|
||||
|
||||
// Deep Research V2.0
|
||||
getDeepResearchDataSources,
|
||||
generateRequirement,
|
||||
executeDeepResearchTask,
|
||||
getDeepResearchTask,
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ const ASLLayout = () => {
|
||||
title: '敬请期待'
|
||||
},
|
||||
{
|
||||
key: '/literature/research/search',
|
||||
key: '/literature/research/deep',
|
||||
icon: <SearchOutlined />,
|
||||
label: '2. 智能文献检索',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Deep Research V2.0 — Agent Terminal
|
||||
*
|
||||
* 暗色终端风格展示 AI 思考与执行过程:
|
||||
* - 每条日志展示完整思考内容
|
||||
* - 自动过滤无意义内容(任务 ID 等)
|
||||
* - 条件 auto-scroll
|
||||
*/
|
||||
|
||||
import { useRef, useEffect, useCallback, useState } from 'react';
|
||||
import { Spin, Typography, Alert } from 'antd';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import type { DeepResearchTask, ExecutionLogEntry } from '../../types/deepResearch';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface AgentTerminalProps {
|
||||
task: DeepResearchTask | null;
|
||||
isRunning: boolean;
|
||||
isFailed: boolean;
|
||||
}
|
||||
|
||||
const LOG_COLORS: Record<string, string> = {
|
||||
thinking: '#8b949e',
|
||||
searching: '#58a6ff',
|
||||
reading: '#d2a8ff',
|
||||
analyzing: '#7ee787',
|
||||
summary: '#ffa657',
|
||||
info: '#79c0ff',
|
||||
};
|
||||
|
||||
const LOG_PREFIXES: Record<string, string> = {
|
||||
thinking: '🤔 思考',
|
||||
searching: '🔍 搜索',
|
||||
reading: '📖 阅读',
|
||||
analyzing: '🧪 分析',
|
||||
summary: '📊 总结',
|
||||
info: 'ℹ️ 信息',
|
||||
};
|
||||
|
||||
const AgentTerminal: React.FC<AgentTerminalProps> = ({ task, isRunning, isFailed }) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [userScrolled, setUserScrolled] = useState(false);
|
||||
const prevLogCountRef = useRef(0);
|
||||
|
||||
const logs = (task?.executionLogs || []).filter(
|
||||
(log: ExecutionLogEntry) => !log.text.includes('Unifuncs 任务 ID')
|
||||
);
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
|
||||
setUserScrolled(distanceFromBottom > 50);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userScrolled && containerRef.current && logs.length > prevLogCountRef.current) {
|
||||
containerRef.current.scrollTop = containerRef.current.scrollHeight;
|
||||
}
|
||||
prevLogCountRef.current = logs.length;
|
||||
}, [logs.length, userScrolled]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
{isRunning && <Spin indicator={<LoadingOutlined spin />} size="small" />}
|
||||
<Text strong className="text-lg">
|
||||
{isRunning ? 'Deep Research 执行中...' : isFailed ? '执行失败' : '执行完成'}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{isFailed && task?.errorMessage && (
|
||||
<Alert
|
||||
type="error"
|
||||
message="执行失败"
|
||||
description={task.errorMessage}
|
||||
className="mb-4"
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={containerRef}
|
||||
onScroll={handleScroll}
|
||||
className="rounded-xl overflow-hidden"
|
||||
style={{
|
||||
background: '#0d1117',
|
||||
color: '#c9d1d9',
|
||||
fontFamily: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace",
|
||||
fontSize: 13,
|
||||
lineHeight: 1.8,
|
||||
padding: '16px 20px',
|
||||
maxHeight: 500,
|
||||
minHeight: 200,
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 mb-4 pb-3" style={{ borderBottom: '1px solid #21262d' }}>
|
||||
<span style={{ width: 12, height: 12, borderRadius: '50%', background: '#ff5f56', display: 'inline-block' }} />
|
||||
<span style={{ width: 12, height: 12, borderRadius: '50%', background: '#ffbd2e', display: 'inline-block' }} />
|
||||
<span style={{ width: 12, height: 12, borderRadius: '50%', background: '#27c93f', display: 'inline-block' }} />
|
||||
<span className="ml-3" style={{ color: '#8b949e' }}>Deep Research Agent</span>
|
||||
</div>
|
||||
|
||||
{/* Log entries */}
|
||||
{logs.map((log: ExecutionLogEntry, i: number) => (
|
||||
<div key={i} className="mb-3 pb-2" style={{ borderBottom: '1px solid #161b22' }}>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span style={{ color: LOG_COLORS[log.type] || '#c9d1d9', fontWeight: 600, fontSize: 12 }}>
|
||||
{LOG_PREFIXES[log.type] || log.title}
|
||||
</span>
|
||||
<span style={{ color: '#484f58', fontSize: 11 }}>
|
||||
{new Date(log.ts).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ color: '#c9d1d9', paddingLeft: 4, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||
{log.text}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Blinking cursor */}
|
||||
{isRunning && (
|
||||
<span className="inline-block mt-1" style={{
|
||||
width: 8,
|
||||
height: 16,
|
||||
background: '#58a6ff',
|
||||
animation: 'blink 1s step-end infinite',
|
||||
}} />
|
||||
)}
|
||||
|
||||
{logs.length === 0 && (
|
||||
<div style={{ color: '#484f58' }}>等待任务启动...</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{userScrolled && isRunning && (
|
||||
<div className="text-center mt-2">
|
||||
<Text
|
||||
type="secondary"
|
||||
className="text-xs cursor-pointer hover:text-blue-400 transition-colors"
|
||||
onClick={() => {
|
||||
setUserScrolled(false);
|
||||
if (containerRef.current) {
|
||||
containerRef.current.scrollTop = containerRef.current.scrollHeight;
|
||||
}
|
||||
}}
|
||||
>
|
||||
↓ 回到底部查看最新日志
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style>{`
|
||||
@keyframes blink {
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentTerminal;
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Deep Research V2.0 — Landing 视图
|
||||
*
|
||||
* 大搜索框 + 推荐预置词,居中展示。
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Input, Button, Tag } from 'antd';
|
||||
import { SearchOutlined, ExperimentOutlined } from '@ant-design/icons';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface LandingViewProps {
|
||||
onSubmit: (query: string) => void;
|
||||
}
|
||||
|
||||
const PRESET_QUERIES = [
|
||||
'他汀类药物在心血管疾病一级预防中的最新RCT证据',
|
||||
'PD-1/PD-L1抑制剂联合化疗治疗非小细胞肺癌的Meta分析',
|
||||
'糖尿病肾病患者SGLT2抑制剂的肾脏保护作用',
|
||||
'阿尔茨海默病早期诊断生物标志物研究进展',
|
||||
];
|
||||
|
||||
const LandingView: React.FC<LandingViewProps> = ({ onSubmit }) => {
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
const handleSubmit = () => {
|
||||
const trimmed = query.trim();
|
||||
if (!trimmed) return;
|
||||
onSubmit(trimmed);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center" style={{ minHeight: 'calc(100vh - 64px)' }}>
|
||||
<div className="text-center mb-8">
|
||||
<ExperimentOutlined className="text-5xl text-blue-500 mb-4" />
|
||||
<h1 className="text-3xl font-bold text-gray-800 mb-2">Deep Research</h1>
|
||||
<p className="text-gray-500 text-base">AI 驱动的深度文献检索,输入您的研究想法</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="w-full bg-white rounded-2xl shadow-md border border-gray-200 p-5 flex flex-col gap-3"
|
||||
style={{ maxWidth: 700 }}
|
||||
>
|
||||
<TextArea
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="输入您的研究想法,例如:他汀类药物预防心血管疾病的系统评价..."
|
||||
autoSize={{ minRows: 3, maxRows: 6 }}
|
||||
className="!border-none !shadow-none !text-base !resize-none"
|
||||
onPressEnter={(e) => {
|
||||
if (!e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SearchOutlined />}
|
||||
size="large"
|
||||
onClick={handleSubmit}
|
||||
disabled={!query.trim()}
|
||||
className="!rounded-lg !px-6"
|
||||
>
|
||||
开始
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8" style={{ maxWidth: 700, width: '100%' }}>
|
||||
<p className="text-gray-400 text-sm mb-3">试试这些:</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{PRESET_QUERIES.map((q, i) => (
|
||||
<Tag
|
||||
key={i}
|
||||
className="cursor-pointer !text-sm !py-1 !px-3 !rounded-full hover:!bg-blue-50 hover:!border-blue-300 transition-colors"
|
||||
onClick={() => {
|
||||
setQuery(q);
|
||||
}}
|
||||
>
|
||||
{q}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LandingView;
|
||||
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* Deep Research V2.0 — 结果展示
|
||||
*
|
||||
* 完成横幅 + AI 综合报告(Markdown 正式渲染)+ 文献清单表格
|
||||
* 降级展示:如果 resultList 为 null,仅展示报告
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Card, Table, Button, Typography, Tag, Divider, Empty, message } from 'antd';
|
||||
import {
|
||||
CheckCircleFilled,
|
||||
FileTextOutlined,
|
||||
TableOutlined,
|
||||
PlusOutlined,
|
||||
DownloadOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { getAccessToken } from '../../../../framework/auth/api';
|
||||
import type { DeepResearchTask, LiteratureItem } from '../../types/deepResearch';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface ResultsViewProps {
|
||||
task: DeepResearchTask;
|
||||
onNewSearch: () => void;
|
||||
}
|
||||
|
||||
const ResultsView: React.FC<ResultsViewProps> = ({ task, onNewSearch }) => {
|
||||
const { synthesisReport, resultList, resultCount, query, completedAt, taskId } = task;
|
||||
const [exporting, setExporting] = useState(false);
|
||||
|
||||
const handleExportWord = async () => {
|
||||
setExporting(true);
|
||||
try {
|
||||
const token = getAccessToken();
|
||||
const res = await fetch(`/api/v1/asl/research/tasks/${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);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '#',
|
||||
key: 'index',
|
||||
width: 50,
|
||||
render: (_: any, __: any, idx: number) => idx + 1,
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
ellipsis: true,
|
||||
render: (text: string, record: LiteratureItem) => (
|
||||
record.url ? (
|
||||
<a href={record.url} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
||||
{text || record.url}
|
||||
</a>
|
||||
) : (
|
||||
<span>{text || '—'}</span>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '作者',
|
||||
dataIndex: 'authors',
|
||||
key: 'authors',
|
||||
width: 160,
|
||||
ellipsis: true,
|
||||
render: (t: string) => t || '—',
|
||||
},
|
||||
{
|
||||
title: '期刊',
|
||||
dataIndex: 'journal',
|
||||
key: 'journal',
|
||||
width: 140,
|
||||
ellipsis: true,
|
||||
render: (t: string) => t || '—',
|
||||
},
|
||||
{
|
||||
title: '年份',
|
||||
dataIndex: 'year',
|
||||
key: 'year',
|
||||
width: 70,
|
||||
render: (t: number | string) => t || '—',
|
||||
},
|
||||
{
|
||||
title: '研究类型',
|
||||
dataIndex: 'studyType',
|
||||
key: 'studyType',
|
||||
width: 100,
|
||||
render: (t: string) => t ? <Tag color="blue">{t}</Tag> : '—',
|
||||
},
|
||||
{
|
||||
title: 'PMID',
|
||||
dataIndex: 'pmid',
|
||||
key: 'pmid',
|
||||
width: 100,
|
||||
render: (t: string) => t ? (
|
||||
<a
|
||||
href={`https://pubmed.ncbi.nlm.nih.gov/${t}/`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500"
|
||||
>
|
||||
{t}
|
||||
</a>
|
||||
) : '—',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 完成横幅 */}
|
||||
<Card className="mb-6 !bg-green-50 !border-green-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircleFilled className="text-2xl text-green-500" />
|
||||
<div>
|
||||
<Text strong className="text-lg block">Deep Research 完成</Text>
|
||||
<Text type="secondary" className="text-sm">
|
||||
「{query}」 — 找到 {resultCount || 0} 篇文献
|
||||
{completedAt && ` · ${new Date(completedAt).toLocaleString()}`}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button icon={<DownloadOutlined />} loading={exporting} onClick={handleExportWord}>
|
||||
导出 Word
|
||||
</Button>
|
||||
<Button icon={<PlusOutlined />} onClick={onNewSearch}>
|
||||
新建检索
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* AI 综合报告 — Markdown 渲染 */}
|
||||
{synthesisReport && (
|
||||
<Card
|
||||
className="mb-6"
|
||||
title={<><FileTextOutlined className="mr-2" />AI 综合分析报告</>}
|
||||
>
|
||||
<div className="prose prose-sm max-w-none prose-headings:text-base prose-headings:font-semibold prose-a:text-blue-600 prose-a:no-underline hover:prose-a:underline">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
a: ({ href, children }) => (
|
||||
<>
|
||||
<a href={href} target="_blank" rel="noopener noreferrer">
|
||||
{children}
|
||||
</a>
|
||||
{href && (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="!text-gray-400 !text-xs !ml-1 hover:!text-blue-500 !no-underline"
|
||||
title="点击打开链接"
|
||||
>
|
||||
{href}
|
||||
</a>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{synthesisReport}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 文献清单表格 */}
|
||||
{resultList && resultList.length > 0 ? (
|
||||
<Card
|
||||
title={<><TableOutlined className="mr-2" />文献清单({resultList.length} 篇)</>}
|
||||
>
|
||||
<Table
|
||||
dataSource={resultList}
|
||||
columns={columns}
|
||||
rowKey={(_, idx) => String(idx)}
|
||||
size="small"
|
||||
pagination={{ pageSize: 20, showSizeChanger: true }}
|
||||
scroll={{ x: 900 }}
|
||||
/>
|
||||
</Card>
|
||||
) : (
|
||||
synthesisReport ? (
|
||||
<Card>
|
||||
<Empty
|
||||
description="AI 未能结构化提取文献列表,请查阅上方综合报告中的文献引用"
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
/>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<Empty description="无结果数据" />
|
||||
</Card>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResultsView;
|
||||
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* Deep Research V2.0 — 配置面板
|
||||
*
|
||||
* 数据源 Checkbox + 高级筛选 + 生成需求书按钮
|
||||
* 支持 collapsed 模式显示为摘要卡片
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card, Checkbox, Button, Input, Select, Spin, Divider, Typography, Tag } from 'antd';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
ThunderboltOutlined,
|
||||
GlobalOutlined,
|
||||
EditOutlined,
|
||||
CheckCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { aslApi } from '../../api';
|
||||
import type { DataSourceConfig } from '../../types/deepResearch';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface SetupPanelProps {
|
||||
initialQuery: string;
|
||||
onSubmit: (
|
||||
query: string,
|
||||
selectedSources: string[],
|
||||
filters: { yearRange?: string; targetCount?: string }
|
||||
) => void;
|
||||
onBack: () => void;
|
||||
loading: boolean;
|
||||
collapsed?: boolean;
|
||||
onExpand?: () => void;
|
||||
}
|
||||
|
||||
const LOADING_TEXTS = [
|
||||
'AI 正在理解您的研究意图...',
|
||||
'正在进行 PICOS 结构化拆解...',
|
||||
'正在匹配 MeSH 医学术语...',
|
||||
'正在生成检索指令书...',
|
||||
];
|
||||
|
||||
const SetupPanel: React.FC<SetupPanelProps> = ({
|
||||
initialQuery, onSubmit, onBack, loading, collapsed, onExpand,
|
||||
}) => {
|
||||
const [query, setQuery] = useState(initialQuery);
|
||||
const [dataSources, setDataSources] = useState<DataSourceConfig[]>([]);
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
const [yearRange, setYearRange] = useState<string>('近5年');
|
||||
const [targetCount, setTargetCount] = useState<string>('全面检索');
|
||||
const [loadingSources, setLoadingSources] = useState(true);
|
||||
const [loadingTextIdx, setLoadingTextIdx] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
aslApi.getDeepResearchDataSources().then(res => {
|
||||
const sources = res.data || [];
|
||||
setDataSources(sources);
|
||||
setSelectedIds(sources.filter((s: DataSourceConfig) => s.defaultChecked).map((s: DataSourceConfig) => s.id));
|
||||
}).catch(() => {
|
||||
setDataSources([]);
|
||||
}).finally(() => {
|
||||
setLoadingSources(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) { setLoadingTextIdx(0); return; }
|
||||
const timer = setInterval(() => {
|
||||
setLoadingTextIdx(prev => (prev + 1) % LOADING_TEXTS.length);
|
||||
}, 3000);
|
||||
return () => clearInterval(timer);
|
||||
}, [loading]);
|
||||
|
||||
const handleToggle = (id: string) => {
|
||||
setSelectedIds(prev =>
|
||||
prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
const domains = dataSources
|
||||
.filter(s => selectedIds.includes(s.id))
|
||||
.map(s => s.domainScope);
|
||||
onSubmit(query, domains, { yearRange, targetCount });
|
||||
};
|
||||
|
||||
const selectedNames = dataSources.filter(s => selectedIds.includes(s.id)).map(s => s.label);
|
||||
|
||||
if (collapsed) {
|
||||
return (
|
||||
<Card size="small" className="!bg-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<CheckCircleOutlined className="text-green-500 text-lg flex-shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<Text strong className="block truncate">{query}</Text>
|
||||
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
||||
{selectedNames.map(name => (
|
||||
<Tag key={name} className="!text-xs !m-0">{name}</Tag>
|
||||
))}
|
||||
<Text type="secondary" className="text-xs">{yearRange} · {targetCount}</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{onExpand && (
|
||||
<Button type="link" icon={<EditOutlined />} onClick={onExpand} className="flex-shrink-0">
|
||||
修改
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const englishSources = dataSources.filter(s => s.category === 'english');
|
||||
const chineseSources = dataSources.filter(s => s.category === 'chinese');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Button type="text" icon={<ArrowLeftOutlined />} onClick={onBack} size="small">
|
||||
返回
|
||||
</Button>
|
||||
<Text strong className="text-lg">配置检索参数</Text>
|
||||
</div>
|
||||
|
||||
<Card className="mb-4" size="small">
|
||||
<Text type="secondary" className="block mb-2 text-xs">研究想法</Text>
|
||||
<Input.TextArea
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||
className="!text-base"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card className="mb-4" size="small" title={<><GlobalOutlined className="mr-2" />选择数据源</>}>
|
||||
{loadingSources ? (
|
||||
<Spin size="small" />
|
||||
) : (
|
||||
<>
|
||||
<Text type="secondary" className="block mb-3 text-xs">英文数据库</Text>
|
||||
<div className="flex flex-col gap-2 mb-4">
|
||||
{englishSources.map(ds => (
|
||||
<Checkbox
|
||||
key={ds.id}
|
||||
checked={selectedIds.includes(ds.id)}
|
||||
onChange={() => handleToggle(ds.id)}
|
||||
>
|
||||
<span className="font-medium">{ds.label}</span>
|
||||
<span className="text-gray-400 text-xs ml-2">{ds.domainScope}</span>
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Divider className="!my-3" />
|
||||
|
||||
<Text type="secondary" className="block mb-3 text-xs">中文数据库</Text>
|
||||
<div className="flex flex-col gap-2">
|
||||
{chineseSources.map(ds => (
|
||||
<Checkbox
|
||||
key={ds.id}
|
||||
checked={selectedIds.includes(ds.id)}
|
||||
onChange={() => handleToggle(ds.id)}
|
||||
>
|
||||
<span className="font-medium">{ds.label}</span>
|
||||
<span className="text-gray-400 text-xs ml-2">{ds.domainScope}</span>
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card className="mb-6" size="small" title="高级筛选">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1">
|
||||
<Text type="secondary" className="block mb-1 text-xs">时间范围</Text>
|
||||
<Select
|
||||
value={yearRange}
|
||||
onChange={setYearRange}
|
||||
className="w-full"
|
||||
options={[
|
||||
{ value: '不限', label: '不限' },
|
||||
{ value: '近1年', label: '近1年' },
|
||||
{ value: '近3年', label: '近3年' },
|
||||
{ value: '近5年', label: '近5年' },
|
||||
{ value: '近10年', label: '近10年' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Text type="secondary" className="block mb-1 text-xs">目标数量</Text>
|
||||
<Select
|
||||
value={targetCount}
|
||||
onChange={setTargetCount}
|
||||
className="w-full"
|
||||
options={[
|
||||
{ value: '全面检索', label: '全面检索' },
|
||||
{ value: '约20篇', label: '约20篇' },
|
||||
{ value: '约50篇', label: '约50篇' },
|
||||
{ value: '约100篇', label: '约100篇' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ThunderboltOutlined />}
|
||||
size="large"
|
||||
block
|
||||
onClick={handleSubmit}
|
||||
loading={loading}
|
||||
disabled={!query.trim() || selectedIds.length === 0}
|
||||
>
|
||||
{loading ? LOADING_TEXTS[loadingTextIdx] : '生成检索需求书'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupPanel;
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Deep Research V2.0 — 检索指令确认
|
||||
*
|
||||
* 单列布局:PICOS 内联摘要 + 可编辑检索指令书 + 保存/启动按钮
|
||||
* 支持 collapsed 模式显示为已确认卡片
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Card, Button, Input, Tag, Typography, Divider, message } from 'antd';
|
||||
import {
|
||||
RocketOutlined,
|
||||
SaveOutlined,
|
||||
CheckCircleOutlined,
|
||||
AimOutlined,
|
||||
TagsOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { IntentSummary } from '../../types/deepResearch';
|
||||
|
||||
const { Text } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface StrategyConfirmProps {
|
||||
generatedRequirement: string;
|
||||
intentSummary: IntentSummary | null;
|
||||
onConfirm: (confirmedRequirement: string) => void;
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
const StrategyConfirm: React.FC<StrategyConfirmProps> = ({
|
||||
generatedRequirement,
|
||||
intentSummary,
|
||||
onConfirm,
|
||||
collapsed,
|
||||
}) => {
|
||||
const [editedRequirement, setEditedRequirement] = useState(generatedRequirement);
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
if (collapsed) {
|
||||
return (
|
||||
<Card size="small" className="!bg-white">
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircleOutlined className="text-green-500 text-lg flex-shrink-0" />
|
||||
<Text strong>检索指令已确认,正在执行深度检索</Text>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
setSaved(true);
|
||||
message.success('检索指令已保存');
|
||||
setTimeout(() => setSaved(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text strong className="text-lg block mb-4">确认检索指令</Text>
|
||||
|
||||
{intentSummary && (
|
||||
<Card size="small" className="mb-4 !bg-blue-50/50">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<AimOutlined className="text-blue-500" />
|
||||
<Text className="text-sm">{intentSummary.objective}</Text>
|
||||
</div>
|
||||
<Divider className="!my-2" />
|
||||
<div className="flex flex-wrap gap-x-6 gap-y-1 text-xs mb-2">
|
||||
<span><Text type="secondary">P 人群</Text> <Text className="ml-1">{intentSummary.population || '—'}</Text></span>
|
||||
<span><Text type="secondary">I 干预</Text> <Text className="ml-1">{intentSummary.intervention || '—'}</Text></span>
|
||||
<span><Text type="secondary">C 对照</Text> <Text className="ml-1">{intentSummary.comparison || '—'}</Text></span>
|
||||
<span><Text type="secondary">O 结局</Text> <Text className="ml-1">{intentSummary.outcome || '—'}</Text></span>
|
||||
</div>
|
||||
{intentSummary.meshTerms && intentSummary.meshTerms.length > 0 && (
|
||||
<div className="flex items-center gap-1 flex-wrap mt-1">
|
||||
<TagsOutlined className="text-green-500 text-xs" />
|
||||
{intentSummary.meshTerms.map((term, i) => (
|
||||
<Tag key={i} color="green" className="!text-xs !m-0">{term}</Tag>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{intentSummary.studyDesign && intentSummary.studyDesign.length > 0 && (
|
||||
<div className="flex items-center gap-1 flex-wrap mt-1">
|
||||
{intentSummary.studyDesign.map((s, i) => (
|
||||
<Tag key={i} color="blue" className="!text-xs !m-0">{s}</Tag>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card size="small" className="mb-4">
|
||||
<TextArea
|
||||
value={editedRequirement}
|
||||
onChange={(e) => { setEditedRequirement(e.target.value); setSaved(false); }}
|
||||
autoSize={{ minRows: 8, maxRows: 20 }}
|
||||
className="!text-sm !leading-relaxed"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSave}
|
||||
disabled={saved}
|
||||
>
|
||||
{saved ? '已保存' : '保存修改'}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<RocketOutlined />}
|
||||
size="large"
|
||||
className="flex-1"
|
||||
onClick={() => onConfirm(editedRequirement)}
|
||||
disabled={!editedRequirement.trim()}
|
||||
>
|
||||
启动 Deep Research
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StrategyConfirm;
|
||||
48
frontend-v2/src/modules/asl/hooks/useDeepResearchTask.ts
Normal file
48
frontend-v2/src/modules/asl/hooks/useDeepResearchTask.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Deep Research V2.0 任务轮询 Hook
|
||||
*
|
||||
* running 时 3s 轮询,completed/failed 停止。
|
||||
*/
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { aslApi } from '../api';
|
||||
|
||||
interface UseDeepResearchTaskOptions {
|
||||
taskId: string | null;
|
||||
enabled?: boolean;
|
||||
pollingInterval?: number;
|
||||
}
|
||||
|
||||
export function useDeepResearchTask({
|
||||
taskId,
|
||||
enabled = true,
|
||||
pollingInterval = 3000,
|
||||
}: UseDeepResearchTaskOptions) {
|
||||
const { data, isLoading, error, refetch } = useQuery({
|
||||
queryKey: ['deep-research-task', taskId],
|
||||
queryFn: () => aslApi.getDeepResearchTask(taskId!),
|
||||
enabled: enabled && !!taskId,
|
||||
refetchInterval: (query) => {
|
||||
const task = query.state.data?.data;
|
||||
if (!task) return pollingInterval;
|
||||
if (task.status === 'completed' || task.status === 'failed') {
|
||||
return false;
|
||||
}
|
||||
return pollingInterval;
|
||||
},
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
const task = data?.data || null;
|
||||
|
||||
return {
|
||||
task,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
isRunning: task?.status === 'running' || task?.status === 'pending',
|
||||
isCompleted: task?.status === 'completed',
|
||||
isFailed: task?.status === 'failed',
|
||||
isDraft: task?.status === 'draft',
|
||||
};
|
||||
}
|
||||
@@ -22,6 +22,9 @@ const FulltextResults = lazy(() => import('./pages/FulltextResults'));
|
||||
// 智能文献检索页面
|
||||
const ResearchSearch = lazy(() => import('./pages/ResearchSearch'));
|
||||
|
||||
// Deep Research V2.0
|
||||
const DeepResearchPage = lazy(() => import('./pages/DeepResearchPage'));
|
||||
|
||||
const ASLModule = () => {
|
||||
return (
|
||||
<Suspense
|
||||
@@ -35,8 +38,11 @@ const ASLModule = () => {
|
||||
<Route path="" element={<ASLLayout />}>
|
||||
<Route index element={<Navigate to="screening/title/settings" replace />} />
|
||||
|
||||
{/* 智能文献检索 */}
|
||||
{/* 智能文献检索 V1.x(保留兼容) */}
|
||||
<Route path="research/search" element={<ResearchSearch />} />
|
||||
|
||||
{/* Deep Research V2.0 */}
|
||||
<Route path="research/deep" element={<DeepResearchPage />} />
|
||||
|
||||
{/* 标题摘要初筛 */}
|
||||
<Route path="screening/title">
|
||||
|
||||
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;
|
||||
76
frontend-v2/src/modules/asl/types/deepResearch.ts
Normal file
76
frontend-v2/src/modules/asl/types/deepResearch.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Deep Research V2.0 类型定义
|
||||
*/
|
||||
|
||||
export interface DataSourceConfig {
|
||||
id: string;
|
||||
label: string;
|
||||
labelEn: string;
|
||||
domainScope: string;
|
||||
category: 'english' | 'chinese';
|
||||
defaultChecked: boolean;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface IntentSummary {
|
||||
objective: string;
|
||||
population: string;
|
||||
intervention: string;
|
||||
comparison: string;
|
||||
outcome: string;
|
||||
studyDesign: string[];
|
||||
meshTerms: string[];
|
||||
condition: string;
|
||||
}
|
||||
|
||||
export interface ExecutionLogEntry {
|
||||
type: 'thinking' | 'searching' | 'reading' | 'analyzing' | 'summary' | 'info';
|
||||
title: string;
|
||||
text: string;
|
||||
ts: string;
|
||||
}
|
||||
|
||||
export interface LiteratureItem {
|
||||
title: string;
|
||||
authors?: string;
|
||||
journal?: string;
|
||||
year?: number | string;
|
||||
doi?: string;
|
||||
pmid?: string;
|
||||
url?: string;
|
||||
abstract?: string;
|
||||
studyType?: string;
|
||||
}
|
||||
|
||||
export interface DeepResearchTask {
|
||||
taskId: string;
|
||||
status: 'draft' | 'pending' | 'running' | 'completed' | 'failed';
|
||||
query: string;
|
||||
targetSources: string[] | null;
|
||||
confirmedRequirement: string | null;
|
||||
aiIntentSummary: IntentSummary | null;
|
||||
executionLogs: ExecutionLogEntry[];
|
||||
synthesisReport: string | null;
|
||||
resultList: LiteratureItem[] | null;
|
||||
resultCount: number | null;
|
||||
errorMessage: string | null;
|
||||
createdAt: string;
|
||||
completedAt: string | null;
|
||||
}
|
||||
|
||||
export interface GenerateRequirementRequest {
|
||||
originalQuery: string;
|
||||
targetSources?: string[];
|
||||
filters?: {
|
||||
yearRange?: string;
|
||||
targetCount?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GenerateRequirementResponse {
|
||||
taskId: string;
|
||||
generatedRequirement: string;
|
||||
intentSummary: IntentSummary;
|
||||
}
|
||||
|
||||
export type DeepResearchStep = 'landing' | 'setup' | 'strategy' | 'executing' | 'results';
|
||||
Reference in New Issue
Block a user