确认

一个基于 alert 的组件,用于管理工具执行批准工作流,具有请求、接受和拒绝状态。

Confirmation 组件提供了一个灵活的系统来显示工具批准请求及其结果。非常适合在 AI 工具执行前需要批准时向用户显示,并在之后显示批准状态。

使用 CLI 安装

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

手动安装

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

Confirmation.vue
ConfirmationTitle.vue
ConfirmationRequest.vue
ConfirmationAccepted.vue
ConfirmationRejected.vue
ConfirmationActions.vue
ConfirmationAction.vue
context.ts
index.ts
<script setup lang="ts">
import type { ToolUIPart } from 'ai'
import type { HTMLAttributes } from 'vue'
import type { ToolUIPartApproval } from './context'
import { Alert } from '@repo/shadcn-vue/components/ui/alert'
import { cn } from '@repo/shadcn-vue/lib/utils'
import { provide, toRef } from 'vue'
import { ConfirmationKey } from './context'

const props = defineProps<{
  approval?: ToolUIPartApproval
  state: ToolUIPart['state']
  class?: HTMLAttributes['class']
}>()

provide(ConfirmationKey, {
  approval: toRef(props, 'approval'),
  state: toRef(props, 'state'),
})
</script>

<template>
  <Alert
    v-if="approval && state !== 'input-streaming' && state !== 'input-available'"
    :class="cn('flex flex-col gap-2', props.class)"
    v-bind="$attrs"
  >
    <slot />
  </Alert>
</template>

与 AI SDK 一起使用

构建一个带有工具批准工作流的聊天 UI,其中危险工具在执行前需要用户确认。

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

pages/index.vue
<script setup lang="ts">
import type { ToolUIPart } from 'ai'
import { useChat } from '@ai-sdk/vue'
import { DefaultChatTransport } from 'ai'
import { CheckIcon, XIcon } from 'lucide-vue-next'
import { computed } from 'vue'
import {
  Confirmation,
  ConfirmationAccepted,
  ConfirmationAction,
  ConfirmationActions,
  ConfirmationRejected,
  ConfirmationRequest,
  ConfirmationTitle,
} from '@/components/ai-elements/confirmation'
import { MessageResponse } from '@/components/ai-elements/message'
import { Button } from '@/components/ui/button'

interface DeleteFileInput {
  filePath: string
  confirm: boolean
}

type DeleteFileToolUIPart = ToolUIPart<{
  delete_file: {
    input: DeleteFileInput
    output: { success: boolean, message: string }
  }
}>

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

function handleDeleteFile() {
  sendMessage({ text: 'Delete the file at /tmp/example.txt' })
}

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

const deleteTool = computed(() => {
  return latestMessage.value?.parts?.find(
    part => part.type === 'tool-delete_file'
  ) as DeleteFileToolUIPart | undefined
})
</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 space-y-4">
      <Button
        :disabled="status !== 'ready'"
        @click="handleDeleteFile"
      >
        Delete Example File
      </Button>

      <Confirmation
        v-if="deleteTool?.approval"
        :approval="deleteTool.approval"
        :state="deleteTool.state"
      >
        <ConfirmationTitle>
          <ConfirmationRequest>
            This tool wants to delete: <code>{{ deleteTool.input?.filePath }}</code>
            <br>
            Do you approve this action?
          </ConfirmationRequest>
          <ConfirmationAccepted>
            <CheckIcon class="size-4" />
            <span>You approved this tool execution</span>
          </ConfirmationAccepted>
          <ConfirmationRejected>
            <XIcon class="size-4" />
            <span>You rejected this tool execution</span>
          </ConfirmationRejected>
        </ConfirmationTitle>
        <ConfirmationActions>
          <ConfirmationAction
            variant="outline"
            @click="
              respondToConfirmationRequest({
                approvalId: deleteTool!.approval!.id,
                approved: false,
              })
            "
          >
            Reject
          </ConfirmationAction>
          <ConfirmationAction
            variant="default"
            @click="
              respondToConfirmationRequest({
                approvalId: deleteTool!.approval!.id,
                approved: true,
              })
            "
          >
            Approve
          </ConfirmationAction>
        </ConfirmationActions>
      </Confirmation>

      <MessageResponse
        v-if="deleteTool?.output"
        :content="
          deleteTool.output.success
            ? deleteTool.output.message
            : `Error: ${deleteTool.output.message}`
        "
      />
    </div>
  </div>
</template>

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

server/api/chat.ts
import { convertToModelMessages, streamText, UIMessage } from 'ai'
import { z } from 'zod'

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

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

  const result = streamText({
    model: 'openai/gpt-4o',
    messages: convertToModelMessages(body.messages),
    tools: {
      delete_file: {
        description: 'Delete a file from the file system',
        parameters: z.object({
          filePath: z.string().describe('The path to the file to delete'),
          confirm: z
            .boolean()
            .default(false)
            .describe('Confirmation that the user wants to delete the file'),
        }),
        requireApproval: true, // Enables approval workflow
        execute: async ({ filePath, confirm }) => {
          if (!confirm) {
            return {
              success: false,
              message: 'Deletion not confirmed',
            }
          }

          // Simulate a file deletion delay
          await new Promise(resolve => setTimeout(resolve, 500))

          return {
            success: true,
            message: `Successfully deleted ${filePath}`,
          }
        },
      },
    },
  })

  // Stream back to the UI
  return result.toAIStreamResponse()
})

特性

  • 基于上下文的批准工作流状态管理
  • 基于批准状态的条件渲染
  • 支持 approval-requested、approval-responded、output-denied 和 output-available 状态
  • 基于 shadcn-vue Alert 和 Button 组件构建
  • 具有全面类型定义的 TypeScript 支持
  • 使用 Tailwind CSS 可自定义样式
  • 键盘导航和可访问性支持
  • 主题感知,具有自动深色模式支持

示例

批准请求状态

当状态为 approval-requested 时显示带有操作按钮的批准请求。

已批准状态

当用户批准且状态为 approval-respondedoutput-available 时显示已接受状态。

已拒绝状态

当用户拒绝且状态为 output-denied 时显示已拒绝状态。

Props

<Confirmation />

approvalToolUIPart['approval']
包含批准 ID 和状态的批准对象。如果未提供或为 undefined,组件将不会渲染。
stateToolUIPart['state']
工具的当前状态(input-streaming、input-available、approval-requested、approval-responded、output-denied 或 output-available)。对于 input-streaming 或 input-available 状态不会渲染。
classstring
应用于组件的额外类。

<ConfirmationTitle />

classstring
应用于组件的额外类。

<ConfirmationActions />

classstring
应用于组件的额外类。