Confirmation
Tool approval request with request, accepted and rejected states.
import { Confirmation, ConfirmationAccepted, ConfirmationAction, ConfirmationActions, ConfirmationDescription, ConfirmationRejected, ConfirmationRequest, ConfirmationTitle,} from "@/components/ai/confirmation";import { useState } from "react";import { IconPlaceholder } from "@/edmi/icon-placeholder";
const check = ( <IconPlaceholder lucide="CheckIcon" tabler="IconCheck" hugeicons="Tick02Icon" phosphor="CheckIcon" remixicon="RiCheckLine" className="size-4" />);const cross = ( <IconPlaceholder lucide="XIcon" tabler="IconX" hugeicons="Cancel01Icon" phosphor="XIcon" remixicon="RiCloseLine" className="size-4" />);
type State = "approval-requested" | "approval-responded" | "output-denied";
function Row({ state: initial, approval: initialApproval, interactive = false,}: { state: State; approval: { id: string; approved?: boolean }; interactive?: boolean;}) { const [state, setState] = useState<State>(initial); const [approval, setApproval] = useState(initialApproval); const respond = (approved: boolean) => { setApproval({ ...approval, approved }); setState(approved ? "approval-responded" : "output-denied"); }; return ( <Confirmation approval={approval} state={state} className="w-full max-w-md"> <ConfirmationTitle> <ConfirmationRequest>Run rebalance on MAG4?</ConfirmationRequest> <ConfirmationAccepted> {check} <span>Approved · rebalance sent</span> </ConfirmationAccepted> <ConfirmationRejected> {cross} <span>Rejected · nothing was changed</span> </ConfirmationRejected> </ConfirmationTitle> <ConfirmationDescription> Sells 0.42 NVDAx and buys MSFTx + AAPLx. Max slippage 1%. </ConfirmationDescription> <ConfirmationActions> <ConfirmationAction onClick={interactive ? () => respond(true) : undefined} > Approve </ConfirmationAction> <ConfirmationAction variant="outline" onClick={interactive ? () => respond(false) : undefined} > Reject </ConfirmationAction> </ConfirmationActions> </Confirmation> );}
export default function Demo() { return ( <div className="flex flex-col gap-4"> <Row state="approval-requested" approval={{ id: "1" }} interactive /> <Row state="approval-responded" approval={{ id: "2", approved: true }} /> <Row state="output-denied" approval={{ id: "3", approved: false }} /> </div> );}<script setup lang="ts">import { Confirmation, ConfirmationAccepted, ConfirmationAction, ConfirmationActions, ConfirmationDescription, ConfirmationRejected, ConfirmationRequest, ConfirmationTitle,} from "@/components/ai/confirmation";import { CheckIcon, XIcon } from "@lucide/vue";import { reactive } from "vue";
const rows = reactive([ { state: "approval-requested", approval: { id: "1" } }, { state: "approval-responded", approval: { id: "2", approved: true } }, { state: "output-denied", approval: { id: "3", approved: false } },]) as { state: "approval-requested" | "approval-responded" | "output-denied"; approval: { id: string; approved?: boolean } }[];
// The first row is live: Approve / Reject move it to the matching state.function respond(row: (typeof rows)[number], approved: boolean) { row.approval = { ...row.approval, approved }; row.state = approved ? "approval-responded" : "output-denied";}</script>
<template> <div class="flex flex-col gap-4"> <Confirmation v-for="row in rows" :key="row.approval.id" :approval="row.approval" :state="row.state" class="w-full max-w-md" > <ConfirmationTitle> <ConfirmationRequest>Run rebalance on MAG4?</ConfirmationRequest> <ConfirmationAccepted> <CheckIcon class="size-4" /> <span>Approved · rebalance sent</span> </ConfirmationAccepted> <ConfirmationRejected> <XIcon class="size-4" /> <span>Rejected · nothing was changed</span> </ConfirmationRejected> </ConfirmationTitle> <ConfirmationDescription> Sells 0.42 NVDAx and buys MSFTx + AAPLx. Max slippage 1%. </ConfirmationDescription> <ConfirmationActions> <ConfirmationAction @click="row.approval.id === '1' && respond(row, true)">Approve</ConfirmationAction> <ConfirmationAction variant="outline" @click="row.approval.id === '1' && respond(row, false)">Reject</ConfirmationAction> </ConfirmationActions> </Confirmation> </div></template><script lang="ts"> import { Confirmation, ConfirmationAccepted, ConfirmationAction, ConfirmationActions, ConfirmationDescription, ConfirmationRejected, ConfirmationRequest, ConfirmationTitle, } from "$lib/components/ai/confirmation"; import IconPlaceholder from "$lib/components/icon-placeholder/icon-placeholder.svelte";
type Row = { state: "approval-requested" | "approval-responded" | "output-denied"; approval: { id: string; approved?: boolean }; }; let rows = $state<Row[]>([ { state: "approval-requested", approval: { id: "1" } }, { state: "approval-responded", approval: { id: "2", approved: true } }, { state: "output-denied", approval: { id: "3", approved: false } }, ]);
// The first row is live: Approve / Reject move it to the matching state. function respond(row: Row, approved: boolean) { if (row.approval.id !== "1") return; row.approval = { ...row.approval, approved }; row.state = approved ? "approval-responded" : "output-denied"; }</script>
<div class="flex flex-col gap-4"> {#each rows as row (row.approval.id)} <Confirmation approval={row.approval} state={row.state} class="w-full max-w-md"> <ConfirmationTitle> <ConfirmationRequest>Run rebalance on MAG4?</ConfirmationRequest> <ConfirmationAccepted> <IconPlaceholder lucide="CheckIcon" tabler="IconCheck" hugeicons="Tick02Icon" phosphor="CheckIcon" remixicon="RiCheckLine" class="size-4" /> <span>Approved · rebalance sent</span> </ConfirmationAccepted> <ConfirmationRejected> <IconPlaceholder lucide="XIcon" tabler="IconX" hugeicons="Cancel01Icon" phosphor="XIcon" remixicon="RiCloseLine" class="size-4" /> <span>Rejected · nothing was changed</span> </ConfirmationRejected> </ConfirmationTitle> <ConfirmationDescription> Sells 0.42 NVDAx and buys MSFTx + AAPLx. Max slippage 1%. </ConfirmationDescription> <ConfirmationActions> <ConfirmationAction onclick={() => respond(row, true)}>Approve</ConfirmationAction> <ConfirmationAction variant="outline" onclick={() => respond(row, false)}> Reject </ConfirmationAction> </ConfirmationActions> </Confirmation> {/each}</div>Built on alert and button.
Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/ai-confirmationpnpm dlx shadcn@latest add @edmi-ui/ai-confirmationyarn dlx shadcn@latest add @edmi-ui/ai-confirmationbunx shadcn@latest add @edmi-ui/ai-confirmationPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install aiTerminal window pnpm add aiTerminal window yarn add aiTerminal window bun add aiAdd the Edmi components this one builds on:
alert,button,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/ai-confirmationpnpm dlx shadcn-vue@latest add @edmi-ui/ai-confirmationyarn dlx shadcn-vue@latest add @edmi-ui/ai-confirmationbunx shadcn-vue@latest add @edmi-ui/ai-confirmationPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install ai @lucide/vueTerminal window pnpm add ai @lucide/vueTerminal window yarn add ai @lucide/vueTerminal window bun add ai @lucide/vueAdd the Edmi components this one builds on:
alert,button,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-confirmation.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-confirmation.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-confirmation.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-confirmation.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
alert,button,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
import { Confirmation, ConfirmationAccepted, ConfirmationAction, ConfirmationActions, ConfirmationDescription, ConfirmationRejected, ConfirmationRequest, ConfirmationTitle,} from "@/components/ai/confirmation";
<Confirmation className="w-full max-w-md"> <ConfirmationTitle> <ConfirmationRequest>Run rebalance on MAG4?</ConfirmationRequest> <ConfirmationAccepted> <span>Approved · rebalance sent</span> </ConfirmationAccepted> <ConfirmationRejected> <span>Rejected · nothing was changed</span> </ConfirmationRejected> </ConfirmationTitle> <ConfirmationDescription> Sells 0.42 NVDAx and buys MSFTx + AAPLx. Max slippage 1%. </ConfirmationDescription> <ConfirmationActions> <ConfirmationAction > Approve </ConfirmationAction> <ConfirmationAction variant="outline" > Reject </ConfirmationAction> </ConfirmationActions></Confirmation><script setup lang="ts">import { Confirmation, ConfirmationAccepted, ConfirmationDescription, ConfirmationRejected, ConfirmationRequest, ConfirmationTitle,} from "@/components/ai/confirmation";</script>
<template> <div class="flex flex-col gap-4"> <Confirmation :key="row.approval.id" :approval="row.approval" :state="row.state" class="w-full max-w-md" > <ConfirmationTitle> <ConfirmationRequest>Run rebalance on MAG4?</ConfirmationRequest> <ConfirmationAccepted> <CheckIcon class="size-4" /> <span>Approved · rebalance sent</span> </ConfirmationAccepted> <ConfirmationRejected> <XIcon class="size-4" /> <span>Rejected · nothing was changed</span> </ConfirmationRejected> </ConfirmationTitle> <ConfirmationDescription> Sells 0.42 NVDAx and buys MSFTx + AAPLx. Max slippage 1%. </ConfirmationDescription> <!-- … --> </Confirmation> </div></template><script lang="ts"> import { Confirmation, ConfirmationAccepted, ConfirmationAction, ConfirmationActions, ConfirmationDescription, ConfirmationRejected, ConfirmationRequest, ConfirmationTitle, } from "$lib/components/ai/confirmation";</script>
<div class="flex flex-col gap-4"> {#each rows as row (row.approval.id)} <Confirmation approval={row.approval} state={row.state} class="w-full max-w-md"> <ConfirmationTitle> <ConfirmationRequest>Run rebalance on MAG4?</ConfirmationRequest> <ConfirmationAccepted> <CheckIcon class="size-4" /> <span>Approved · rebalance sent</span> </ConfirmationAccepted> <ConfirmationRejected> <XIcon class="size-4" /> <span>Rejected · nothing was changed</span> </ConfirmationRejected> </ConfirmationTitle> <ConfirmationDescription> Sells 0.42 NVDAx and buys MSFTx + AAPLx. Max slippage 1%. </ConfirmationDescription> <ConfirmationActions> <ConfirmationAction onclick={() => respond(row, true)}>Approve</ConfirmationAction> <ConfirmationAction variant="outline" onclick={() => respond(row, false)}> Reject </ConfirmationAction> </ConfirmationActions> </Confirmation> {/each}</div>Component source
Section titled “Component source”"use client";
import type { ToolUIPart } from "ai";import { cn } from "cn";import type { ComponentProps, ReactNode } from "react";import { createContext, useContext, useMemo } from "react";import { ShieldIcon } from "@phosphor-icons/react";import { Alert } from "@/registry/edmi/ui/alert";import { Button } from "@/registry/edmi/ui/button";import type { Elevation } from "@/registry/edmi/ui/elevation";
type ToolUIPartApproval = | { id: string; approved?: never; reason?: never; } | { id: string; approved: boolean; reason?: string; } | undefined;
interface ConfirmationContextValue { approval: ToolUIPartApproval; state: ToolUIPart["state"]; elevation: Elevation | undefined;}
const ConfirmationContext = createContext<ConfirmationContextValue | null>( null,);
const useConfirmation = () => { const context = useContext(ConfirmationContext);
if (!context) { throw new Error("Confirmation components must be used within Confirmation"); }
return context;};
export type ConfirmationProps = Omit< ComponentProps<typeof Alert>, "variant"> & { approval?: ToolUIPartApproval; state: ToolUIPart["state"]; /** ✦ depth of the action buttons (they inherit it, each can override). The alert itself stays flat. */ elevation?: Elevation; /** Leading icon of the request. Defaults to a shield. */ icon?: ReactNode;};
/** * Request = warning alert with Approve / Reject. Once answered it collapses to a plain status line * (green check for approved, muted cross for rejected), as on board AI 02. */export const Confirmation = ({ className, approval, state, elevation, icon, children, ...props}: ConfirmationProps) => { const contextValue = useMemo( () => ({ approval, elevation, state }), [approval, elevation, state], );
if (!approval || state === "input-streaming" || state === "input-available") { return null; }
if (state !== "approval-requested") { return ( <ConfirmationContext.Provider value={contextValue}> <div data-slot="ai-confirmation" data-state={state} className={cn("flex items-center gap-2 text-[13.5px]", className)} {...(props as ComponentProps<"div">)} > {children} </div> </ConfirmationContext.Provider> ); }
return ( <ConfirmationContext.Provider value={contextValue}> <Alert data-slot="ai-confirmation" data-state={state} variant="warning" className={className} {...props} > {icon ?? ( <ShieldIcon /> )} {children} </Alert> </ConfirmationContext.Provider> );};
export type ConfirmationTitleProps = ComponentProps<"div">;
export const ConfirmationTitle = ({ className, ...props}: ConfirmationTitleProps) => { const { state } = useConfirmation(); return ( <div data-slot="ai-confirmation-title" className={cn( state === "approval-requested" ? "col-start-2 font-medium text-foreground" : "flex items-center gap-2", className, )} {...props} /> );};
export type ConfirmationDescriptionProps = ComponentProps<"div">;
/** ✦ Muted second line under the request title (only while the request is open). */export const ConfirmationDescription = ({ className, ...props}: ConfirmationDescriptionProps) => { const { state } = useConfirmation(); if (state !== "approval-requested") { return null; } return ( <div data-slot="ai-confirmation-description" className={cn("col-start-2 text-[13px] text-foreground-2", className)} {...props} /> );};
export interface ConfirmationRequestProps { children?: ReactNode;}
export const ConfirmationRequest = ({ children }: ConfirmationRequestProps) => { const { state } = useConfirmation();
// Only show when approval is requested if (state !== "approval-requested") { return null; }
return children;};
export interface ConfirmationAcceptedProps { children?: ReactNode;}
export const ConfirmationAccepted = ({ children,}: ConfirmationAcceptedProps) => { const { approval, state } = useConfirmation();
// Only show when approved and in response states if ( !approval?.approved || (state !== "approval-responded" && state !== "output-denied" && state !== "output-available") ) { return null; }
return ( <span className="inline-flex items-center gap-2 text-success-text"> {children} </span> );};
export interface ConfirmationRejectedProps { children?: ReactNode;}
export const ConfirmationRejected = ({ children,}: ConfirmationRejectedProps) => { const { approval, state } = useConfirmation();
// Only show when rejected and in response states if ( approval?.approved !== false || (state !== "approval-responded" && state !== "output-denied" && state !== "output-available") ) { return null; }
return ( <span className="inline-flex items-center gap-2 text-muted-foreground"> {children} </span> );};
export type ConfirmationActionsProps = ComponentProps<"div">;
export const ConfirmationActions = ({ className, ...props}: ConfirmationActionsProps) => { const { state } = useConfirmation();
// Only show when approval is requested if (state !== "approval-requested") { return null; }
return ( <div data-slot="ai-confirmation-actions" className={cn("col-start-2 mt-2.5 flex items-center gap-2", className)} {...props} /> );};
export type ConfirmationActionProps = ComponentProps<typeof Button>;
export const ConfirmationAction = ({ elevation, size = "sm", ...props}: ConfirmationActionProps) => { const ctx = useContext(ConfirmationContext); return ( <Button data-slot="ai-confirmation-action" elevation={elevation ?? ctx?.elevation} size={size} type="button" {...props} /> );};<script setup lang="ts">import type { ToolUIPart } from "ai"import type { HTMLAttributes } from "vue"import type { ToolUIPartApproval } from "./context"import { ShieldIcon } from "@lucide/vue"import { computed, provide, toRef } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { Alert } from "@/registry/edmi/ui/alert"import type { Elevation } from "@/registry/edmi/ui/elevation"import { ConfirmationKey } from "./context"
const props = withDefaults(defineProps<{ approval?: ToolUIPartApproval state: ToolUIPart["state"] /** ✦ depth of the action buttons (they inherit it, each can override). The alert itself stays flat. */ elevation?: Elevation class?: HTMLAttributes["class"]}>(), { elevation: undefined,})
provide(ConfirmationKey, { approval: toRef(props, "approval"), state: toRef(props, "state"), elevation: toRef(props, "elevation"),})
const hidden = computed(() => !props.approval || props.state === "input-streaming" || props.state === "input-available")</script>
<template> <template v-if="!hidden"> <!-- Request = warning alert with Approve / Reject (leading icon in the `icon` slot). --> <Alert v-if="state === 'approval-requested'" data-slot="ai-confirmation" :data-state="state" variant="warning" :class="props.class" > <slot name="icon"> <ShieldIcon /> </slot> <slot /> </Alert> <!-- Once answered it collapses to a plain status line. --> <div v-else data-slot="ai-confirmation" :data-state="state" :class="cn('flex items-center gap-2 text-[13.5px]', props.class)" > <slot /> </div> </template></template><script setup lang="ts">import { useConfirmationContext } from "./context"
const { approval, state } = useConfirmationContext()</script>
<template> <span v-if="approval?.approved && (state === 'approval-responded' || state === 'output-denied' || state === 'output-available')" class="inline-flex items-center gap-2 text-success-text" > <slot /> </span></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import type { ButtonVariants } from "@/registry/edmi/ui/button"import { inject } from "vue"import { Button } from "@/registry/edmi/ui/button"import type { Elevation } from "@/registry/edmi/ui/elevation"import { ConfirmationKey } from "./context"
// `elevation` falls back to the Confirmation's (an explicit value here wins).const props = withDefaults(defineProps<{ variant?: ButtonVariants["variant"] size?: ButtonVariants["size"] elevation?: Elevation class?: HTMLAttributes["class"]}>(), { size: "sm", elevation: undefined,})
const ctx = inject(ConfirmationKey, null)</script>
<template> <Button data-slot="ai-confirmation-action" type="button" :variant="props.variant" :size="props.size" :elevation="props.elevation ?? ctx?.elevation.value" :class="props.class" > <slot /> </Button></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { useConfirmationContext } from "./context"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const { state } = useConfirmationContext()</script>
<template> <div v-if="state === 'approval-requested'" data-slot="ai-confirmation-actions" :class="cn('col-start-2 mt-2.5 flex items-center gap-2', props.class)" > <slot /> </div></template><script setup lang="ts">// ✦ Edmi addition: muted second line under the request title (only while the request is open).import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { useConfirmationContext } from "./context"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const { state } = useConfirmationContext()</script>
<template> <div v-if="state === 'approval-requested'" data-slot="ai-confirmation-description" :class="cn('col-start-2 text-[13px] text-foreground-2', props.class)" > <slot /> </div></template><script setup lang="ts">import { useConfirmationContext } from "./context"
const { approval, state } = useConfirmationContext()</script>
<template> <span v-if="approval?.approved === false && (state === 'approval-responded' || state === 'output-denied' || state === 'output-available')" class="inline-flex items-center gap-2 text-muted-foreground" > <slot /> </span></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 type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { useConfirmationContext } from "./context"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()
const { state } = useConfirmationContext()</script>
<template> <div data-slot="ai-confirmation-title" :class="cn( state === 'approval-requested' ? 'col-start-2 font-medium text-foreground' : 'flex items-center gap-2', props.class, )" > <slot /> </div></template>import type { ToolUIPart } from "ai";import type { InjectionKey, Ref } from "vue";import { inject } from "vue";import type { Elevation } from "@/registry/edmi/ui/elevation";
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"]>; elevation: Ref<Elevation | undefined>;}
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 ConfirmationDescription } from "./ConfirmationDescription.vue";export { default as ConfirmationRejected } from "./ConfirmationRejected.vue";export { default as ConfirmationRequest } from "./ConfirmationRequest.vue";export { default as ConfirmationTitle } from "./ConfirmationTitle.vue";export * from "./context";<script lang="ts"> import type { Snippet } from "svelte"; import { useConfirmation } from "./use-confirmation.svelte.js";
let { children }: { children?: Snippet } = $props();
const confirmation = useConfirmation(); const show = $derived( !!confirmation.approval?.approved && (confirmation.state === "approval-responded" || confirmation.state === "output-denied" || confirmation.state === "output-available") );</script>
{#if show} <span class="inline-flex items-center gap-2 text-success-text">{@render children?.()}</span>{/if}<script lang="ts"> import { Button, type ButtonProps } from "$lib/registry/ui/button/index.js"; import { useConfirmationOptional } from "./use-confirmation.svelte.js";
// `elevation` falls back to the Confirmation's (an explicit value here wins). let { elevation, size = "sm", children, ...restProps }: ButtonProps = $props();
const confirmation = useConfirmationOptional();</script>
<Button data-slot="ai-confirmation-action" type="button" elevation={elevation && elevation !== "auto" ? elevation : confirmation?.elevation} {size} {...restProps}> {@render children?.()}</Button><script lang="ts"> import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements"; import { useConfirmation } from "./use-confirmation.svelte.js";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> = $props();
const confirmation = useConfirmation();</script>
{#if confirmation.state === "approval-requested"} <div data-slot="ai-confirmation-actions" class={cn("col-start-2 mt-2.5 flex items-center gap-2", className)} {...restProps} > {@render children?.()} </div>{/if}<!-- ✦ Edmi addition: muted second line under the request title (only while the request is open). --><script lang="ts"> import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements"; import { useConfirmation } from "./use-confirmation.svelte.js";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> = $props();
const confirmation = useConfirmation();</script>
{#if confirmation.state === "approval-requested"} <div data-slot="ai-confirmation-description" class={cn("col-start-2 text-[13px] text-foreground-2", className)} {...restProps} > {@render children?.()} </div>{/if}<script lang="ts"> import type { Snippet } from "svelte"; import { useConfirmation } from "./use-confirmation.svelte.js";
let { children }: { children?: Snippet } = $props();
const confirmation = useConfirmation(); const show = $derived( confirmation.approval?.approved === false && (confirmation.state === "approval-responded" || confirmation.state === "output-denied" || confirmation.state === "output-available") );</script>
{#if show} <span class="inline-flex items-center gap-2 text-muted-foreground">{@render children?.()}</span>{/if}<script lang="ts"> import type { Snippet } from "svelte"; import { useConfirmation } from "./use-confirmation.svelte.js";
let { children }: { children?: Snippet } = $props();
const confirmation = useConfirmation();</script>
{#if confirmation.state === "approval-requested"} {@render children?.()}{/if}<script lang="ts"> import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements"; import { useConfirmation } from "./use-confirmation.svelte.js";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> = $props();
const confirmation = useConfirmation();</script>
<div data-slot="ai-confirmation-title" class={cn( confirmation.state === "approval-requested" ? "col-start-2 font-medium text-foreground" : "flex items-center gap-2", className )} {...restProps}> {@render children?.()}</div><script lang="ts"> import ShieldIcon from "phosphor-svelte/lib/Shield"; import { cn } from "$lib/utils.js"; import { Alert } from "$lib/registry/ui/alert/index.js"; import type { Elevation } from "$lib/registry/ui/elevation/index.js"; import type { ToolUIPart } from "ai"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements"; import { setConfirmationContext, type ToolUIPartApproval } from "./use-confirmation.svelte.js";
let { class: className, approval, state, elevation = "auto", icon, children, ...restProps }: Omit<HTMLAttributes<HTMLDivElement>, "children"> & { approval?: ToolUIPartApproval; state: ToolUIPart["state"]; /** ✦ depth of the action buttons (they inherit it, each can override). The alert itself stays flat. */ elevation?: Elevation; /** Leading icon of the request. Defaults to a shield. */ icon?: Snippet; children?: Snippet; } = $props();
setConfirmationContext({ get approval() { return approval; }, get state() { return state; }, get elevation() { return elevation; }, });
const hidden = $derived(!approval || state === "input-streaming" || state === "input-available");</script>
{#if !hidden} <!-- Request = warning alert with Approve / Reject; once answered it collapses to a plain status line. --> {#if state === "approval-requested"} <Alert data-slot="ai-confirmation" data-state={state} variant="warning" class={className} {...restProps}> {#if icon} {@render icon()} {:else} <ShieldIcon /> {/if} {@render children?.()} </Alert> {:else} <div data-slot="ai-confirmation" data-state={state} class={cn("flex items-center gap-2 text-[13.5px]", className)} {...restProps} > {@render children?.()} </div> {/if}{/if}import Root from "./confirmation.svelte";import Accepted from "./confirmation-accepted.svelte";import Action from "./confirmation-action.svelte";import Actions from "./confirmation-actions.svelte";import Description from "./confirmation-description.svelte";import Rejected from "./confirmation-rejected.svelte";import Request from "./confirmation-request.svelte";import Title from "./confirmation-title.svelte";
export * from "./use-confirmation.svelte.js";export { Accepted, Accepted as ConfirmationAccepted, Action, Action as ConfirmationAction, Actions, Actions as ConfirmationActions, Description, Description as ConfirmationDescription, Rejected, Rejected as ConfirmationRejected, Request, Request as ConfirmationRequest, Root, // Root as Confirmation, Title, Title as ConfirmationTitle,};import type { ToolUIPart } from "ai";import { getContext, setContext } from "svelte";import type { Elevation } from "$lib/registry/ui/elevation/index.js";
export type ToolUIPartApproval = | { id: string; approved?: never; reason?: never } | { id: string; approved: boolean; reason?: string } | undefined;
/** Getter object so consumers stay reactive. */export interface ConfirmationContextValue { readonly approval: ToolUIPartApproval; readonly state: ToolUIPart["state"]; readonly elevation: Elevation | undefined;}
const KEY = Symbol("ai-confirmation");
export function setConfirmationContext(value: ConfirmationContextValue) { return setContext(KEY, value);}
export function useConfirmation(): ConfirmationContextValue { const ctx = getContext<ConfirmationContextValue | undefined>(KEY); if (!ctx) throw new Error( "Confirmation components must be used within <Confirmation>", ); return ctx;}
export function useConfirmationOptional(): | ConfirmationContextValue | undefined { return getContext<ConfirmationContextValue | undefined>(KEY);}Elevation ✦
Section titled “Elevation ✦”See the Elevation guide for the levels, roles and layered mode.
The request is a flat warning alert; elevation goes to the action buttons (flat, raised +1, floating +2, sunken -1 like a Button) and each ConfirmationAction can override it.
Flat (0)
Raised (+1)
Floating (+2)
Flat (0)
Raised (+1)
Floating (+2)
Flat (0)
Raised (+1)
Floating (+2)
import { Confirmation, ConfirmationAction, ConfirmationActions, ConfirmationDescription, ConfirmationRequest, ConfirmationTitle,} from "@/components/ai/confirmation";
const levels = [ { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" },] as const;
export default function Demo() { return ( <div className="flex w-full max-w-2xl flex-col gap-5"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <Confirmation elevation={value} approval={{ id: "1" }} state="approval-requested" className="max-w-md" > <ConfirmationTitle> <ConfirmationRequest>Run rebalance on MAG4?</ConfirmationRequest> </ConfirmationTitle> <ConfirmationDescription> Sells 0.42 NVDAx and buys MSFTx + AAPLx. Max slippage 1%. </ConfirmationDescription> <ConfirmationActions> <ConfirmationAction>Approve</ConfirmationAction> <ConfirmationAction variant="outline">Reject</ConfirmationAction> </ConfirmationActions> </Confirmation> </div> ))} </div> );}<script setup lang="ts">import { Confirmation, ConfirmationAction, ConfirmationActions, ConfirmationDescription, ConfirmationRequest, ConfirmationTitle,} from "@/components/ai/confirmation";
const levels = [{ value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }];</script>
<template> <div class="flex w-full max-w-2xl flex-col gap-5"> <div v-for="level in levels" :key="level.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ level.label }}</p> <Confirmation :elevation="level.value" :approval="{ id: '1' }" state="approval-requested" class="max-w-md"> <ConfirmationTitle> <ConfirmationRequest>Run rebalance on MAG4?</ConfirmationRequest> </ConfirmationTitle> <ConfirmationDescription> Sells 0.42 NVDAx and buys MSFTx + AAPLx. Max slippage 1%. </ConfirmationDescription> <ConfirmationActions> <ConfirmationAction>Approve</ConfirmationAction> <ConfirmationAction variant="outline">Reject</ConfirmationAction> </ConfirmationActions> </Confirmation> </div> </div></template><script lang="ts"> import { Confirmation, ConfirmationAction, ConfirmationActions, ConfirmationDescription, ConfirmationRequest, ConfirmationTitle, } from "$lib/components/ai/confirmation";
const levels = [{ value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }] as const;</script>
<div class="flex w-full max-w-2xl flex-col gap-5"> {#each levels as level (level.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{level.label}</p> <Confirmation elevation={level.value} approval={{ id: "1" }} state="approval-requested" class="max-w-md"> <ConfirmationTitle> <ConfirmationRequest>Run rebalance on MAG4?</ConfirmationRequest> </ConfirmationTitle> <ConfirmationDescription> Sells 0.42 NVDAx and buys MSFTx + AAPLx. Max slippage 1%. </ConfirmationDescription> <ConfirmationActions> <ConfirmationAction>Approve</ConfirmationAction> <ConfirmationAction variant="outline">Reject</ConfirmationAction> </ConfirmationActions> </Confirmation> </div> {/each}</div>| Part | Prop | Type | Default |
|---|---|---|---|
| Confirmation | approval, state |
AI SDK tool approval and ToolUIPart["state"] |
|
| Confirmation | elevation |
"auto" | "sunken" | "flat" | "raised" | "floating" (the action buttons inherit it) |
"auto" |
| Confirmation | icon |
ReactNode |
shield |
| ConfirmationTitle | request title (put ConfirmationRequest / Accepted / Rejected inside) |
||
| ConfirmationDescription | muted second line, only while the request is open | ||
| ConfirmationActions | row of actions, only while the request is open | ||
| ConfirmationAction | elevation, variant, size |
Button props | size="sm" |
✦ Edmi additions
Section titled “✦ Edmi additions”- The open request is a
warningalert; once answered it collapses to a plain status line (green check when approved, muted cross when rejected). ConfirmationDescription✦ is the muted line under the title.elevation✦ is passed to everyConfirmationAction(each can override it); the alert itself stays flat.