返回 AiToEarn
ToolDetailModal.tsx
根目录 / project / aitoearn-web / src / components / Chat / ChatMessage / ToolDetailModal.tsx
1 /**
2 * ToolDetailModal - 工具调用详情弹框
3 * 展示工具的输入参数和执行结果
4 */
5
6 'use client'
7
8 import type { IWorkflowStep } from '@/store/agent'
9 import { Wrench } from 'lucide-react'
10 import { memo } from 'react'
11 import { useTransClient } from '@/app/i18n/client'
12 import { Modal } from '@/components/ui/modal'
13
14 export interface IToolDetailModalProps {
15 /** 是否打开弹框 */
16 open: boolean
17 /** 关闭弹框回调 */
18 onOpenChange: (open: boolean) => void
19 /** 工具调用步骤数据 */
20 step: IWorkflowStep | null
21 }
22
23 /**
24 * 格式化工具名称(移除 mcp__ 前缀)
25 */
26 function formatToolName(name: string) {
27 return name.replace(/^mcp__\w+__/, '')
28 }
29
30 /**
31 * JSON 格式化(美化显示)
32 */
33 function formatJSON(content?: string): string {
34 if (!content)
35 return ''
36 try {
37 return JSON.stringify(JSON.parse(content), null, 2)
38 }
39 catch {
40 return content
41 }
42 }
43
44 /**
45 * 结果格式化(尝试 JSON,否则原样显示)
46 */
47 function formatResult(result?: string): string {
48 if (!result)
49 return ''
50 try {
51 const parsed = JSON.parse(result)
52 return JSON.stringify(parsed, null, 2)
53 }
54 catch {
55 return result
56 }
57 }
58
59 /**
60 * 弹框内部内容组件(使用 hooks)
61 */
62 const ToolDetailModalContent = memo(
63 ({ onOpenChange, step }: { onOpenChange: (open: boolean) => void, step: IWorkflowStep }) => {
64 const { t } = useTransClient('chat')
65
66 const title = (
67 <div className="flex items-center gap-2">
68 <Wrench className="w-4 h-4" />
69 <span>{formatToolName(step.toolName || 'Tool')}</span>
70 </div>
71 )
72
73 return (
74 <Modal
75 open
76 onCancel={() => onOpenChange(false)}
77 title={title}
78 footer={null}
79 width={600}
80 destroyOnClose
81 bodyClassName="!py-0"
82 >
83 {/* 输入参数区域 */}
84 <div className="mb-4">
85 <h4 className="text-sm font-medium mb-2 text-foreground">{t('toolDetail.input')}</h4>
86 <pre className="bg-muted p-3 rounded-lg text-xs overflow-auto max-h-48 whitespace-pre-wrap break-all text-muted-foreground">
87 {formatJSON(step.content) || '-'}
88 </pre>
89 </div>
90
91 {/* 执行结果区域 */}
92 <div>
93 <h4 className="text-sm font-medium mb-2 text-foreground">{t('toolDetail.output')}</h4>
94 {step.result ? (
95 <pre className="bg-muted p-3 rounded-lg text-xs overflow-auto max-h-64 whitespace-pre-wrap break-all text-muted-foreground">
96 {formatResult(step.result)}
97 </pre>
98 ) : (
99 <div className="bg-muted p-3 rounded-lg text-xs text-muted-foreground/70">
100 {t('toolDetail.noResult')}
101 </div>
102 )}
103 </div>
104 </Modal>
105 )
106 },
107 )
108
109 ToolDetailModalContent.displayName = 'ToolDetailModalContent'
110
111 /**
112 * 工具详情弹框(外层组件,控制渲染时机,避免 i18n 闪烁)
113 */
114 export function ToolDetailModal({ open, onOpenChange, step }: IToolDetailModalProps) {
115 // 只在打开时渲染内部组件,避免动态加载 namespace 导致闪烁
116 if (!open || !step)
117 return null
118
119 return <ToolDetailModalContent onOpenChange={onOpenChange} step={step} />
120 }
121
122 export default ToolDetailModal
123
123 lines Plain Text