132 lines
6.4 KiB
TypeScript
132 lines
6.4 KiB
TypeScript
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>
|
||
)
|
||
}
|