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:
@@ -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;
|
||||
Reference in New Issue
Block a user