Tool
Tool call card with a state badge, collapsible input parameters and output or error.
{
"symbol": "NVDAx",
"window": "30d"
}{
"price": 188.2,
"change": 0.024
}{
"symbol": "NVDAx",
"window": "30d"
}Rate limit: retry in 20 s
{
"symbol": "NVDAx",
"window": "30d"
}{
"price": 188.2,
"change": 0.024
}{
"symbol": "NVDAx",
"window": "30d"
}Rate limit: retry in 20 s
{
"symbol": "NVDAx",
"window": "30d"
}{
"price": 188.2,
"change": 0.024
}{
"symbol": "NVDAx",
"window": "30d"
}Rate limit: retry in 20 s
{
"symbol": "NVDAx",
"window": "30d"
}{
"symbol": "NVDAx",
"window": "30d"
}{
"symbol": "NVDAx",
"window": "30d"
}{
"symbol": "NVDAx",
"window": "30d"
}{
"symbol": "NVDAx",
"window": "30d"
}import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput,} from "@/components/ai/tool";
const input = { symbol: "NVDAx", window: "30d" };
export default function Demo() { return ( <div className="flex w-full max-w-md flex-col gap-3"> <Tool defaultOpen> <ToolHeader type="tool-get_prices" state="output-available" /> <ToolContent> <ToolInput input={input} /> <ToolOutput output={{ price: 188.2, change: 0.024 }} errorText={undefined} /> </ToolContent> </Tool> <Tool defaultOpen> <ToolHeader type="tool-get_prices" state="output-error" /> <ToolContent> <ToolInput input={input} /> <ToolOutput output={undefined} errorText="Rate limit: retry in 20 s" /> </ToolContent> </Tool> {( [ "input-streaming", "input-available", "approval-requested", "approval-responded", "output-denied", ] as const ).map((state) => ( <Tool key={state}> <ToolHeader type="tool-get_prices" state={state} /> <ToolContent> <ToolInput input={input} /> </ToolContent> </Tool> ))} </div> );}<script setup lang="ts">import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput } from "@/components/ai/tool";
const input = { symbol: "NVDAx", window: "30d" };const states = [ "input-streaming", "input-available", "approval-requested", "approval-responded", "output-denied",] as const;</script>
<template> <div class="flex w-full max-w-md flex-col gap-3"> <Tool default-open> <ToolHeader type="tool-get_prices" state="output-available" /> <ToolContent> <ToolInput :input="input" /> <ToolOutput :output="{ price: 188.2, change: 0.024 }" :error-text="undefined" /> </ToolContent> </Tool> <Tool default-open> <ToolHeader type="tool-get_prices" state="output-error" /> <ToolContent> <ToolInput :input="input" /> <ToolOutput :output="undefined" error-text="Rate limit: retry in 20 s" /> </ToolContent> </Tool> <Tool v-for="state in states" :key="state"> <ToolHeader type="tool-get_prices" :state="state" /> <ToolContent> <ToolInput :input="input" /> </ToolContent> </Tool> </div></template><script lang="ts"> import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput } from "$lib/components/ai/tool";
const input = { symbol: "NVDAx", window: "30d" }; const states = [ "input-streaming", "input-available", "approval-requested", "approval-responded", "output-denied", ] as const;</script>
<div class="flex w-full max-w-md flex-col gap-3"> <Tool open> <ToolHeader type="tool-get_prices" state="output-available" /> <ToolContent> <ToolInput {input} /> <ToolOutput output={{ price: 188.2, change: 0.024 }} /> </ToolContent> </Tool> <Tool open> <ToolHeader type="tool-get_prices" state="output-error" /> <ToolContent> <ToolInput {input} /> <ToolOutput errorText="Rate limit: retry in 20 s" /> </ToolContent> </Tool> {#each states as state (state)} <Tool> <ToolHeader type="tool-get_prices" {state} /> <ToolContent> <ToolInput {input} /> </ToolContent> </Tool> {/each}</div>Built on badge and collapsible.
Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/ai-toolpnpm dlx shadcn@latest add @edmi-ui/ai-toolyarn dlx shadcn@latest add @edmi-ui/ai-toolbunx shadcn@latest add @edmi-ui/ai-toolPick 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:
badge,collapsible,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-toolpnpm dlx shadcn-vue@latest add @edmi-ui/ai-toolyarn dlx shadcn-vue@latest add @edmi-ui/ai-toolbunx shadcn-vue@latest add @edmi-ui/ai-toolPick 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/vue reka-uiTerminal window pnpm add ai @lucide/vue reka-uiTerminal window yarn add ai @lucide/vue reka-uiTerminal window bun add ai @lucide/vue reka-uiAdd the Edmi components this one builds on:
badge,collapsible,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-tool.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-tool.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-tool.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-tool.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:
badge,collapsible,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput,} from "@/components/ai/tool";
<div className="flex w-full max-w-md flex-col gap-3"> <Tool defaultOpen> <ToolHeader type="tool-get_prices" state="output-available" /> <ToolContent> <ToolInput /> <ToolOutput output={{ price: 188.2, change: 0.024 }} errorText={undefined} /> </ToolContent> </Tool> <Tool defaultOpen> <ToolHeader type="tool-get_prices" state="output-error" /> <ToolContent> <ToolInput /> <ToolOutput output={undefined} errorText="Rate limit: retry in 20 s" /> </ToolContent> </Tool></div><script setup lang="ts">import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput,} from "@/components/ai/tool";</script>
<template> <div class="flex w-full max-w-md flex-col gap-3"> <Tool default-open> <ToolHeader type="tool-get_prices" state="output-available" /> <ToolContent> <ToolInput /> <ToolOutput :output="{ price: 188.2, change: 0.024 }" :error-text="undefined" /> </ToolContent> </Tool> <Tool default-open> <ToolHeader type="tool-get_prices" state="output-error" /> <ToolContent> <ToolInput /> <ToolOutput :output="undefined" error-text="Rate limit: retry in 20 s" /> </ToolContent> </Tool> <Tool :key="state"> <ToolHeader type="tool-get_prices" :state="state" /> <ToolContent> <ToolInput /> </ToolContent> </Tool> </div></template><script lang="ts"> import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput, } from "$lib/components/ai/tool";</script>
<div class="flex w-full max-w-md flex-col gap-3"> <Tool open> <ToolHeader type="tool-get_prices" state="output-available" /> <ToolContent> <ToolInput /> <ToolOutput output={{ price: 188.2, change: 0.024 }} /> </ToolContent> </Tool> <Tool open> <ToolHeader type="tool-get_prices" state="output-error" /> <ToolContent> <ToolInput /> <ToolOutput errorText="Rate limit: retry in 20 s" /> </ToolContent> </Tool> {#each states as state (state)} <Tool> <ToolHeader type="tool-get_prices" {state} /> <ToolContent> <ToolInput /> </ToolContent> </Tool> {/each}</div>Component source
Section titled “Component source”"use client";
import type { DynamicToolUIPart, ToolUIPart } from "ai";import { cn } from "cn";import type { ComponentProps, ReactNode } from "react";import { isValidElement } from "react";import { ShieldIcon, CheckIcon, ClockIcon, CircleIcon, CheckCircleIcon, XIcon, XCircleIcon, WrenchIcon, CaretDownIcon } from "@phosphor-icons/react";import { Badge } from "@/registry/edmi/ui/badge";import { Collapsible, CollapsibleContent, CollapsibleTrigger,} from "@/registry/edmi/ui/collapsible";import { type Elevation, SurfaceProvider, useElevation,} from "@/registry/edmi/ui/elevation";
export type ToolProps = ComponentProps<typeof Collapsible> & { /** ✦ depth of the card (sunken -1, flat 0, raised +1, floating +2). */ elevation?: Elevation;};
// surface role: same faces as the ui cardconst toolElevation = { sunken: "border-sk-bd bg-sk-bg shadow-sunken", flat: "", raised: "border-transparent shadow-raised", floating: "border-transparent shadow-floating",};
export const Tool = ({ className, elevation, ...props }: ToolProps) => { const level = useElevation(elevation, "surface"); return ( <SurfaceProvider level={level}> <Collapsible data-slot="ai-tool" className={cn( "group/tool not-prose w-full overflow-hidden rounded-xl border border-border bg-card text-card-foreground", toolElevation[level], className, )} {...props} /> </SurfaceProvider> );};
export type ToolPart = ToolUIPart | DynamicToolUIPart;
export type ToolHeaderProps = { title?: string; className?: string;} & ( | { type: ToolUIPart["type"]; state: ToolUIPart["state"]; toolName?: never } | { type: DynamicToolUIPart["type"]; state: DynamicToolUIPart["state"]; toolName: string; });
const statusLabels: Record<ToolPart["state"], string> = { "approval-requested": "Awaiting approval", "approval-responded": "Responded", "input-available": "Running", "input-streaming": "Pending", "output-available": "Completed", "output-denied": "Denied", "output-error": "Error",};
const statusVariants: Record< ToolPart["state"], "secondary" | "info" | "warning" | "success" | "outline" | "destructive"> = { "approval-requested": "warning", "approval-responded": "secondary", "input-available": "info", "input-streaming": "secondary", "output-available": "success", "output-denied": "outline", "output-error": "destructive",};
const statusIcons: Record<ToolPart["state"], ReactNode> = { "approval-requested": ( <ShieldIcon /> ), "approval-responded": ( <CheckIcon /> ), "input-available": ( <ClockIcon className="animate-pulse" /> ), "input-streaming": ( <CircleIcon /> ), "output-available": ( <CheckCircleIcon /> ), "output-denied": ( <XIcon /> ), "output-error": ( <XCircleIcon /> ),};
export const getStatusBadge = (status: ToolPart["state"]) => ( <Badge className="gap-1" data-slot="ai-tool-status" shape="pill" variant={statusVariants[status]} > {statusIcons[status]} {statusLabels[status]} </Badge>);
export const ToolHeader = ({ className, title, type, state, toolName, ...props}: ToolHeaderProps) => { const derivedName = type === "dynamic-tool" ? toolName : type.split("-").slice(1).join("-");
return ( <CollapsibleTrigger data-slot="ai-tool-header" className={cn( "group/tool-trigger flex w-full items-center gap-2.5 px-3.5 py-2.5 text-left text-[13.5px] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring", className, )} {...props} > <WrenchIcon className="size-[15px] shrink-0 text-muted-foreground" /> <span className="font-mono text-[12.5px]">{title ?? derivedName}</span> {getStatusBadge(state)} <CaretDownIcon className="ml-auto size-3.5 shrink-0 text-muted-foreground transition-transform group-data-[panel-open]/tool-trigger:rotate-180" /> </CollapsibleTrigger> );};
export type ToolContentProps = ComponentProps<typeof CollapsibleContent>;
export const ToolContent = ({ className, children, ...props}: ToolContentProps) => ( <CollapsibleContent data-slot="ai-tool-content" className={cn( "h-(--collapsible-panel-height) overflow-hidden outline-none transition-[height] duration-200 ease-out data-[ending-style]:h-0 data-[starting-style]:h-0", className, )} {...props} > <div className="space-y-3 border-t border-border px-3.5 py-3 text-[13px]"> {children} </div> </CollapsibleContent>);
const labelClass = "mb-1.5 font-mono text-[11px] font-medium tracking-[0.8px] text-muted-foreground uppercase";const preClass = "m-0 overflow-x-auto rounded-lg bg-muted px-3 py-2.5 font-mono text-xs leading-[1.6] whitespace-pre text-foreground";
export type ToolInputProps = ComponentProps<"div"> & { input: ToolPart["input"];};
export const ToolInput = ({ className, input, ...props }: ToolInputProps) => ( <div data-slot="ai-tool-input" className={cn("overflow-hidden", className)} {...props} > <div className={labelClass}>Parameters</div> <pre className={preClass}>{JSON.stringify(input, null, 2)}</pre> </div>);
export type ToolOutputProps = ComponentProps<"div"> & { output: ToolPart["output"]; errorText: ToolPart["errorText"];};
export const ToolOutput = ({ className, output, errorText, ...props}: ToolOutputProps) => { if (!(output || errorText)) { return null; }
let body: ReactNode; if (errorText) { body = ( <pre className={cn(preClass, "bg-destructive-soft text-destructive-text")} > {errorText} </pre> ); } else if (isValidElement(output)) { body = <div className="overflow-x-auto">{output}</div>; } else if (typeof output === "string") { body = <pre className={preClass}>{output}</pre>; } else { body = <pre className={preClass}>{JSON.stringify(output, null, 2)}</pre>; }
return ( <div data-slot="ai-tool-output" className={className} {...props}> <div className={labelClass}>{errorText ? "Error" : "Result"}</div> {body} </div> );};<script setup lang="ts">import type { CollapsibleRootProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { Collapsible } from "@/registry/edmi/ui/collapsible"import { type Elevation, provideSurface, useElevation } from "@/registry/edmi/ui/elevation"
interface ToolProps extends /* @vue-ignore */ CollapsibleRootProps { class?: HTMLAttributes["class"] /** ✦ depth of the card: sunken -1, flat 0, raised +1, floating +2. */ elevation?: Elevation}
const props = withDefaults(defineProps<ToolProps>(), { elevation: undefined,})
// surface role: same faces as the ui cardconst toolElevation = { sunken: 'border-sk-bd bg-sk-bg shadow-sunken', flat: '', raised: 'border-transparent shadow-raised', floating: 'border-transparent shadow-floating',}
const level = useElevation(() => props.elevation, 'surface')provideSurface(() => level.value)</script>
<template> <!-- Collapsible props (open, defaultOpen, update:open) fall through as attrs. --> <Collapsible data-slot="ai-tool" :class="cn( 'group/tool not-prose w-full overflow-hidden rounded-xl border border-border bg-card text-card-foreground', toolElevation[level], props.class, )" > <slot /> </Collapsible></template><script setup lang="ts">import type { HTMLAttributes } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { CollapsibleContent } from "@/registry/edmi/ui/collapsible"
const props = defineProps<{ class?: HTMLAttributes["class"]}>()</script>
<template> <CollapsibleContent data-slot="ai-tool-content" :class="cn( 'overflow-hidden outline-none data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down', props.class, )" > <div class="space-y-3 border-t border-border px-3.5 py-3 text-[13px]"> <slot /> </div> </CollapsibleContent></template><script setup lang="ts">import type { DynamicToolUIPart, ToolUIPart } from "ai"import type { HTMLAttributes } from "vue"import { ChevronDownIcon, SettingsIcon } from "@lucide/vue"import { computed } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { CollapsibleTrigger } from "@/registry/edmi/ui/collapsible"import ToolStatusBadge from "./ToolStatusBadge.vue"
type ToolHeaderProps = { title?: string class?: HTMLAttributes["class"]} & ( | { type: ToolUIPart["type"], state: ToolUIPart["state"], toolName?: never } | { type: DynamicToolUIPart["type"], state: DynamicToolUIPart["state"], toolName: string })
const props = defineProps<ToolHeaderProps>()
const derivedName = computed(() => props.type === "dynamic-tool" ? props.toolName : props.type.split("-").slice(1).join("-"),)</script>
<template> <CollapsibleTrigger data-slot="ai-tool-header" :class="cn( 'group/tool-trigger flex w-full items-center gap-2.5 px-3.5 py-2.5 text-left text-[13.5px] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring', props.class, )" > <SettingsIcon class="size-[15px] shrink-0 text-muted-foreground" /> <span class="font-mono text-[12.5px]">{{ props.title ?? derivedName }}</span> <ToolStatusBadge :state="props.state" /> <ChevronDownIcon class="ml-auto size-3.5 shrink-0 text-muted-foreground transition-transform group-data-[state=open]/tool-trigger:rotate-180" /> </CollapsibleTrigger></template><script setup lang="ts">import type { DynamicToolUIPart, ToolUIPart } from "ai"import type { HTMLAttributes } from "vue"import { computed } from "vue"import { cn } from "@/registry/edmi/lib/utils"
type ToolPart = ToolUIPart | DynamicToolUIPart
const props = defineProps<{ input: ToolPart["input"] class?: HTMLAttributes["class"]}>()
const formatted = computed(() => JSON.stringify(props.input, null, 2))</script>
<template> <div data-slot="ai-tool-input" :class="cn('overflow-hidden', props.class)"> <div class="mb-1.5 font-mono text-[11px] font-medium tracking-[0.8px] text-muted-foreground uppercase"> Parameters </div> <pre class="m-0 overflow-x-auto rounded-lg bg-muted px-3 py-2.5 font-mono text-xs leading-[1.6] whitespace-pre text-foreground">{{ formatted }}</pre> </div></template><script setup lang="ts">import type { DynamicToolUIPart, ToolUIPart } from "ai"import type { HTMLAttributes } from "vue"import { computed, isVNode } from "vue"import { cn } from "@/registry/edmi/lib/utils"
type ToolPart = ToolUIPart | DynamicToolUIPart
const props = defineProps<{ output: ToolPart["output"] errorText: ToolPart["errorText"] class?: HTMLAttributes["class"]}>()
const show = computed(() => Boolean(props.output || props.errorText))const isNode = computed(() => isVNode(props.output))const text = computed(() => { if (typeof props.output === "string") return props.output return JSON.stringify(props.output, null, 2)})
const pre = "m-0 overflow-x-auto rounded-lg bg-muted px-3 py-2.5 font-mono text-xs leading-[1.6] whitespace-pre text-foreground"</script>
<template> <div v-if="show" data-slot="ai-tool-output" :class="props.class"> <div class="mb-1.5 font-mono text-[11px] font-medium tracking-[0.8px] text-muted-foreground uppercase"> {{ props.errorText ? "Error" : "Result" }} </div> <pre v-if="props.errorText" :class="cn(pre, 'bg-destructive-soft text-destructive-text')">{{ props.errorText }}</pre> <div v-else-if="isNode" class="overflow-x-auto"> <component :is="() => props.output" /> </div> <pre v-else :class="pre">{{ text }}</pre> </div></template><script setup lang="ts">import type { DynamicToolUIPart, ToolUIPart } from "ai"import type { Component } from "vue"import { CheckIcon, CircleCheckIcon, CircleIcon, ClockIcon, OctagonXIcon, ShieldIcon, XIcon,} from "@lucide/vue"import { computed } from "vue"import { Badge } from "@/registry/edmi/ui/badge"
type ToolPart = ToolUIPart | DynamicToolUIParttype State = ToolPart["state"]
const props = defineProps<{ state: State}>()
const labels: Record<State, string> = { "approval-requested": "Awaiting approval", "approval-responded": "Responded", "input-available": "Running", "input-streaming": "Pending", "output-available": "Completed", "output-denied": "Denied", "output-error": "Error",}
const variants: Record<State, "secondary" | "info" | "warning" | "success" | "outline" | "destructive"> = { "approval-requested": "warning", "approval-responded": "secondary", "input-available": "info", "input-streaming": "secondary", "output-available": "success", "output-denied": "outline", "output-error": "destructive",}
const icons: Record<State, Component> = { "approval-requested": ShieldIcon, "approval-responded": CheckIcon, "input-available": ClockIcon, "input-streaming": CircleIcon, "output-available": CircleCheckIcon, "output-denied": XIcon, "output-error": OctagonXIcon,}
const icon = computed(() => icons[props.state])</script>
<template> <Badge data-slot="ai-tool-status" shape="pill" class="gap-1" :variant="variants[state]"> <component :is="icon" :class="state === 'input-available' && 'animate-pulse'" /> {{ labels[state] }} </Badge></template>export { default as Tool } from "./Tool.vue";export { default as ToolContent } from "./ToolContent.vue";export { default as ToolHeader } from "./ToolHeader.vue";export { default as ToolInput } from "./ToolInput.vue";export { default as ToolOutput } from "./ToolOutput.vue";export { default as ToolStatusBadge } from "./ToolStatusBadge.vue";import Root from "./tool.svelte";import Content from "./tool-content.svelte";import Header from "./tool-header.svelte";import Input from "./tool-input.svelte";import Output from "./tool-output.svelte";import StatusBadge from "./tool-status-badge.svelte";
export type * from "./types.js";export { Content, Content as ToolContent, Header, Header as ToolHeader, Input, Input as ToolInput, Output, Output as ToolOutput, Root, // Root as Tool, StatusBadge, StatusBadge as ToolStatusBadge,};<script lang="ts"> import { cn } from "$lib/utils.js"; import { CollapsibleContent } from "$lib/registry/ui/collapsible/index.js"; import type { ComponentProps } from "svelte";
let { class: className, children, ...restProps }: ComponentProps<typeof CollapsibleContent> = $props();</script>
<CollapsibleContent data-slot="ai-tool-content" class={cn( "overflow-hidden outline-none data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down", className )} {...restProps}> <div class="space-y-3 border-t border-border px-3.5 py-3 text-[13px]"> {@render children?.()} </div></CollapsibleContent><script lang="ts"> import WrenchIcon from "phosphor-svelte/lib/Wrench"; import CaretDownIcon from "phosphor-svelte/lib/CaretDown"; import { cn } from "$lib/utils.js"; import { CollapsibleTrigger } from "$lib/registry/ui/collapsible/index.js"; import type { ComponentProps } from "svelte"; import ToolStatusBadge from "./tool-status-badge.svelte"; import type { ToolState } from "./types.js";
let { class: className, title, type, state, toolName, ...restProps }: Omit<ComponentProps<typeof CollapsibleTrigger>, "children" | "title" | "type"> & { /** `tool-<name>` (static tools) or `dynamic-tool` (then pass `toolName`). */ type: string; state: ToolState; title?: string; toolName?: string; } = $props();
const derivedName = $derived( type === "dynamic-tool" ? toolName : type.split("-").slice(1).join("-") );</script>
<CollapsibleTrigger data-slot="ai-tool-header" class={cn( "group/tool-trigger flex w-full items-center gap-2.5 px-3.5 py-2.5 text-left text-[13.5px] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring", className )} {...restProps}> <WrenchIcon class="size-[15px] shrink-0 text-muted-foreground" /> <span class="font-mono text-[12.5px]">{title ?? derivedName}</span> <ToolStatusBadge {state} /> <CaretDownIcon class="ml-auto size-3.5 shrink-0 text-muted-foreground transition-transform group-data-[state=open]/tool-trigger:rotate-180" /></CollapsibleTrigger><script lang="ts"> import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, input, ...restProps }: HTMLAttributes<HTMLDivElement> & { input: unknown } = $props();
const formatted = $derived(JSON.stringify(input, null, 2));</script>
<div data-slot="ai-tool-input" class={cn("overflow-hidden", className)} {...restProps}> <div class="mb-1.5 font-mono text-[11px] font-medium tracking-[0.8px] text-muted-foreground uppercase"> Parameters </div> <pre class="m-0 overflow-x-auto rounded-lg bg-muted px-3 py-2.5 font-mono text-xs leading-[1.6] whitespace-pre text-foreground">{formatted}</pre></div><script lang="ts"> import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { class: className, output, errorText, ...restProps }: HTMLAttributes<HTMLDivElement> & { output?: unknown; errorText?: string } = $props();
const text = $derived(typeof output === "string" ? output : JSON.stringify(output, null, 2)); const pre = "m-0 overflow-x-auto rounded-lg bg-muted px-3 py-2.5 font-mono text-xs leading-[1.6] whitespace-pre text-foreground";</script>
{#if output || errorText} <div data-slot="ai-tool-output" class={className} {...restProps}> <div class="mb-1.5 font-mono text-[11px] font-medium tracking-[0.8px] text-muted-foreground uppercase"> {errorText ? "Error" : "Result"} </div> {#if errorText} <pre class={cn(pre, "bg-destructive-soft text-destructive-text")}>{errorText}</pre> {:else} <pre class={pre}>{text}</pre> {/if} </div>{/if}<script lang="ts"> import ShieldIcon from "phosphor-svelte/lib/Shield"; import CheckIcon from "phosphor-svelte/lib/Check"; import ClockIcon from "phosphor-svelte/lib/Clock"; import CircleIcon from "phosphor-svelte/lib/Circle"; import CheckCircleIcon from "phosphor-svelte/lib/CheckCircle"; import XIcon from "phosphor-svelte/lib/X"; import XCircleIcon from "phosphor-svelte/lib/XCircle"; import { Badge, type BadgeVariant } from "$lib/registry/ui/badge/index.js"; import type { ToolState } from "./types.js";
let { state }: { state: ToolState } = $props();
const labels: Record<ToolState, string> = { "approval-requested": "Awaiting approval", "approval-responded": "Responded", "input-available": "Running", "input-streaming": "Pending", "output-available": "Completed", "output-denied": "Denied", "output-error": "Error", };
const variants: Record<ToolState, BadgeVariant> = { "approval-requested": "warning", "approval-responded": "secondary", "input-available": "info", "input-streaming": "secondary", "output-available": "success", "output-denied": "outline", "output-error": "destructive", };</script>
<Badge data-slot="ai-tool-status" shape="pill" class="gap-1" variant={variants[state]}> {#if state === "approval-requested"} <ShieldIcon /> {:else if state === "approval-responded"} <CheckIcon /> {:else if state === "input-available"} <ClockIcon class="animate-pulse" /> {:else if state === "input-streaming"} <CircleIcon /> {:else if state === "output-available"} <CheckCircleIcon /> {:else if state === "output-denied"} <XIcon /> {:else} <XCircleIcon /> {/if} {labels[state]}</Badge><script lang="ts"> import { cn } from "$lib/utils.js"; import { Collapsible } from "$lib/registry/ui/collapsible/index.js"; import { type Elevation, setSurface, useElevation } from "$lib/registry/ui/elevation/index.js"; import type { ComponentProps } from "svelte";
let { class: className, elevation = "auto", open = $bindable(false), children, ...restProps }: ComponentProps<typeof Collapsible> & { /** ✦ depth of the card: sunken -1, flat 0, raised +1, floating +2. */ elevation?: Elevation; } = $props();
// surface role: same faces as the ui card const toolElevation = { sunken: "border-sk-bd bg-sk-bg shadow-sunken", flat: "", raised: "border-transparent shadow-raised", floating: "border-transparent shadow-floating", };
const level = useElevation(() => elevation, "surface"); setSurface(() => level.current);</script>
<Collapsible bind:open data-slot="ai-tool" class={cn( "group/tool not-prose w-full overflow-hidden rounded-xl border border-border bg-card text-card-foreground", toolElevation[level.current], className )} {...restProps}> {@render children?.()}</Collapsible>import type { DynamicToolUIPart, ToolUIPart } from "ai";
export type ToolPart = ToolUIPart | DynamicToolUIPart;export type ToolState = ToolPart["state"];Elevation ✦
Section titled “Elevation ✦”See the Elevation guide for the levels, roles and layered mode.
The card are flat by default and take the surface level in layered mode (raised). sunken (-1) is a soft inset, raised (+1) a bevel and floating (+2) a bevel with a soft drop.
Sunken (-1)
{
"symbol": "NVDAx",
"window": "30d"
}{
"price": 188.2,
"change": 0.024
}Flat (0)
{
"symbol": "NVDAx",
"window": "30d"
}{
"price": 188.2,
"change": 0.024
}Raised (+1)
{
"symbol": "NVDAx",
"window": "30d"
}{
"price": 188.2,
"change": 0.024
}Floating (+2)
{
"symbol": "NVDAx",
"window": "30d"
}{
"price": 188.2,
"change": 0.024
}Sunken (-1)
{
"symbol": "NVDAx",
"window": "30d"
}{
"price": 188.2,
"change": 0.024
}Flat (0)
{
"symbol": "NVDAx",
"window": "30d"
}{
"price": 188.2,
"change": 0.024
}Raised (+1)
{
"symbol": "NVDAx",
"window": "30d"
}{
"price": 188.2,
"change": 0.024
}Floating (+2)
{
"symbol": "NVDAx",
"window": "30d"
}{
"price": 188.2,
"change": 0.024
}Sunken (-1)
{
"symbol": "NVDAx",
"window": "30d"
}{
"price": 188.2,
"change": 0.024
}Flat (0)
{
"symbol": "NVDAx",
"window": "30d"
}{
"price": 188.2,
"change": 0.024
}Raised (+1)
{
"symbol": "NVDAx",
"window": "30d"
}{
"price": 188.2,
"change": 0.024
}Floating (+2)
{
"symbol": "NVDAx",
"window": "30d"
}{
"price": 188.2,
"change": 0.024
}import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput,} from "@/components/ai/tool";
const levels = [ { value: "sunken", label: "Sunken (-1)" }, { 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> <Tool elevation={value} defaultOpen className="max-w-md"> <ToolHeader type="tool-get_prices" state="output-available" /> <ToolContent> <ToolInput input={{ symbol: "NVDAx", window: "30d" }} /> <ToolOutput output={{ price: 188.2, change: 0.024 }} errorText={undefined} /> </ToolContent> </Tool> </div> ))} </div> );}<script setup lang="ts">import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput } from "@/components/ai/tool";
const levels = [{ value: "sunken", label: "Sunken (-1)" }, { 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> <Tool :elevation="level.value" default-open class="max-w-md"> <ToolHeader type="tool-get_prices" state="output-available" /> <ToolContent> <ToolInput :input="{ symbol: 'NVDAx', window: '30d' }" /> <ToolOutput :output="{ price: 188.2, change: 0.024 }" :error-text="undefined" /> </ToolContent> </Tool> </div> </div></template><script lang="ts"> import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput } from "$lib/components/ai/tool";
const levels = [{ value: "sunken", label: "Sunken (-1)" }, { 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> <Tool elevation={level.value} open class="max-w-md"> <ToolHeader type="tool-get_prices" state="output-available" /> <ToolContent> <ToolInput input={{ symbol: "NVDAx", window: "30d" }} /> <ToolOutput output={{ price: 188.2, change: 0.024 }} /> </ToolContent> </Tool> </div> {/each}</div>| Part | Prop | Type | Default |
|---|---|---|---|
| Tool | elevation |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
| Tool | open, defaultOpen, onOpenChange |
collapsible state | closed |
| ToolHeader | type, state, title, toolName |
AI SDK ToolUIPart / DynamicToolUIPart fields |
|
| ToolInput | input |
unknown (shown as JSON) |
|
| ToolOutput | output, errorText |
unknown, string |
|
| getStatusBadge | state |
returns the badge for one of the seven AI SDK states |
✦ Edmi additions
Section titled “✦ Edmi additions”- One badge colour per state: pending (secondary), running (info), awaiting approval (warning), responded (secondary), completed (success), denied (outline), error (destructive).
- The parameters and result render as plain mono blocks on
--muted(the error block on--destructive-soft), so the item does not depend onai-code-block. elevation✦ puts depth on the card (surface role); a nested card drops to flat.