wui
组件

AI 工具调用 AI Tool

展示 AI 工具的执行状态、参数、结果与错误。

基础示例

Loading…
pnpm dlx wui@latest add @wui/ai-tool
components/ui/ai-tool.tsx
"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 行为有帮助时才使用该组件。

组件属性

PropTypeDefaultDescription
status"pending" | "approval" | "running" | "success" | "error" | "denied"pendingTool execution state.
disabledboolean
asChildboolean

事件

组件透传 Radix Collapsible 的 opendefaultOpenonOpenChange,支持受控和非受控展开。工具批准、拒绝与重试按钮由业务层放入 AiToolSection,并自行绑定请求事件。

扩展使用

状态映射

使用 pendingapprovalrunningsuccesserrordenied 映射工具协议状态。颜色、图标和文字会共同表达状态,不依赖单一颜色;running 图标会在系统开启“减少动态效果”时停止旋转。

自定义输出

JSON 参数可放入 AiToolCode,普通文本、表格、文件列表或图片可以直接组合在 AiToolSection 内。失败信息使用 AiToolError,避免把堆栈和内部路径直接暴露给不需要调试信息的用户。