Files
AIclinicalresearch/frontend-v2/src/modules/asl/components/deep-research/AgentTerminal.tsx
HaHafeng 8f06d4f929 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>
2026-02-23 13:21:52 +08:00

166 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;