chattool

Afficher un état d'invocation d'outil AI pliable.

Utilisation

Le composant ChatTool rend un bloc pliable qui affiche le statut d'appel de l'outil d'IA, tel que "Recherche de composants" ou "Lecture de documentation". Lorsqu 'un emplacement par défaut est fourni, il devient pliable pour révéler la sortie de l'outil.

<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>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'

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>

Texte écrit

Utilisez la prop text pour définir le texte d'état de l'outil.

<template>
  <UChatTool text="Searched components" />
</template>

suffixe

Utilisez le prop suffix pour afficher le texte secondaire après l'étiquette principale.

<template>
  <UChatTool text="Reading component" suffix="Button" />
</template>

Streaming écouter

Utilisez la prop streaming pour indiquer que l'outil est activement en cours d'exécution. Le texte affiche une animation de miroitement.

<template>
  <UChatTool streaming text="Searching components..." />
</template>
Utilisez l'utilitaire isToolStreaming de @nuxt/ui/utils/ai pour déterminer si une pièce d'outil est toujours en cours d'exécution. Il renvoie false lorsque l'outil est en attente d'une approbation de l'utilisateur.

écran

Lors du streaming, l'étiquette de déclenchement utilise le composant ChatShimmer. Utilisez la prop shimmer pour personnaliser ses duration et spread.

<template>
  <UChatTool
    streaming
    text="Searching components..."
    :shimmer="{
      duration: 2,
      spread: 2
    }"
  />
</template>

icône

Utilisez la prop icon pour afficher un composant Icon à côté du déclencheur.

<template>
  <UChatTool icon="i-lucide-search" text="Searched components" />
</template>

Chargement

Utilisez le prop loading pour afficher un indicateur de chargement. Utilisez le prop loading-icon pour personnaliser l'icône de chargement.

<template>
  <UChatTool loading text="Searching components..." />
</template>

Icône de chargement

Utilisez la prop loading-icon pour personnaliser l'icône de chargement. Par défaut, i-lucide-loader-circle.

<template>
  <UChatTool loading loading-icon="i-lucide-loader" text="Searching components..." />
</template>
Vous pouvez personnaliser cette icône globalement dans votre app.config.ts sous la touche ui.icons.loading.
Vous pouvez personnaliser cette icône globalement dans votre vite.config.ts sous la touche ui.icons.loading.

chevron

Utilisez le prop chevron pour modifier la position de l'icône du chevron.

Lorsque chevron est réglé sur leading avec un icon, l'icône change avec le chevron en survol et lorsqu 'il est ouvert.
<template>
  <UChatTool chevron="leading" icon="i-lucide-search" text="Searched components">
    Tool output content
  </UChatTool>
</template>

Icône ### Chevron

Utilisez la prop chevron-icon pour personnaliser le chevron Icon.

<template>
  <UChatTool chevron-icon="i-lucide-arrow-down" text="Searched components">
    Tool output content
  </UChatTool>
</template>
Vous pouvez personnaliser cette icône globalement dans votre app.config.ts sous la touche ui.icons.chevronDown.
Vous pouvez personnaliser cette icône globalement dans votre vite.config.ts sous la touche ui.icons.chevronDown.

Variant

Utilisez la prop variant pour modifier le style visuel. Par défaut, inline.

<template>
  <UChatTool variant="card" text="Searched components" icon="i-lucide-search" chevron="trailing">
    Tool output content
  </UChatTool>
</template>

Actions: badge{label="4.10+" class="align-text-top"}

Utilisez la prop actions pour afficher une liste de Button sous le déclencheur, utile pour les outils qui nécessitent une confirmation de l'utilisateur avant de s'exécuter.

$ pnpm run lint
<template>
  <UChatTool
    :actions="[
      {
        label: 'Approve'
      },
      {
        label: 'Deny',
        color: 'neutral',
        variant: 'soft'
      }
    ]"
    text="Run terminal command"
    variant="card"
    icon="i-lucide-terminal"
  >
    $ pnpm run lint
  </UChatTool>
</template>

Exemples

Consultez la page d'aperçu Chat pour les instructions d'installation, la configuration du serveur et les exemples d'utilisation.

Avec flux d'approbation: badge{label="4.10+" class="align-text-top"}

Utilisez la prop actions pour créer un flux d'approbation d'outil avec le AI SDK. Lorsqu 'une pièce d'outil est dans l'état approval-requested, affichez les actions approuver et refuser et répondez avec addToolApprovalResponse.

$ pnpm run lint
<script setup lang="ts">
import type { ButtonProps } from '@nuxt/ui'

const state = ref<'approval-requested' | 'output-available' | 'output-denied'>('approval-requested')
const result = ref('')

const text = computed(() => {
  if (state.value === 'approval-requested') return 'Run terminal command'
  if (state.value === 'output-denied') return 'Command cancelled'
  return result.value ? 'Ran terminal command' : 'Running terminal command'
})

const output = computed(() => result.value || '$ pnpm run lint')

const actions = computed<ButtonProps[] | undefined>(() => {
  if (state.value !== 'approval-requested') return undefined

  return [
    { label: 'Approve', onClick: onApprove },
    { label: 'Deny', color: 'neutral', variant: 'soft', onClick: onDeny }
  ]
})

let timer: ReturnType<typeof setTimeout> | undefined

function onApprove() {
  state.value = 'output-available'

  timer = setTimeout(() => {
    result.value = `$ pnpm run lint

> eslint .

✔ No lint errors found.
`
  }, 2000)
}

function onDeny() {
  state.value = 'output-denied'
}

function reset() {
  clearTimeout(timer)
  state.value = 'approval-requested'
  result.value = ''
}

onUnmounted(() => {
  clearTimeout(timer)
})
</script>

<template>
  <div class="flex flex-col items-start gap-4">
    <UChatTool
      :text="text"
      icon="i-lucide-terminal"
      variant="card"
      :streaming="state === 'output-available' && !result"
      :actions="actions"
      class="w-80"
    >
      <pre language="bash" v-text="output" />
    </UChatTool>

    <UButton
      v-if="state !== 'approval-requested'"
      label="Reset"
      color="neutral"
      variant="link"
      size="xs"
      icon="i-lucide-rotate-ccw"
      class="p-0 absolute top-4 right-4"
      @click="reset"
    />
  </div>
</template>
<script setup lang="ts">
import { ref, computed, onUnmounted } from 'vue'
import type { ButtonProps } from '@nuxt/ui'

const state = ref<'approval-requested' | 'output-available' | 'output-denied'>('approval-requested')
const result = ref('')

const text = computed(() => {
  if (state.value === 'approval-requested') return 'Run terminal command'
  if (state.value === 'output-denied') return 'Command cancelled'
  return result.value ? 'Ran terminal command' : 'Running terminal command'
})

const output = computed(() => result.value || '$ pnpm run lint')

const actions = computed<ButtonProps[] | undefined>(() => {
  if (state.value !== 'approval-requested') return undefined

  return [
    { label: 'Approve', onClick: onApprove },
    { label: 'Deny', color: 'neutral', variant: 'soft', onClick: onDeny }
  ]
})

let timer: ReturnType<typeof setTimeout> | undefined

function onApprove() {
  state.value = 'output-available'

  timer = setTimeout(() => {
    result.value = `$ pnpm run lint

> eslint .

✔ No lint errors found.
`
  }, 2000)
}

function onDeny() {
  state.value = 'output-denied'
}

function reset() {
  clearTimeout(timer)
  state.value = 'approval-requested'
  result.value = ''
}

onUnmounted(() => {
  clearTimeout(timer)
})
</script>

<template>
  <div class="flex flex-col items-start gap-4">
    <UChatTool
      :text="text"
      icon="i-lucide-terminal"
      variant="card"
      :streaming="state === 'output-available' && !result"
      :actions="actions"
      class="w-80"
    >
      <pre language="bash" v-text="output" />
    </UChatTool>

    <UButton
      v-if="state !== 'approval-requested'"
      label="Reset"
      color="neutral"
      variant="link"
      size="xs"
      icon="i-lucide-rotate-ccw"
      class="p-0 absolute top-4 right-4"
      @click="reset"
    />
  </div>
</template>
Utilisez l'utilitaire isToolApprovalPending de @nuxt/ui/utils/ai pour détecter une approbation en attente, isToolStreaming renvoie false dans cet état.
<script setup lang="ts">
import { useChat } from '@ai-sdk/vue'
import { lastAssistantMessageIsCompleteWithApprovalResponses } from 'ai'

const { messages, addToolApprovalResponse } = useChat({
  sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses
})
</script>

<template>
  <UChatTool
    v-if="isToolUIPart(part)"
    :text="getToolName(part)"
    :streaming="isToolStreaming(part)"
    :actions="part.state === 'approval-requested' ? [
      { label: 'Approve', onClick: () => addToolApprovalResponse({ id: part.approval.id, approved: true }) },
      { label: 'Deny', color: 'neutral', variant: 'ghost', onClick: () => addToolApprovalResponse({ id: part.approval.id, approved: false }) }
    ] : undefined"
  />
</template>

API

Props

Prop Default Type
text
string

The text content to display.

suffix
string

The suffix text displayed after the main text.

icon
any

The icon displayed next to the trigger.

loading
false
boolean

Whether the tool is in a loading state.

loadingIcon
appConfig.ui.icons.loading
any

The icon displayed when loading.

streaming
false
boolean

Whether the tool content is currently streaming.

variant
'inline'
"inline" | "card"

The visual variant of the tool display.

chevron
'trailing'
"leading" | "trailing"

The position of the chevron icon.

chevronIcon
appConfig.ui.icons.chevronDown
any

The icon displayed as the chevron.

shimmer
Partial<Omit<ChatShimmerProps, "text">>

Customize the ChatShimmer component when streaming.

actions
ButtonProps[]

Display a list of actions below the trigger, useful for tool approval flows. { size: 'xs' }

disabled
boolean

When true, prevents the user from interacting with the collapsible.

open
undefined
boolean

The controlled open state of the collapsible. Can be binded with v-model.

defaultOpen
boolean

The open state of the collapsible when it is initially rendered.
Use when you do not need to control its open state.

unmountOnHide
false
boolean

When true, the element will be unmounted on closed state.

ui
{ root?: SlotClass; trigger?: SlotClass; leading?: SlotClass; leadingIcon?: SlotClass; chevronIcon?: SlotClass; label?: SlotClass; suffix?: SlotClass; trailingIcon?: SlotClass; content?: SlotClass; body?: SlotClass; actions?: SlotClass; }

Slots

Slot Type
default
{ open: boolean; }
actions
{}

Emits

Event Type
update:open
[value: boolean]

Thème

app.config.ts
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 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 ease-out motion-reduce:transition-none',
        label: 'truncate',
        suffix: 'text-dimmed ms-1',
        trailingIcon: 'size-4 shrink-0 group-data-[state=open]:rotate-180 transition-transform duration-200 ease-out motion-reduce:transition-none',
        content: 'data-[state=open]:animate-[collapsible-down_200ms_var(--ease-out)] data-[state=closed]:animate-[collapsible-up_200ms_var(--ease-out)] data-[state=closed]:overflow-hidden',
        body: 'text-sm text-dimmed whitespace-pre-wrap',
        actions: 'flex items-center justify-end gap-1.5'
      },
      variants: {
        variant: {
          inline: {
            trigger: 'rounded-sm outline-primary/25 focus-visible:outline-3',
            body: 'pt-2',
            actions: 'pt-2'
          },
          card: {
            root: 'rounded-md ring ring-default overflow-hidden outline-primary/25 has-focus-visible:outline-3 has-focus-visible:ring-primary',
            trigger: 'px-2 py-1 focus:outline-none',
            trailingIcon: 'ms-auto',
            body: 'border-t border-default p-2 max-h-[200px] overflow-y-auto focus:outline-none',
            actions: 'border-t border-default p-2'
          }
        },
        chevron: {
          leading: '',
          trailing: ''
        },
        loading: {
          true: {
            leadingIcon: 'animate-spin'
          }
        },
        alone: {
          false: {
            leadingIcon: [
              'absolute inset-0 group-hover:opacity-0 group-data-[state=open]:opacity-0',
              'transition-opacity duration-200 ease-out'
            ],
            chevronIcon: [
              'absolute inset-0 opacity-0 group-hover:opacity-100 group-data-[state=open]:opacity-100',
              'transition-[rotate,opacity] duration-200 ease-out motion-reduce:transition-none'
            ]
          }
        }
      }
    }
  }
})
vite.config.ts
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 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 ease-out motion-reduce:transition-none',
            label: 'truncate',
            suffix: 'text-dimmed ms-1',
            trailingIcon: 'size-4 shrink-0 group-data-[state=open]:rotate-180 transition-transform duration-200 ease-out motion-reduce:transition-none',
            content: 'data-[state=open]:animate-[collapsible-down_200ms_var(--ease-out)] data-[state=closed]:animate-[collapsible-up_200ms_var(--ease-out)] data-[state=closed]:overflow-hidden',
            body: 'text-sm text-dimmed whitespace-pre-wrap',
            actions: 'flex items-center justify-end gap-1.5'
          },
          variants: {
            variant: {
              inline: {
                trigger: 'rounded-sm outline-primary/25 focus-visible:outline-3',
                body: 'pt-2',
                actions: 'pt-2'
              },
              card: {
                root: 'rounded-md ring ring-default overflow-hidden outline-primary/25 has-focus-visible:outline-3 has-focus-visible:ring-primary',
                trigger: 'px-2 py-1 focus:outline-none',
                trailingIcon: 'ms-auto',
                body: 'border-t border-default p-2 max-h-[200px] overflow-y-auto focus:outline-none',
                actions: 'border-t border-default p-2'
              }
            },
            chevron: {
              leading: '',
              trailing: ''
            },
            loading: {
              true: {
                leadingIcon: 'animate-spin'
              }
            },
            alone: {
              false: {
                leadingIcon: [
                  'absolute inset-0 group-hover:opacity-0 group-data-[state=open]:opacity-0',
                  'transition-opacity duration-200 ease-out'
                ],
                chevronIcon: [
                  'absolute inset-0 opacity-0 group-hover:opacity-100 group-data-[state=open]:opacity-100',
                  'transition-[rotate,opacity] duration-200 ease-out motion-reduce:transition-none'
                ]
              }
            }
          }
        }
      }
    })
  ]
})

Changelog

No recent changes