Files
2026-03-20 07:33:46 +00:00

132 lines
6.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react'
import {
Code,
Terminal,
Search,
FileCode,
Globe,
FileText,
FileEdit,
Image,
GalleryHorizontal,
Puzzle,
Bot,
RefreshCw,
Eye,
Layers,
Wrench,
} from 'lucide-react'
import { cn } from '@/utils/cn'
import type { ApiEvent } from '../../types'
import { SILENT_ACTION_TYPES } from './utils'
export interface ToolCallActionProps {
name?: string
arguments?: string[]
action_type?: string
event?: ApiEvent
onClick?: (event: ApiEvent) => void
}
// ─── action_type → { icon, label } ──────────────────────────────────────────
const ACTION_TYPE_META: Record<string, { icon: React.ReactNode; label: string }> = {
shell_execute: { icon: <Terminal className="w-4 h-4" />, label: '执行命令' },
terminal_operator: { icon: <Terminal className="w-4 h-4" />, label: '终端操作' },
code_execute: { icon: <Code className="w-4 h-4" />, label: '运行代码' },
file_operator: { icon: <FileCode className="w-4 h-4" />, label: '文件操作' },
file_create: { icon: <FileText className="w-4 h-4" />, label: '创建文件' },
file_read: { icon: <FileText className="w-4 h-4" />, label: '读取文件' },
file_replace_text: { icon: <FileEdit className="w-4 h-4" />, label: '编辑文件' },
file_write_text: { icon: <FileEdit className="w-4 h-4" />, label: '写入文件' },
str_replace: { icon: <FileEdit className="w-4 h-4" />, label: '替换内容' },
info_search_web: { icon: <Search className="w-4 h-4" />, label: '搜索网页' },
info_fetch_webpage: { icon: <Globe className="w-4 h-4" />, label: '获取网页' },
news_search: { icon: <Search className="w-4 h-4" />, label: '新闻搜索' },
image_search: { icon: <Image className="w-4 h-4" />, label: '图片搜索' },
info_search_custom_knowledge: { icon: <Search className="w-4 h-4" />, label: '知识检索' },
search_custom_knowledge: { icon: <Search className="w-4 h-4" />, label: '知识检索' },
browser_use: { icon: <Globe className="w-4 h-4" />, label: '浏览器操作' },
slide_init: { icon: <GalleryHorizontal className="w-4 h-4" />, label: '初始化幻灯片' },
slide_template: { icon: <GalleryHorizontal className="w-4 h-4" />, label: '选择模板' },
slide_create: { icon: <GalleryHorizontal className="w-4 h-4" />, label: '生成幻灯片' },
slide_create_batch: { icon: <Layers className="w-4 h-4" />, label: '批量生成幻灯片' },
slide_present: { icon: <GalleryHorizontal className="w-4 h-4" />, label: '展示幻灯片' },
media_generate_image: { icon: <Image className="w-4 h-4" />, label: '生成图片' },
media_vision_image: { icon: <Eye className="w-4 h-4" />, label: '识别图片' },
generate_image: { icon: <Image className="w-4 h-4" />, label: '生成图片' },
call_flow: { icon: <RefreshCw className="w-4 h-4" />, label: '调用流程' },
integrated_app: { icon: <Puzzle className="w-4 h-4" />, label: '集成应用' },
custom_api: { icon: <Wrench className="w-4 h-4" />, label: '自定义工具' },
skill_loader: { icon: <Bot className="w-4 h-4" />, label: '加载技能' },
brand_search: { icon: <Search className="w-4 h-4" />, label: '品牌检索' },
xiaohongshu_search: { icon: <Search className="w-4 h-4" />, label: '小红书搜索' },
e_commerce: { icon: <Search className="w-4 h-4" />, label: '电商搜索' },
experience_query: { icon: <Search className="w-4 h-4" />, label: '经验查询' },
writer: { icon: <FileEdit className="w-4 h-4" />, label: '文档创作' },
parallel_map: { icon: <Layers className="w-4 h-4" />, label: '并行任务' },
media_comments: { icon: <Search className="w-4 h-4" />, label: '媒体搜索' },
system_api: { icon: <Wrench className="w-4 h-4" />, label: '系统接口' },
}
/**
* 工具调用 Action 组件
* - 静默事件类型 → 不渲染
* - name / label / argsText 全空 → 不渲染
* - 点击触发外部 onClick打开右侧面板
*/
export function ToolCallAction({
name,
arguments: args,
action_type,
event,
onClick,
}: ToolCallActionProps) {
// 静默类型直接跳过
if (action_type && SILENT_ACTION_TYPES.has(action_type)) return null
const meta = action_type ? ACTION_TYPE_META[action_type] : undefined
const icon = meta?.icon ?? <Code className="w-4 h-4" />
const label = name || meta?.label || ''
const argsText = args?.filter(Boolean).join(' ') || ''
// 没有任何可展示内容时不渲染
if (!label && !argsText) return null
const handleClick = () => {
if (event && onClick) onClick(event)
}
const isClickable = !!(event && onClick)
return (
<div
className={cn(
'inline-flex items-center gap-2 px-3 py-1.5 rounded-lg max-w-full mb-2',
'border border-gray-200 bg-white/80',
'text-sm',
isClickable && 'cursor-pointer transition-all duration-150 hover:bg-gray-50 hover:border-gray-300',
)}
onClick={isClickable ? handleClick : undefined}
role={isClickable ? 'button' : undefined}
tabIndex={isClickable ? 0 : undefined}
onKeyDown={
isClickable
? e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
handleClick()
}
}
: undefined
}
>
<span className="shrink-0 text-gray-500">{icon}</span>
{label && <span className="shrink-0 font-medium text-gray-800">{label}:</span>}
{argsText && (
<code className="min-w-0 flex-1 truncate font-mono text-xs text-gray-500">
{argsText}
</code>
)}
</div>
)
}