Mostrar un estado de invocación de herramienta de IA plegable.

xph0000xUso

El componente ChatTool representa un bloque plegable que muestra el estado de invocación de la herramienta de IA, como "Buscar componentes" o "Leer documentación". Cuando se proporciona una ranura predeterminada, se vuelve plegable para revelar la salida de la herramienta.

<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>

Text (Edición española)

Utilice el prop text para establecer el texto de estado de la herramienta.

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

sufijo

Utilice el prop suffix para mostrar texto secundario después de la etiqueta principal.

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

Streaming en Español

Utilice el prop streaming para indicar que la herramienta se está ejecutando activamente.El texto muestra una animación de brillo.

<template>
  <UChatTool streaming text="Searching components..." />
</template>
Utilice la utilidad isToolStreaming de @nuxt/ui/utils/ai para determinar si una parte de la herramienta todavía está en ejecución. Devuelve false cuando la herramienta está esperando la aprobación del usuario.

Climatización

Cuando se transmite, la etiqueta de activación utiliza el componente ChatShimmer. Use el prop shimmer para personalizar su duration y spread.

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

Icono

Utilice el prop icon para mostrar un componente Icon junto al disparador.

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

Cargando

Utilice el accesorio loading para mostrar un indicador de carga. Use el accesorio loading-icon para personalizar el icono de carga.

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

Icono de carga

Utilice el prop loading-icon para personalizar el icono de carga.

<template>
  <UChatTool loading loading-icon="i-lucide-loader" text="Searching components..." />
</template>
Puede personalizar este icono globalmente en su app.config.ts bajo la tecla ui.icons.loading.
Puede personalizar este icono de forma global en su Xph110x bajo la tecla Xph111x.

Xph112xChevron (Edición española)

Utilice el accesorio chevron para cambiar la posición del icono de chevron.

Cuando chevron se establece en leading con un icon, el icono se intercambia con el chevron en el cursor y cuando se abre.
<template>
  <UChatTool chevron="leading" icon="i-lucide-search" text="Searched components">
    Tool output content
  </UChatTool>
</template>

XPH133xChevron Icono de diseño

Utilice el prop chevron-icon para personalizar el chevron Icon.

<template>
  <UChatTool chevron-icon="i-lucide-arrow-down" text="Searched components">
    Tool output content
  </UChatTool>
</template>
Puede personalizar este icono de forma global en su XPH155x bajo la tecla XPH156x.
Puede personalizar este icono globalmente en su vite.config.ts bajo la tecla ui.icons.chevronDown.

Variante

Utilice el prop variant para cambiar el estilo visual.

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

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

Utilice el prop actions para mostrar una lista de Button debajo del disparador, útil para herramientas que requieren una confirmación del usuario antes de ejecutarse.

$ 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>

Ejemplos

Consulte la página de descripción general de Chat para obtener instrucciones de instalación, configuración del servidor y ejemplos de uso.

Con flujo de aprobación: badge{label="4.10+" class="align-text-top"}

Utilice el prop actions para crear un flujo de aprobación de herramientas con el SDK](https://ai-sdk.dev/docs/agents/tool-approvals) [AI. Cuando una pieza de herramienta está en el estado approval-requested, muestre las acciones aprobar y denegar y responda con 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>
Utilice la utilidad isToolApprovalPending de @nuxt/ui/utils/ai para detectar una aprobación pendiente, isToolStreaming devuelve false en este estado.
<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 (accesorios)

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 en línea

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

Emisiones

Event Type
update:open
[value: boolean]

Temas

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 (Edición española)

No recent changes