ChatReasoning Soon

Display a collapsible AI reasoning or thinking process.

Usage

The ChatReasoning component renders a collapsible block that displays AI reasoning or thinking content. It auto-opens during streaming and auto-closes after.

<script setup lang="ts">
const streaming = ref(false)
const text = ref('')

async function simulateStreaming() {
  streaming.value = true
  text.value = ''

  const content =
    'The user is asking about Vue components. I should explain the Composition API pattern and how it relates to their question about reactive state management. Let me think about the best way to structure this response.\n\nFirst, I need to consider the key differences between the Options API and Composition API. The Composition API was introduced in Vue 3 to address limitations of the Options API when building large-scale applications.\n\nFor reactive state management specifically, the Composition API offers ref() for primitive values and reactive() for objects.'

  for (const char of content) {
    text.value += char
    await new Promise((resolve) => setTimeout(resolve, 10))
  }

  streaming.value = false
}

onMounted(simulateStreaming)
</script>

<template>
  <UChatReasoning :text="text" :streaming="streaming" class="w-80" />
</template>
The body content uses the useScrollShadow composable to apply fade shadows when overflowing.

Streaming

Use the streaming prop to indicate active reasoning. The component auto-opens when streaming starts and auto-closes when it ends.

Let me think about this...
<template>
  <UChatReasoning streaming text="Let me think about this..." />
</template>
Use the isStreamingPart utility from @nuxt/ui/utils/ai to determine if a specific message part is currently being streamed.

Shimmer

When streaming, the trigger label uses the ChatShimmer component. Use the shimmer prop to customize its duration and spread.

Let me think about this...
<template>
  <UChatReasoning
    streaming
    text="Let me think about this..."
    :shimmer="{
      duration: 2,
      spread: 2
    }"
  />
</template>

Icon

Use the icon prop to display an Icon component next to the trigger.

<template>
  <UChatReasoning icon="i-lucide-brain" text="The user is asking about Vue components..." />
</template>

Chevron

Use the chevron prop to change the position of the chevron icon.

When chevron is set to leading with an icon, the icon swaps with the chevron on hover and when open.
<template>
  <UChatReasoning
    chevron="leading"
    icon="i-lucide-brain"
    text="The user is asking about Vue components..."
  />
</template>

Chevron Icon

Use the chevron-icon prop to customize the chevron Icon. Defaults to i-lucide-chevron-down.

<template>
  <UChatReasoning
    chevron-icon="i-lucide-arrow-down"
    text="The user is asking about Vue components..."
  />
</template>
You can customize this icon globally in your app.config.ts under ui.icons.chevronDown key.
You can customize this icon globally in your vite.config.ts under ui.icons.chevronDown key.

Examples

Check the ChatMessages documentation for server API setup and installation instructions.

Within a page

Use the ChatReasoning component inside the ChatMessages #content slot to display reasoning blocks alongside regular message parts.

The AI SDK provides the isReasoningUIPart helper to identify reasoning parts in a message.

pages/[id].vue
<script setup lang="ts">
import { isReasoningUIPart, isTextUIPart } from 'ai'
import { Chat } from '@ai-sdk/vue'
import { isStreamingPart } from '@nuxt/ui/utils/ai'

const input = ref('')

const chat = new Chat({
  onError(error) {
    console.error(error)
  }
})

function onSubmit() {
  chat.sendMessage({ text: input.value })

  input.value = ''
}
</script>

<template>
  <UDashboardPanel>
    <template #body>
      <UContainer>
        <UChatMessages
          :messages="chat.messages"
          :status="chat.status"
        >
          <template #content="{ message }">
            <template
              v-for="(part, index) in message.parts"
              :key="`${message.id}-${part.type}-${index}`"
            >
              <UChatReasoning
                v-if="isReasoningUIPart(part)"
                :text="part.text"
                :streaming="isStreamingPart(message, index, chat)"
              >
                <MDC
                  :value="part.text"
                  :cache-key="`reasoning-${message.id}-${index}`"
                  class="*:first:mt-0 *:last:mb-0"
                />
              </UChatReasoning>

              <MDC
                v-else-if="isTextUIPart(part)"
                :value="part.text"
                :cache-key="`${message.id}-${index}`"
                class="*:first:mt-0 *:last:mb-0"
              />
            </template>
          </template>
        </UChatMessages>
      </UContainer>
    </template>

    <template #footer>
      <UContainer class="pb-4 sm:pb-6">
        <UChatPrompt
          v-model="input"
          :error="chat.error"
          @submit="onSubmit"
        >
          <UChatPromptSubmit
            :status="chat.status"
            @stop="chat.stop()"
            @reload="chat.regenerate()"
          />
        </UChatPrompt>
      </UContainer>
    </template>
  </UDashboardPanel>
</template>
Check out the source code of our AI Chat template on GitHub for a real-life example.

API

Props

Prop Default Type
text string

The reasoning text content to display.

streamingfalseboolean

Whether the reasoning content is currently streaming.

duration number

The duration in seconds that the AI spent reasoning. If not provided, it will be calculated automatically based on streaming time.

iconany

The icon displayed next to the trigger.

chevron'trailing' "leading" | "trailing"

The position of the chevron icon.

chevronIconappConfig.ui.icons.chevronDownany

The icon displayed as the chevron.

autoCloseDelay500 number

The delay in milliseconds before auto-closing when streaming ends. Set to 0 to disable auto-close.

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

Customize the ChatShimmer component when streaming.

disabledboolean

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

openundefinedboolean

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

defaultOpenboolean

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

unmountOnHidefalseboolean

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

ui { root?: ClassNameValue; trigger?: ClassNameValue; leading?: ClassNameValue; leadingIcon?: ClassNameValue; chevronIcon?: ClassNameValue; label?: ClassNameValue; trailingIcon?: ClassNameValue; content?: ClassNameValue; body?: ClassNameValue; }

Slots

Slot Type
default{ open: boolean; }

Emits

Event Type
update:open[value: boolean]

Theme

app.config.ts
export default defineAppConfig({
  ui: {
    chatReasoning: {
      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',
        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: 'max-h-[200px] pt-2 overflow-y-auto text-sm text-dimmed whitespace-pre-wrap'
      },
      variants: {
        chevron: {
          leading: {
            leadingIcon: 'group-hover:opacity-0'
          },
          trailing: ''
        },
        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-[opacity,transform] duration-200'
            ]
          }
        }
      }
    }
  }
})
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: {
        chatReasoning: {
          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',
            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: 'max-h-[200px] pt-2 overflow-y-auto text-sm text-dimmed whitespace-pre-wrap'
          },
          variants: {
            chevron: {
              leading: {
                leadingIcon: 'group-hover:opacity-0'
              },
              trailing: ''
            },
            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-[opacity,transform] duration-200'
                ]
              }
            }
          }
        }
      }
    })
  ]
})

Changelog

No recent changes