AstroResearch/dashboard/src/components/agent/ToolCallCard.tsx
Asfmq c5fd5b0d66 refactor: 全栈质量硬化
后端:
  - 权限系统重写: 全局→按 Session 隔离, 新增规则查询 API
  - 安全加固: 登录 IP 限流, Token 仅存 Cookie, bibcode 白名单校验
  - SSE 超时保护, 异步 I/O 迁移, 10+ 处静默 DB 错误改为显式日志
  - ar5iv 下标解析修复, parse_paper_row 去重, 优雅关闭

  前端:
  - useSyncScroll 重写: 段落 ID 映射修复中英错位
  - 全局竞态修复 (active 标志), libraryRef 闭包过期修复
  - ErrorBoundary + vitest 测试基础设施
  - Logo 组件提取, CustomSelect 泛型化, TabId 类型统一
  - ReaderPanel 自动视图模式, AIAssistantPanel 状态批处理
2026-06-28 14:40:26 +08:00

114 lines
4.4 KiB
TypeScript

// dashboard/src/components/agent/ToolCallCard.tsx
// 工具调用卡片:展示参数和结果,皆可独立折叠
import { Settings, Eye } from 'lucide-react';
import type { ColorScheme } from './constants';
import { getToolDisplayName } from './toolDisplayNames';
import { AgentMarkdown } from './AgentMarkdown';
import { useAutoScroll } from './useAutoScroll';
interface ToolCallCardProps {
step: number;
name: string;
arguments: Record<string, unknown>;
result?: { output: string; isError: boolean };
isStreaming: boolean;
colorScheme: ColorScheme;
isArgsExpanded: boolean;
isResultExpanded: boolean;
onToggleArgs: () => void;
onToggleResult: () => void;
}
export function ToolCallCard({
step,
name,
arguments: args,
result,
isStreaming,
colorScheme,
isArgsExpanded,
isResultExpanded,
onToggleArgs,
onToggleResult,
}: ToolCallCardProps) {
const hasResult = !!result;
const dotColor = isStreaming ? colorScheme.tool_call.active : colorScheme.tool_call.idle;
const { chatEndRef, scrollContainerRef, handleScroll } = useAutoScroll(
result ? [result.output] : [],
);
return (
<div className="relative space-y-2 select-none">
<div
className={`absolute -left-[21px] top-3 w-2.5 h-2.5 rounded-full border-2 border-white ${dotColor}`}
/>
<div className="space-y-2 border border-slate-100 bg-white rounded-md p-3 shadow-2xs">
{/* 工具名称 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5 text-[10px] font-extrabold text-sky-700 tracking-wider uppercase">
<Settings
className={`w-3.5 h-3.5 ${hasResult ? 'text-slate-500' : 'text-sky-500 animate-spin'}`}
/>
<span>: {getToolDisplayName(name)}</span>
<span className="text-[9px] text-slate-400 font-mono font-medium">
· Step {step}
</span>
</div>
<button
onClick={onToggleArgs}
className="text-[10px] font-bold text-sky-600 hover:text-sky-800 hover:underline cursor-pointer flex items-center gap-0.5"
>
{isArgsExpanded ? '收起参数' : '展开参数'}
</button>
</div>
{/* 参数 */}
{isArgsExpanded && (
<div className="font-mono text-[10px] bg-slate-50 border border-slate-200 rounded-md p-2 text-slate-650 overflow-x-auto whitespace-pre-wrap max-w-full leading-relaxed select-text">
{JSON.stringify(args, null, 2)}
</div>
)}
{/* 结果 */}
{hasResult && (
<div className="space-y-1.5 border-t border-slate-100 pt-2.5 mt-2.5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5 text-[10px] font-extrabold text-emerald-700 tracking-wider uppercase">
<Eye className="w-3.5 h-3.5" />
<span> (Observation)</span>
{result!.isError && (
<span className="px-1.5 py-0.2 rounded bg-rose-50 text-rose-700 border border-rose-100 text-[9px]">
</span>
)}
</div>
<button
onClick={onToggleResult}
className="text-[10px] font-bold text-sky-600 hover:text-sky-800 hover:underline cursor-pointer flex items-center gap-0.5"
>
{isResultExpanded ? '收起结果' : '展开结果'}
</button>
</div>
{isResultExpanded ? (
<div
ref={scrollContainerRef}
onScroll={handleScroll}
className="text-xs bg-slate-50 border border-slate-200 rounded-md p-2.5 text-slate-700 overflow-auto select-text leading-relaxed max-h-[60vh] prose prose-sm max-w-none prose-headings:text-slate-800 prose-code:text-sky-700 prose-pre:bg-slate-100 prose-pre:text-xs prose-img:rounded-lg"
>
<AgentMarkdown>
{result!.output}
</AgentMarkdown>
<div ref={chatEndRef} />
</div>
) : (
<div className="text-[10px] text-slate-400 italic font-medium">
( {result!.output.length} )
</div>
)}
</div>
)}
</div>
</div>
);
}