组件
AI 工具调用 AI Tool
展示 AI 工具的执行状态、参数、结果与错误。
基础示例
Loading…
pnpm dlx wui@latest add @wui/ai-tool"use client"
import * as React from "react"
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
import { cva } from "class-variance-authority"
import {
CheckCircle2Icon,
ChevronDownIcon,
CircleDashedIcon,
CircleXIcon,
LoaderCircleIcon,
ShieldAlertIcon,
WrenchIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
export type AiToolStatus =
| "pending"
| "approval"
| "running"
| "success"
| "error"
| "denied"
const AiToolContext = React.createContext<{ status: AiToolStatus }>({
status: "pending",
})
const aiToolStatusVariants = cva(
"inline-flex items-center gap-1.5 text-xs font-medium",
{
variants: {
status: {
pending: "text-muted-foreground",
approval: "text-warning",
running: "text-info",
success: "text-success",
error: "text-destructive",
denied: "text-muted-foreground",
},
},
defaultVariants: { status: "pending" },
}
)
const statusMeta = {
pending: { label: "等待中", icon: CircleDashedIcon },
approval: { label: "等待确认", icon: ShieldAlertIcon },
running: { label: "执行中", icon: LoaderCircleIcon },
success: { label: "已完成", icon: CheckCircle2Icon },
error: { label: "执行失败", icon: CircleXIcon },
denied: { label: "已拒绝", icon: CircleXIcon },
} as const
export interface AiToolProps
extends React.ComponentProps<typeof CollapsiblePrimitive.Root> {
/** Tool execution state. @default "pending" */
status?: AiToolStatus
}
/** A compact disclosure for an AI tool invocation and its input/output. */
function AiTool({
className,
status = "pending",
defaultOpen,
...props
}: AiToolProps) {
return (
<AiToolContext.Provider value={{ status }}>
<CollapsiblePrimitive.Root
data-slot="ai-tool"
data-status={status}
className={cn("overflow-hidden rounded-md border bg-background", className)}
defaultOpen={defaultOpen ?? status === "error"}
{...props}
/>
</AiToolContext.Provider>
)
}
export interface AiToolTriggerProps
extends React.ComponentProps<typeof CollapsiblePrimitive.Trigger> {
/** Human-readable tool name. */
name?: string
}
function AiToolTrigger({
className,
name = "工具调用",
children,
...props
}: AiToolTriggerProps) {
return (
<CollapsiblePrimitive.Trigger
data-slot="ai-tool-trigger"
className={cn(
"group flex w-full items-center gap-2.5 px-3 py-2.5 text-left outline-none transition-colors hover:bg-muted/60 focus-visible:ring-[3px] focus-visible:ring-ring/35",
className
)}
{...props}
>
{children ?? (
<>
<WrenchIcon className="size-4 shrink-0 text-muted-foreground" />
<span className="min-w-0 flex-1 truncate font-mono text-sm font-medium">
{name}
</span>
<AiToolStatus />
<ChevronDownIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
</>
)}
</CollapsiblePrimitive.Trigger>
)
}
export interface AiToolStatusProps extends React.ComponentProps<"span"> {
/** Override the state inherited from AiTool. */
status?: AiToolStatus
/** Override the localized status label. */
label?: string
}
function AiToolStatus({
className,
status: statusProp,
label,
...props
}: AiToolStatusProps) {
const context = React.useContext(AiToolContext)
const status = statusProp ?? context.status
const meta = statusMeta[status]
const Icon = meta.icon
return (
<span
data-slot="ai-tool-status"
data-status={status}
className={cn(aiToolStatusVariants({ status }), className)}
{...props}
>
<Icon
className={cn(
"size-3.5",
status === "running" && "motion-safe:animate-spin"
)}
/>
{label ?? meta.label}
</span>
)
}
function AiToolContent({
className,
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Content>) {
return (
<CollapsiblePrimitive.Content
data-slot="ai-tool-content"
className={cn(
"overflow-hidden border-t text-sm",
className
)}
{...props}
/>
)
}
function AiToolSection({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="ai-tool-section"
className={cn("border-b px-3 py-3 last:border-b-0", className)}
{...props}
/>
)
}
function AiToolLabel({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="ai-tool-label"
className={cn(
"mb-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground",
className
)}
{...props}
/>
)
}
function AiToolCode({
className,
...props
}: React.ComponentProps<"pre">) {
return (
<pre
data-slot="ai-tool-code"
className={cn(
"overflow-x-auto rounded-md bg-muted/60 p-3 font-mono text-xs leading-5 text-foreground",
className
)}
{...props}
/>
)
}
function AiToolError({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
role="alert"
data-slot="ai-tool-error"
className={cn(
"rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive",
className
)}
{...props}
/>
)
}
export {
AiTool,
AiToolCode,
AiToolContent,
AiToolError,
AiToolLabel,
AiToolSection,
AiToolStatus,
AiToolTrigger,
aiToolStatusVariants,
}
组件作用
AiTool 用于把一次工具调用压缩成可扫描的状态行,并在需要时展开参数、结果或错误。它适合搜索、读文件、数据库查询和生成任务,不负责真正调用工具,也不应把敏感参数直接展示给最终用户。
普通后台任务状态更适合进度条或时间轴;只有当“哪个工具做了什么”对理解 Agent 行为有帮助时才使用该组件。
组件属性
| Prop | Type | Default | Description |
|---|---|---|---|
| status | "pending" | "approval" | "running" | "success" | "error" | "denied" | pending | Tool execution state. |
| disabled | boolean | — | |
| asChild | boolean | — |
事件
组件透传 Radix Collapsible 的 open、defaultOpen 与 onOpenChange,支持受控和非受控展开。工具批准、拒绝与重试按钮由业务层放入 AiToolSection,并自行绑定请求事件。
扩展使用
状态映射
使用 pending、approval、running、success、error 与 denied 映射工具协议状态。颜色、图标和文字会共同表达状态,不依赖单一颜色;running 图标会在系统开启“减少动态效果”时停止旋转。
自定义输出
JSON 参数可放入 AiToolCode,普通文本、表格、文件列表或图片可以直接组合在 AiToolSection 内。失败信息使用 AiToolError,避免把堆栈和内部路径直接暴露给不需要调试信息的用户。