确认
一个基于 alert 的组件,用于管理工具执行批准工作流,具有请求、接受和拒绝状态。
Confirmation 组件提供了一个灵活的系统来显示工具批准请求及其结果。非常适合在 AI 工具执行前需要批准时向用户显示,并在之后显示批准状态。
使用 CLI 安装
AI Elements Vue
shadcn-vue CLI
npx ai-elements-vue@latest add confirmation
npx shadcn-vue@latest add https://registry.ai-elements-vue.com/confirmation.json
手动安装
将以下代码复制并粘贴到同一文件夹中。
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>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { AlertDescription } from '@repo/shadcn-vue/components/ui/alert'
import { cn } from '@repo/shadcn-vue/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<AlertDescription
:class="cn('inline', props.class)"
v-bind="$attrs"
>
<slot />
</AlertDescription>
</template>
<script setup lang="ts">
import { useConfirmationContext } from './context'
const { state } = useConfirmationContext()
</script>
<template>
<template v-if="state === 'approval-requested'">
<slot />
</template>
</template>
<script setup lang="ts">
import { useConfirmationContext } from './context'
const { approval, state } = useConfirmationContext()
</script>
<template>
<template
v-if="
approval?.approved
&& (state === 'approval-responded'
|| state === 'output-denied'
|| state === 'output-available')
"
>
<slot />
</template>
</template>
<script setup lang="ts">
import { useConfirmationContext } from './context'
const { approval, state } = useConfirmationContext()
</script>
<template>
<template
v-if="
approval?.approved === false
&& (state === 'approval-responded'
|| state === 'output-denied'
|| state === 'output-available')
"
>
<slot />
</template>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@repo/shadcn-vue/lib/utils'
import { useConfirmationContext } from './context'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
const { state } = useConfirmationContext()
</script>
<template>
<div
v-if="state === 'approval-requested'"
:class="
cn('flex items-center justify-end gap-2 self-end', props.class)
"
v-bind="$attrs"
>
<slot />
</div>
</template>
<script setup lang="ts">
import { Button } from '@repo/shadcn-vue/components/ui/button'
</script>
<template>
<Button class="h-8 px-3 text-sm" type="button" v-bind="$attrs">
<slot />
</Button>
</template>
import type { ToolUIPart } from 'ai'
import type { InjectionKey, Ref } from 'vue'
import { inject } from 'vue'
export type ToolUIPartApproval
= | {
id: string
approved?: never
reason?: never
}
| {
id: string
approved: boolean
reason?: string
}
| undefined
export interface ConfirmationContextValue {
approval: Ref<ToolUIPartApproval>
state: Ref<ToolUIPart['state']>
}
export const ConfirmationKey: InjectionKey<ConfirmationContextValue>
= Symbol('ConfirmationContext')
export function useConfirmationContext() {
const context = inject(ConfirmationKey)
if (!context)
throw new Error('Confirmation components must be used within <Confirmation>')
return context
}
export { default as Confirmation } from './Confirmation.vue'
export { default as ConfirmationAccepted } from './ConfirmationAccepted.vue'
export { default as ConfirmationAction } from './ConfirmationAction.vue'
export { default as ConfirmationActions } from './ConfirmationActions.vue'
export { default as ConfirmationRejected } from './ConfirmationRejected.vue'
export { default as ConfirmationRequest } from './ConfirmationRequest.vue'
export { default as ConfirmationTitle } from './ConfirmationTitle.vue'
与 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-responded 或 output-available 时显示已接受状态。
已拒绝状态
当用户拒绝且状态为 output-denied 时显示已拒绝状态。
Props
<Confirmation />
approvalToolUIPart['approval']
stateToolUIPart['state']
classstring
<ConfirmationTitle />
classstring
<ConfirmationActions />
classstring