-
-
- 权限确认
-
- {req.tool_name}
-
-
+
+ {activeRequests.map(req => {
+ const hasArgs = Object.keys(req.arguments || {}).length > 0;
+ const showArgs = expandedParams[req.permission_id] || false;
-
{req.message}
-
- {/* 显示工具参数的简化预览 */}
- {Object.keys(req.arguments).length > 0 && (
-
- {JSON.stringify(req.arguments, null, 2)}
-
- )}
-
-
-
- ))}
+ );
+ })}
);
}
+
+
diff --git a/dashboard/src/features/agent/ResearchAgentPanel.tsx b/dashboard/src/features/agent/ResearchAgentPanel.tsx
index ca57526..8d347d4 100644
--- a/dashboard/src/features/agent/ResearchAgentPanel.tsx
+++ b/dashboard/src/features/agent/ResearchAgentPanel.tsx
@@ -1,17 +1,11 @@
// dashboard/src/features/agent/ResearchAgentPanel.tsx
import { useState, useEffect, useRef } from 'react';
import axios from 'axios';
-import ReactMarkdown from 'react-markdown';
-import remarkMath from 'remark-math';
-import remarkGfm from 'remark-gfm';
-import rehypeRaw from 'rehype-raw';
-import rehypeKatex from 'rehype-katex';
-import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
import 'katex/dist/katex.min.css';
import {
- Brain, Settings, Eye, CheckCircle2, AlertTriangle,
+ Brain, CheckCircle2, AlertTriangle,
Send, Loader, Plus, Trash2, Compass, Clock, Square,
- BarChart3, ScrollText, Network, Rewind, RotateCcw,
+ BarChart3, ScrollText, Rewind, RotateCcw,
GitBranch, RefreshCw, Search, X, MessageSquare, BookOpen,
PanelLeftClose, PanelLeftOpen, Paperclip
} from 'lucide-react';
@@ -19,6 +13,12 @@ import { AskUserQuestionCard } from './AskUserQuestionCard';
import { PermissionRequestCard } from './PermissionRequestCard';
import { AgentMetricsPanel } from './AgentMetricsPanel';
import { AuditLogViewer } from './AuditLogViewer';
+import {
+ AgentMarkdown, ThoughtCard, ToolCallCard, AnswerCard, SubAgentContainer,
+ PARENT_COLORS, useAutoScroll,
+ routeThought, routeToolCall, routeToolResult, routeTextDelta,
+} from '../../components/agent';
+import type { TimelineItem, ColorScheme } from '../../components/agent';
interface SessionSummary {
session_id: string;
title: string;
@@ -62,15 +62,7 @@ interface ToolCall {
};
}
-// ── 时间线条目:思考 / 工具调用 / 最终答案 / 子代理容器 ──
-type TimelineItem =
- | { type: 'thought'; step: number; content: string }
- | { type: 'tool_call'; step: number; id: string; name: string; arguments: any; result?: { output: string; isError: boolean } }
- | { type: 'answer'; content: string }
- | { type: 'subagent_container'; id: string; status: 'streaming' | 'complete'; children: TimelineItem[]; summary?: string };
-
-// 颜色方案:父代理 vs 子代理(时间线圆点颜色不同)
-type ColorScheme = 'parent' | 'subagent';
+// (colorScheme 类型和 TimelineItem 均从 ../../components/agent 导入)
interface ActiveTurn {
question: string;
@@ -120,17 +112,6 @@ interface RetryResult {
image_path?: string | null;
}
-const safeSchema = {
- ...defaultSchema,
- attributes: {
- ...defaultSchema.attributes,
- '*': (defaultSchema.attributes?.['*'] || []).concat(['className', 'style', 'mathvariant', 'display']),
- },
- tagNames: (defaultSchema.tagNames || []).concat([
- 'math', 'mrow', 'mi', 'mo', 'mn', 'msup', 'msub', 'msubsup', 'mfrac', 'mover', 'munder', 'munderover', 'mspace', 'mtext', 'annotation'
- ]),
-};
-
const QUICK_PROMPTS = [
"检索 2023 年以后关于热亚矮星 (Subdwarf) 双星演化的研究成果",
"读取文献 2024ApJ...960..123A 并总结其核心观测结论",
@@ -138,53 +119,6 @@ const QUICK_PROMPTS = [
"查询天体 GD 358 的物理参数"
];
-// 辅助函数:根据工具名称获取人类可读中文名称
-function getToolDisplayName(name: string): string {
- switch (name) {
- // 文件系统工具
- case 'read_file': return '读取文件内容';
- case 'grep_files': return '正则搜索文件';
- case 'glob_files': return '通配符匹配文件';
- case 'run_bash': return '执行 Shell 命令';
- case 'file_write': return '写入文件';
- case 'file_edit': return '精确编辑文件';
- // 天文科研工具
- case 'search_papers': return '检索 ADS/arXiv 文献';
- case 'get_paper_metadata': return '获取文献详细元数据';
- case 'download_paper': return '下载文献全文资源';
- case 'parse_paper': return '结构化解析文献内容';
- case 'get_paper_content': return '获取文献全文内容';
- case 'rag_search': return '检索馆藏知识库';
- case 'query_target': return '查询天体物理参数 (CDS)';
- case 'save_note': return '保存文献手札';
- // Agent 控制工具
- case 'todo_write': return '管理任务列表';
- case 'compress_context': return '压缩上下文窗口';
- case 'load_skill': return '加载专家技能';
- case 'subagent': return '派发子代理';
- case 'ask_user': return '向用户提问';
- // 记忆系统
- case 'save_memory': return '保存项目记忆';
- case 'load_memory': return '读取项目记忆';
- // 后台任务
- case 'bg_task_run': return '启动后台任务';
- case 'bg_task_check': return '检查后台任务状态';
- // 团队协作
- case 'spawn_teammate': return '创建团队成员';
- case 'send_teammate_message': return '发送团队成员消息';
- case 'team_broadcast': return '团队广播消息';
- case 'check_team_inbox': return '检查团队收件箱';
- default: {
- // 子代理工具名带 [sub] 前缀
- if (name.startsWith('[sub] ')) {
- const inner = name.slice(6);
- return `子代理: ${getToolDisplayName(inner)}`;
- }
- return name;
- }
- }
-}
-
interface AgentMode {
id: string;
name: string;
@@ -202,6 +136,8 @@ function getIconComponent(iconName: string): React.ComponentType<{ className?: s
return ICON_MAP[iconName] || Brain;
}
+
+
interface ResearchAgentPanelProps {
showConfirm?: (message: string, onConfirm: () => void, title?: string) => void;
showAlert?: (message: string, title?: string) => void;
@@ -265,40 +201,14 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane
// 新功能面板状态
const [showMetrics, setShowMetrics] = useState(false);
const [showAuditLog, setShowAuditLog] = useState(false);
+ const [hasPendingPermission, setHasPendingPermission] = useState(false);
+ const [hasPendingQuestion, setHasPendingQuestion] = useState(false);
- const chatEndRef = useRef
(null);
- const scrollContainerRef = useRef(null);
- const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
const skipNextHistoryLoadRef = useRef(false);
- const scrollToBottom = () => {
- chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
- };
-
- useEffect(() => {
- if (shouldAutoScroll) {
- scrollToBottom();
- }
- }, [messages, activeTurn, streaming, shouldAutoScroll]);
-
- const handleScroll = () => {
- if (!scrollContainerRef.current) return;
- const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
- // 距离底部 50 像素以内视为贴紧底部
- const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
- setShouldAutoScroll(isAtBottom);
- };
-
- // 查找最后一个正在流式收集中间步骤的 subagent_container 索引
- const findStreamingSubAgent = (timeline: TimelineItem[]): number => {
- for (let i = timeline.length - 1; i >= 0; i--) {
- const t = timeline[i];
- if (t.type === 'subagent_container' && t.status === 'streaming') {
- return i;
- }
- }
- return -1;
- };
+ const {
+ chatEndRef, scrollContainerRef, setShouldAutoScroll, scrollToBottom, handleScroll,
+ } = useAutoScroll([messages, activeTurn, streaming]);
// 获取会话列表
const fetchSessions = async (autoSelectFirst = false) => {
@@ -318,7 +228,7 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane
};
useEffect(() => {
- fetchSessions(true);
+ fetchSessions(false);
const fetchModes = async () => {
try {
@@ -343,6 +253,7 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane
if (!silent) {
setLoadingHistory(true);
}
+ setShouldAutoScroll(true);
try {
const res = await axios.get<{ session: SessionSummary; messages: MessageRecord[] }>(`/api/chat/sessions/${sessionId}`);
const newMessages = res.data.messages;
@@ -356,6 +267,8 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane
onSuccess();
}
setMessages(newMessages);
+ // 加载历史后瞬时跳到底部(无动画)
+ requestAnimationFrame(() => scrollToBottom('auto'));
// 自动迁移流式工具调用的展开/折叠状态至历史视图
// streaming 和 history 现在使用相同的 LLM tool_call_id,无需转换
@@ -729,214 +642,48 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane
case 'thought':
setActiveTurn(prev => {
if (!prev) return prev;
- // 子代理思考 → 路由到活跃的 subagent_container 内部
- if (event.content.startsWith('[子代理] ')) {
- const subContent = event.content.slice(6); // 去掉 "[子代理] " 前缀
- const saIdx = findStreamingSubAgent(prev.timeline);
- if (saIdx >= 0) {
- const container = prev.timeline[saIdx] as Extract;
- const existing = container.children.find(
- c => c.type === 'thought' && c.step === event.step
- );
- let newChildren: TimelineItem[];
- if (existing) {
- newChildren = container.children.map(c =>
- c.type === 'thought' && c.step === event.step
- ? { ...c, content: subContent } as TimelineItem
- : c
- );
- } else {
- newChildren = [...container.children, { type: 'thought', step: event.step, content: subContent }];
- }
- const newTimeline = [...prev.timeline];
- newTimeline[saIdx] = { ...container, children: newChildren };
- return { ...prev, timeline: newTimeline };
- }
- // 无活跃容器时 fallback 为普通 thought
- return { ...prev, timeline: [...prev.timeline, { type: 'thought', step: event.step, content: subContent }] };
- }
- // 父代理思考(正常路径)
- const existing = prev.timeline.find(
- t => t.type === 'thought' && t.step === event.step
- );
- if (existing) {
- return {
- ...prev,
- timeline: prev.timeline.map(t =>
- t.type === 'thought' && t.step === event.step
- ? { ...t, content: event.content }
- : t
- ),
- };
- }
- return {
- ...prev,
- timeline: [...prev.timeline, { type: 'thought', step: event.step, content: event.content }],
- };
+ return { ...prev, timeline: routeThought(prev.timeline, event.step, event.content) };
});
break;
case 'tool_call':
setActiveTurn(prev => {
if (!prev) return prev;
- const tcId = event.id;
- // subagent → 创建 subagent_container(作为时间线条目,收集子代理内部步骤)
- if (event.name === 'subagent') {
- const existing = prev.timeline.find(
- t => t.type === 'subagent_container' && t.id === tcId
- );
- if (existing) return prev;
- return {
- ...prev,
- timeline: [...prev.timeline, {
- type: 'subagent_container' as const,
- id: tcId,
- status: 'streaming' as const,
- children: [],
- }],
- };
- }
- // 子代理工具调用 → 路由到活跃容器
- if (event.name.startsWith('[sub] ')) {
- const cleanName = event.name.slice(6); // 去掉 "[sub] " 前缀
- const saIdx = findStreamingSubAgent(prev.timeline);
- if (saIdx >= 0) {
- const container = prev.timeline[saIdx] as Extract;
- const dup = container.children.find(
- c => c.type === 'tool_call' && 'id' in c && c.id === tcId
- );
- if (dup) return prev;
- const newTimeline = [...prev.timeline];
- newTimeline[saIdx] = {
- ...container,
- children: [...container.children, {
- type: 'tool_call' as const,
- step: event.step,
- id: tcId,
- name: cleanName,
- arguments: event.arguments,
- }],
- };
- return { ...prev, timeline: newTimeline };
- }
- // 无活跃容器时 fallback 为普通 tool_call
- return {
- ...prev,
- timeline: [...prev.timeline, {
- type: 'tool_call' as const,
- step: event.step,
- id: tcId,
- name: cleanName,
- arguments: event.arguments,
- }],
- };
- }
- // 父代理工具调用(正常路径)
- const existing = prev.timeline.find(
- t => t.type === 'tool_call' && 'id' in t && t.id === tcId
- );
- if (existing) return prev;
return {
...prev,
- timeline: [...prev.timeline, {
- type: 'tool_call' as const,
- step: event.step,
- id: tcId,
- name: event.name,
- arguments: event.arguments,
- }],
+ timeline: routeToolCall(prev.timeline, event.step, event.id, event.name, event.arguments),
};
});
break;
case 'tool_result':
- // 自动展开结果区域
setExpandedResults(prev => ({ ...prev, [event.tool_call_id]: true }));
setActiveTurn(prev => {
if (!prev) return prev;
- const tcId = event.tool_call_id;
- // 检查是否匹配 subagent → 完成 subagent_container
- const saIdx = prev.timeline.findIndex(
- t => t.type === 'subagent_container' && t.id === tcId
- );
- if (saIdx >= 0) {
- const container = prev.timeline[saIdx] as Extract;
- const newTimeline = [...prev.timeline];
- newTimeline[saIdx] = { ...container, status: 'complete' as const, summary: event.output };
- return { ...prev, timeline: newTimeline };
- }
- // 子代理工具结果 → 路由到活跃容器
- if (event.name.startsWith('[sub] ')) {
- const activeSaIdx = findStreamingSubAgent(prev.timeline);
- if (activeSaIdx >= 0) {
- const container = prev.timeline[activeSaIdx] as Extract;
- const newTimeline = [...prev.timeline];
- newTimeline[activeSaIdx] = {
- ...container,
- children: container.children.map(c => {
- if (c.type === 'tool_call' && c.id === tcId && !c.result) {
- return { ...c, result: { output: event.output, isError: event.is_error } };
- }
- return c;
- }),
- };
- return { ...prev, timeline: newTimeline };
- }
- }
- // 父代理工具结果(正常路径)
- // 始终更新 result,即使已有流式输出的部分内容——最终 tool_result 是完整答案
return {
...prev,
- timeline: prev.timeline.map(t => {
- if (t.type === 'tool_call' && t.id === tcId) {
- return {
- ...t,
- result: { output: event.output, isError: event.is_error },
- };
- }
- return t;
- }),
+ timeline: routeToolResult(prev.timeline, event.tool_call_id, event.name, event.output, event.is_error),
};
});
break;
case 'text_delta':
- // 如果带 tool_call_id,自动展开对应工具的结果区域以显示流式输出
if (event.tool_call_id) {
setExpandedResults(prev => ({ ...prev, [event.tool_call_id]: true }));
}
setActiveTurn(prev => {
if (!prev) return prev;
- // 如果带有 tool_call_id,流式输出到对应工具的结果区域
if (event.tool_call_id) {
- const tcId = event.tool_call_id;
- // 检查 timeline 中是否有匹配的 tool_call
+ // 检查 timeline 中是否有匹配的 tool_call(TTL 保护)
const hasToolCall = prev.timeline.some(
- t => t.type === 'tool_call' && 'id' in t && t.id === tcId
+ t => t.type === 'tool_call' && 'id' in t && t.id === event.tool_call_id,
);
- if (hasToolCall) {
- return {
- ...prev,
- timeline: prev.timeline.map(t => {
- if (t.type === 'tool_call' && t.id === tcId) {
- const prevOutput = t.result?.output || '';
- return {
- ...t,
- result: {
- output: prevOutput + event.content,
- isError: false,
- },
- };
- }
- return t;
- }),
- };
- }
- // 工具调用尚未出现(TTL 场景),暂存到 finalAnswer 不处理
- // 等 tool_call + tool_result 出现后,tool_result 会覆盖
- return prev;
+ if (!hasToolCall) return prev;
}
- // 无 tool_call_id → 主回答文本流
+ const { timeline, answerDelta } = routeTextDelta(
+ prev.timeline, event.content, event.tool_call_id,
+ );
return {
...prev,
- finalAnswer: prev.finalAnswer + event.content,
+ timeline,
+ finalAnswer: prev.finalAnswer + answerDelta,
};
});
break;
@@ -995,247 +742,72 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane
const toggleResult = (tcId: string) => setExpandedResults(prev => ({ ...prev, [tcId]: !prev[tcId] }));
const toggleSubAgent = (id: string) => setCollapsedSubAgents(prev => ({ ...prev, [id]: !prev[id] }));
- // ── 子代理容器渲染 ──
-
- const renderSubAgentContainer = (
- item: Extract,
+ // ── 渲染时间线条目(委托到共享组件)──
+ const renderTimelineItem = (
+ item: TimelineItem,
idx: number,
isStreaming: boolean,
+ colors: ColorScheme = PARENT_COLORS,
) => {
- const isComplete = item.status === 'complete';
- const isCollapsed = collapsedSubAgents[item.id] !== false; // 默认折叠
- const stepCount = item.children.length;
- const toolCount = item.children.filter(c => c.type === 'tool_call').length;
-
- return (
-
-
-
- {/* 头部 */}
-
toggleSubAgent(item.id)}
- className="w-full flex items-center justify-between px-3.5 py-2.5 cursor-pointer hover:bg-violet-50/80 transition-colors"
- >
-
-
-
- {isStreaming ? '子代理执行中...' : '子代理完成'}
-
- {isComplete && (
-
-
- {stepCount} 步骤 · {toolCount} 工具
-
- )}
- {isStreaming && (
-
- )}
-
-
- {isCollapsed ? '展开' : '收起'}
-
-
-
- {/* 折叠态摘要 */}
- {isCollapsed && item.summary && (
-
-
- {item.summary.length > 200
- ? item.summary.slice(0, 200) + '...'
- : item.summary}
-
-
- )}
- {isCollapsed && !item.summary && isStreaming && (
-
- )}
-
- {/* 展开态:嵌套时间线 */}
- {!isCollapsed && (
-
- {item.children.length === 0 && isStreaming && (
-
-
- 子代理初始化中...
-
- )}
-
- {item.children.map((child, childIdx) =>
- renderTimelineItem(child, childIdx, isStreaming, 'subagent')
- )}
-
-
- )}
-
-
- );
- };
-
- // ── 时间线条目渲染辅助函数 ──
- // 每个条目独立渲染:思考卡片 / 工具调用卡片 / 答案卡片 / 子代理容器
- const renderTimelineItem = (item: TimelineItem, idx: number, isStreaming: boolean, colorScheme: ColorScheme = 'parent') => {
- // 颜色方案:子代理使用 violet 色调
- const dotColors = colorScheme === 'subagent'
- ? { thought: { active: 'bg-violet-400 animate-pulse', idle: 'bg-violet-300' },
- tool_call: { active: 'bg-violet-500 animate-pulse', idle: 'bg-violet-400' },
- answer: 'bg-violet-400' }
- : { thought: { active: 'bg-purple-400 animate-pulse', idle: 'bg-purple-300' },
- tool_call: { active: 'bg-sky-500 animate-pulse', idle: 'bg-sky-400' },
- answer: 'bg-emerald-400' };
-
switch (item.type) {
- // ── 子代理容器 ──
case 'subagent_container':
- return renderSubAgentContainer(item, idx, isStreaming);
- // ── 思考卡片(默认折叠,只显示预览) ──
+ return (
+ toggleSubAgent(item.id)}
+ expandedThoughts={expandedThoughts}
+ expandedArgs={expandedArgs}
+ expandedResults={expandedResults}
+ onToggleThought={toggleThought}
+ onToggleArgs={toggleArgs}
+ onToggleResult={toggleResult}
+ />
+ );
case 'thought': {
const thoughtKey = `thought-${item.step}`;
- const isExpanded = expandedThoughts[thoughtKey] === true;
- const preview = item.content.length > 120 ? item.content.slice(0, 120) + '…' : item.content;
-
return (
-
-
-
-
toggleThought(thoughtKey)}
- className="w-full flex items-center justify-between text-left cursor-pointer"
- >
-
-
- 分析与推理 (Thought)
- · Step {item.step}
-
-
- {isExpanded ? '收起' : '展开'}
-
-
- {isExpanded ? (
-
- {item.content}
-
- ) : (
-
- {preview}
-
- )}
-
-
+ toggleThought(thoughtKey)}
+ />
);
}
-
- // ── 工具调用卡片 ──
case 'tool_call': {
const tcId = item.id;
- const isArgsExpanded = expandedArgs[tcId] === true;
- const isResultExpanded = expandedResults[tcId] === true;
- const hasResult = !!item.result;
-
return (
-
-
-
- {/* 工具名称 */}
-
-
-
- 调用工具: {getToolDisplayName(item.name)}
- · Step {item.step}
-
-
toggleArgs(tcId)}
- className="text-[10px] font-bold text-sky-600 hover:text-sky-800 hover:underline cursor-pointer flex items-center gap-0.5"
- >
- {isArgsExpanded ? '收起参数' : '展开参数'}
-
-
-
- {/* 参数 */}
- {isArgsExpanded && (
-
- {JSON.stringify(item.arguments, null, 2)}
-
- )}
-
- {/* 结果 */}
- {hasResult && (
-
-
-
-
- 观测与反馈 (Observation)
- {item.result!.isError && (
- 错误返回
- )}
-
-
toggleResult(tcId)}
- className="text-[10px] font-bold text-sky-600 hover:text-sky-800 hover:underline cursor-pointer flex items-center gap-0.5"
- >
- {isResultExpanded ? '收起结果' : '展开结果'}
-
-
- {isResultExpanded ? (
-
-
- {item.result!.output}
-
-
- ) : (
-
- 结果已截断 (共 {item.result!.output.length} 字符)。点击右侧展开。
-
- )}
-
- )}
-
-
+ toggleArgs(tcId)}
+ onToggleResult={() => toggleResult(tcId)}
+ />
);
}
-
- // ── 答案卡片 ──
case 'answer':
return (
-
-
-
-
-
- 结论
-
-
-
- {item.content}
-
-
-
-
+
);
-
default:
return null;
}
@@ -1250,7 +822,7 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane
- {!sidebarCollapsed && 智能科研研讨}
+ {!sidebarCollapsed && 会话历史}
)}
- {/* 交互式问答 / 权限确认 — 固定在聊天区域上方,确保用户始终能看到 */}
-
-
{
- if (currentSessionId) {
- loadSessionHistory(currentSessionId, true);
- }
- }}
- />
- {currentSessionId && (
-
- )}
-
-
{/* 消息滚动区域 */}
{loadingHistory ? (
@@ -1596,7 +1156,7 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane
{/* 当前提问 */}
我
-
+
{activeTurn.imagePath && (

)}
@@ -1618,13 +1178,8 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane
科研智能体输出最终结论
-
-
- {activeTurn.finalAnswer}
-
+
+
{activeTurn.finalAnswer}
)}
@@ -1688,145 +1243,164 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane
- {/* 底部输入框 */}
-