ChatTool Soon
Usage
The ChatTool component renders a collapsible block that displays AI tool invocation status, such as "Searching components" or "Reading documentation". When a default slot is provided, it becomes collapsible to reveal tool output.
$ pnpm run lint > eslint . ✔ No lint errors found.
<script setup lang="ts">
const streaming = ref(true)
const result = ref(`$ pnpm run lint
> eslint .
✔ No lint errors found.
`)
let timer: ReturnType<typeof setTimeout> | undefined
onMounted(() => {
timer = setTimeout(() => {
streaming.value = false
}, 5000)
})
onUnmounted(() => {
clearTimeout(timer)
})
</script>
<template>
<UChatTool
:text="streaming ? 'Running lint checks' : 'Lint checks completed'"
suffix="cd, pnpm run"
:streaming="streaming"
icon="i-lucide-terminal"
variant="card"
chevron="leading"
class="w-80"
>
<pre language="bash" v-text="result" />
</UChatTool>
</template>
Text
Use the text prop to set the tool status text.
<template>
<UChatTool text="Searched components" />
</template>
Suffix
Use the suffix prop to display secondary text after the main label.
<template>
<UChatTool text="Reading component" suffix="Button" />
</template>
Streaming
Use the streaming prop to indicate the tool is actively running. The text displays a shimmer animation.
<template>
<UChatTool streaming text="Searching components..." />
</template>
isToolStreaming utility from @nuxt/ui/utils/ai to determine if a tool part is still running.Shimmer
When streaming, the trigger label uses the ChatShimmer component. Use the shimmer prop to customize its duration and spread.
<template>
<UChatTool
streaming
text="Searching components..."
:shimmer="{
duration: 2,
spread: 2
}"
/>
</template>
Icon
Use the icon prop to display an Icon component next to the trigger.
<template>
<UChatTool icon="i-lucide-search" text="Searched components" />
</template>
Loading
Use the loading prop to show a loading indicator. Use the loading-icon prop to customize the loading icon.
<template>
<UChatTool loading text="Searching components..." />
</template>
Loading Icon
Use the loading-icon prop to customize the loading icon. Defaults to i-lucide-loader-circle.
<template>
<UChatTool loading loading-icon="i-lucide-loader" text="Searching components..." />
</template>
Chevron
Use the chevron prop to change the position of the chevron icon.
chevron is set to leading with an icon, the icon swaps with the chevron on hover and when open.<template>
<UChatTool chevron="leading" icon="i-lucide-search" text="Searched components">
Tool output content
</UChatTool>
</template>
Chevron Icon
Use the chevron-icon prop to customize the chevron Icon. Defaults to i-lucide-chevron-down.
<template>
<UChatTool chevron-icon="i-lucide-arrow-down" text="Searched components">
Tool output content
</UChatTool>
</template>
Variant
Use the variant prop to change the visual style. Defaults to inline.
<template>
<UChatTool variant="card" text="Searched components" icon="i-lucide-search" chevron="trailing">
Tool output content
</UChatTool>
</template>
Examples
API
Props
| Prop | Default | Type |
|---|---|---|
text | stringThe text content to display. | |
suffix | stringThe suffix text displayed after the main text. | |
icon | anyThe icon displayed next to the trigger. | |
loading | false | boolean Whether the tool is in a loading state. |
loadingIcon | appConfig.ui.icons.loading | anyThe icon displayed when loading. |
streaming | false | boolean Whether the tool content is currently streaming. |
variant | 'inline' | "card" | "inline"The visual variant of the tool display. |
chevron | 'trailing' | "leading" | "trailing"The position of the chevron icon. |
chevronIcon | appConfig.ui.icons.chevronDown | anyThe icon displayed as the chevron. |
shimmer | Partial<Omit<ChatShimmerProps, "text">>Customize the | |
disabled | boolean When | |
open | undefined | boolean The controlled open state of the collapsible. Can be binded with |
defaultOpen | boolean The open state of the collapsible when it is initially rendered. | |
unmountOnHide | false | boolean When |
ui | { root?: ClassNameValue; trigger?: ClassNameValue; leading?: ClassNameValue; leadingIcon?: ClassNameValue; chevronIcon?: ClassNameValue; label?: ClassNameValue; suffix?: ClassNameValue; trailingIcon?: ClassNameValue; content?: ClassNameValue; body?: ClassNameValue; } |
Slots
| Slot | Type |
|---|---|
default | { open: boolean; } |
Emits
| Event | Type |
|---|---|
update:open | [value: boolean] |
Theme
export default defineAppConfig({
ui: {
chatTool: {
slots: {
root: '',
trigger: [
'group flex w-full items-center gap-1.5 text-muted text-sm disabled:cursor-default disabled:hover:text-muted hover:text-default focus-visible:outline-offset-2 focus-visible:outline-primary min-w-0',
'transition-colors'
],
leading: 'relative size-4 shrink-0',
leadingIcon: 'size-4 shrink-0',
chevronIcon: 'size-4 shrink-0 group-data-[state=open]:rotate-180 transition-transform duration-200',
label: 'truncate',
suffix: 'text-dimmed ms-1',
trailingIcon: 'size-4 shrink-0 group-data-[state=open]:rotate-180 transition-transform duration-200',
content: 'data-[state=open]:animate-[collapsible-down_200ms_ease-out] data-[state=closed]:animate-[collapsible-up_200ms_ease-out] overflow-hidden',
body: 'text-sm text-dimmed whitespace-pre-wrap'
},
variants: {
variant: {
inline: {
body: 'pt-2'
},
card: {
root: 'rounded-md ring ring-default overflow-hidden',
trigger: 'px-2 py-1',
trailingIcon: 'ms-auto',
body: 'border-t border-default p-2 max-h-[200px] overflow-y-auto'
}
},
chevron: {
leading: {
leadingIcon: 'group-hover:opacity-0'
},
trailing: ''
},
loading: {
true: {
leadingIcon: 'animate-spin'
}
},
alone: {
false: {
leadingIcon: [
'absolute inset-0 group-data-[state=open]:opacity-0',
'transition-opacity duration-200'
],
chevronIcon: [
'absolute inset-0 opacity-0 group-hover:opacity-100 group-data-[state=open]:opacity-100',
'transition-[rotate,opacity] duration-200'
]
}
}
},
defaultVariants: {
variant: 'inline'
}
}
}
})
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
ui: {
chatTool: {
slots: {
root: '',
trigger: [
'group flex w-full items-center gap-1.5 text-muted text-sm disabled:cursor-default disabled:hover:text-muted hover:text-default focus-visible:outline-offset-2 focus-visible:outline-primary min-w-0',
'transition-colors'
],
leading: 'relative size-4 shrink-0',
leadingIcon: 'size-4 shrink-0',
chevronIcon: 'size-4 shrink-0 group-data-[state=open]:rotate-180 transition-transform duration-200',
label: 'truncate',
suffix: 'text-dimmed ms-1',
trailingIcon: 'size-4 shrink-0 group-data-[state=open]:rotate-180 transition-transform duration-200',
content: 'data-[state=open]:animate-[collapsible-down_200ms_ease-out] data-[state=closed]:animate-[collapsible-up_200ms_ease-out] overflow-hidden',
body: 'text-sm text-dimmed whitespace-pre-wrap'
},
variants: {
variant: {
inline: {
body: 'pt-2'
},
card: {
root: 'rounded-md ring ring-default overflow-hidden',
trigger: 'px-2 py-1',
trailingIcon: 'ms-auto',
body: 'border-t border-default p-2 max-h-[200px] overflow-y-auto'
}
},
chevron: {
leading: {
leadingIcon: 'group-hover:opacity-0'
},
trailing: ''
},
loading: {
true: {
leadingIcon: 'animate-spin'
}
},
alone: {
false: {
leadingIcon: [
'absolute inset-0 group-data-[state=open]:opacity-0',
'transition-opacity duration-200'
],
chevronIcon: [
'absolute inset-0 opacity-0 group-hover:opacity-100 group-data-[state=open]:opacity-100',
'transition-[rotate,opacity] duration-200'
]
}
}
},
defaultVariants: {
variant: 'inline'
}
}
}
})
]
})