后端: - 权限系统重写: 全局→按 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 状态批处理
114 lines
4.4 KiB
TypeScript
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>
|
|
);
|
|
}
|