ChatTool (ChatTool)

축소 가능한 AI 도구 호출 상태를 표시합니다.

Usage

ChatTool 구성 요소는 "구성 요소 검색" 또는 "문서 읽기"와 같이 AI 도구 호출 상태를 표시하는 축소 가능한 블록을 렌더링합니다. 기본 슬롯이 제공되면 축소 가능하게 되어 도구 출력을 표시합니다.

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

text Prop을 사용하여 공구 상태 텍스트를 설정합니다.

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

접미어

suffix prop 을 사용하여 주 레이블 뒤에 보조 텍스트를 표시합니다.

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

Streaming 스트리밍

streaming Prop을 사용하여 도구가 활성화되어 있음을 나타냅니다. 텍스트에는 Shimmer 애니메이션이 표시됩니다.

<template>
  <UChatTool streaming text="Searching components..." />
</template>
@nuxt/ui/utils/ai의 isToolStreaming 유틸리티를 사용하여 도구 부품이 여전히 실행 중인지 확인합니다. 사용자의 승인을 기다리는 동안 false를 반환합니다.

시머

스트리밍 시 트리거 레이블은 ChatShimmer 구성 요소를 사용합니다. shimmer 소품을 사용하여 duration 및 spread를 사용자 정의합니다.

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

Icon

icon prop을 사용하여 트리거 옆에 Icon 구성 요소를 표시합니다.

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

Loading 중

loading prop을 사용하여 로딩 표시기를 표시하고, loading-icon prop을 사용하여 로딩 아이콘을 사용자 정의합니다.

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

loading 아이콘

loading-icon 소품을 사용하여 로드 아이콘을 사용자 정의합니다. 기본값은 i-lucide-loader-circle입니다.

<template>
  <UChatTool loading loading-icon="i-lucide-loader" text="Searching components..." />
</template>
ui.icons.loading 키 아래의 app.config.ts에서 이 아이콘을 전역적으로 사용자 정의할 수 있습니다.
ui.icons.loading 키 아래의 vite.config.ts에서 이 아이콘을 전역적으로 사용자 지정할 수 있습니다.

Chevron 차량

chevron 소품을 사용하여 갈매기 모양 아이콘의 위치를 변경합니다.

chevron가 icon와 leading로 설정되면 아이콘이 마우스 위에 있고 열려 있을 때 chevron과 스왑됩니다.
<template>
  <UChatTool chevron="leading" icon="i-lucide-search" text="Searched components">
    Tool output content
  </UChatTool>
</template>

Chevron 아이콘 아이콘

chevron-icon 소품을 사용하여 chevron Icon를 사용자 정의합니다. 기본값은 i-lucide-chevron-down입니다.

<template>
  <UChatTool chevron-icon="i-lucide-arrow-down" text="Searched components">
    Tool output content
  </UChatTool>
</template>
ui.icons.chevronDown 키 아래의 app.config.ts에서 이 아이콘을 전역적으로 사용자 정의할 수 있습니다.
ui.icons.chevronDown 키 아래의 vite.config.ts에서 이 아이콘을 전역적으로 사용자 정의할 수 있습니다.

변형

variant 소품을 사용하여 비주얼 스타일을 변경합니다. 기본값은 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"} 동작

actions prop을 사용하여 트리거 아래에 Button 목록을 표시하며, 실행 전에 사용자 확인이 필요한 도구에 유용합니다.

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

examples 예제

Chat 개요 페이지에서 설치 지침, 서버 설정 및 사용법 예를 확인하십시오.

승인 흐름 포함: badge{label="4.10+" class="align-text-top"}

actions 소품을 사용하여 [AI SDK](https://ai-sdk.dev/docs/agents/tool-approvalsxph22x로 도구 승인 플로우를 구축합니다. 도구 부품이 approval-requested 상태에 있으면 승인 및 거부 작업을 표시하고 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>
@nuxt/ui/utils/ai의 isToolApprovalPending 유틸리티를 사용하여 보류 중인 승인을 감지하면 isToolStreaming는 이 상태에서 false를 반환합니다.
<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 (### 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; }

슬롯

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

Emits

Event Type
update:open
[value: boolean]

테마

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'
                ]
              }
            }
          }
        }
      }
    })
  ]
})

변경 로그

No recent changes