工具

一个可折叠组件,用于在 AI 聊天机器人界面中显示工具调用详细信息。

Tool 组件显示一个可折叠界面,用于显示/隐藏工具详细信息。它设计用于接受来自 AI SDK 的 ToolUIPart 类型,并在可折叠界面中显示它。

使用 CLI 安装

AI Elements Vue
shadcn-vue CLI
npx ai-elements-vue@latest add tool

手动安装

将以下代码复制并粘贴到同一文件夹中。

Tool.vue
ToolStatusBadge.vue
ToolHeader.vue
ToolContent.vue
ToolInput.vue
ToolOutput.vue
index.ts
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { Collapsible } from '@repo/shadcn-vue/components/ui/collapsible'
import { cn } from '@repo/shadcn-vue/lib/utils'

const props = defineProps<{
  class?: HTMLAttributes['class']
}>()
</script>

<template>
  <Collapsible
    :class="cn('not-prose mb-4 w-full rounded-md border', props.class)"
    v-bind="$attrs"
  >
    <slot />
  </Collapsible>
</template>

与 AI SDK 一起使用

构建一个简单的有状态天气应用,使用 useChat 在工具中渲染最后一条消息。

将以下组件添加到你的前端:

pages/index.vue
<script setup lang="ts">
import type { ToolUIPart } from 'ai'
import { useChat } from '@ai-sdk/vue'
import { DefaultChatTransport } from 'ai'
import { computed, h } from 'vue'
import { MessageResponse } from '@/components/ai-elements/message'
import {
  Tool,
  ToolContent,
  ToolHeader,
  ToolInput,
  ToolOutput,
} from '@/components/ai-elements/tool'
import { Button } from '@/components/ui/button'

interface WeatherToolInput {
  location: string
  units: 'celsius' | 'fahrenheit'
}

interface WeatherToolOutput {
  location: string
  temperature: string
  conditions: string
  humidity: string
  windSpeed: string
  lastUpdated: string
}

type WeatherToolUIPart = ToolUIPart<{
  fetch_weather_data: {
    input: WeatherToolInput
    output: WeatherToolOutput
  }
}>

const { messages, sendMessage, status } = useChat({
  transport: new DefaultChatTransport({
    api: '/api/weather',
  }),
})

function formatWeatherResult(result: WeatherToolOutput): string {
  if (!result)
    return ''
  return `**Weather for ${result.location}**

**Temperature:** ${result.temperature}
**Conditions:** ${result.conditions}
**Humidity:** ${result.humidity}
**Wind Speed:** ${result.windSpeed}

*Last updated: ${result.lastUpdated}*`
}

function handleWeatherClick() {
  sendMessage({ text: 'Get weather data for San Francisco in fahrenheit' })
}

const latestMessage = computed(() => {
  if (!messages.value || messages.value.length === 0) {
    return undefined
  }
  return messages.value[messages.value.length - 1]
})

const weatherTool = computed(() => {
  return latestMessage.value?.parts?.find(
    part => part.type === 'tool-fetch_weather_data'
  ) as WeatherToolUIPart | undefined
})

const weatherOutputVNode = computed(() => {
  if (!weatherTool.value?.output) {
    return null
  }
  const markdown = formatWeatherResult(weatherTool.value.output)
  return h(MessageResponse, { content: markdown })
})
</script>

<template>
  <div
    class="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]"
  >
    <div class="flex flex-col h-full">
      <div class="space-y-4">
        <Button
          :disabled="status !== 'ready'"
          @click="handleWeatherClick"
        >
          Get Weather for San Francisco
        </Button>

        <Tool v-if="weatherTool" :default-open="true">
          <ToolHeader
            type="tool-fetch_weather_data"
            :state="weatherTool.state"
          />
          <ToolContent>
            <ToolInput :input="weatherTool.input" />
            <ToolOutput
              :output="weatherOutputVNode"
              :error-text="weatherTool.errorText"
            />
          </ToolContent>
        </Tool>
      </div>
    </div>
  </div>
</template>

将以下路由添加到你的后端:

server/api/agent.ts
import { convertToModelMessages, streamText, UIMessage } from 'ai'
import { defineEventHandler, readBody } from 'h3'
import { z } from 'zod'

// Allow streaming responses up to 30 seconds
export const maxDuration = 30

export default defineEventHandler(async (event) => {
  const body = await readBody(event) as { messages: UIMessage[] }
  const { messages } = body

  const result = streamText({
    model: 'openai/gpt-4o',
    messages: convertToModelMessages(messages),
    tools: {
      fetch_weather_data: {
        description: 'Fetch weather information for a specific location',
        parameters: z.object({
          location: z.string().describe('The city or location to get weather for'),
          units: z.enum(['celsius', 'fahrenheit']).default('celsius').describe('Temperature units'),
        }),
        inputSchema: z.object({
          location: z.string(),
          units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
        }),
        execute: async ({ location, units }) => {
          await new Promise(resolve => setTimeout(resolve, 1500))
          const temp = units === 'celsius'
            ? Math.floor(Math.random() * 35) + 5
            : Math.floor(Math.random() * 63) + 41

          return {
            location,
            temperature: `${temp}°${units === 'celsius' ? 'C' : 'F'}`,
            conditions: 'Sunny',
            humidity: '12%',
            windSpeed: `35 ${units === 'celsius' ? 'km/h' : 'mph'}`,
            lastUpdated: new Date().toLocaleString(),
          }
        },
      },
    },
  })

  return result.toUIMessageStreamResponse()
})

特性

  • 可折叠界面,用于显示/隐藏工具详细信息
  • 带有图标和徽章的可视化状态指示器
  • 支持多种工具执行状态(待处理、运行中、已完成、错误)
  • 格式化的参数显示,带有 JSON 语法高亮
  • 结果和错误处理,具有适当的样式
  • 可组合结构,用于灵活的布局
  • 可访问的键盘导航和屏幕阅读器支持
  • 与你的设计系统匹配的一致样式
  • 默认自动打开已完成的工具以获得更好的用户体验

示例

输入流式传输(待处理)

显示处于初始状态的工具,同时正在处理参数。

输入可用(运行中)

显示正在使用其参数主动执行的工具。

输入流式传输(已完成)

显示已完成且结果成功的工具。默认打开以显示结果。在这种情况下,输出是一个 JSON 对象,因此我们可以使用 CodeBlock 组件来显示它。

输出错误

显示在执行过程中遇到错误的工具。默认打开以显示错误。

Props

<Tool/>

classstring
应用于组件的额外 CSS 类。

<ToolHeader/>

typeToolUIPart['type']
工具的类型/名称。
stateToolUIPart['state']
工具的当前状态(input-streaming、input-available、output-available 或 output-error)。
titlestring
任务的标题。
classstring
应用于组件的额外 CSS 类。

<ToolContent/>

classstring
应用于组件的额外 CSS 类。

<ToolInput/>

inputToolUIPart['input']
传递给工具的输入参数,显示为格式化的 JSON。
classstring
应用于组件的额外 CSS 类。

<ToolOutput/>

outputToolUIPart['output']
工具执行的输出/结果。
errorTextToolUIPart['errorText']
如果工具执行失败,则为错误消息。
classstring
应用于组件的额外 CSS 类。