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>
166 lines
5.2 KiB
TypeScript
166 lines
5.2 KiB
TypeScript
/**
|
||
* 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;
|