推理

一个可折叠组件,显示 AI 推理内容,在流式传输期间自动打开,完成后关闭。

Reasoning 组件是一个可折叠组件,显示 AI 推理内容,在流式传输期间自动打开,完成后关闭。

使用 CLI 安装

AI Elements Vue
shadcn-vue CLI
npx ai-elements-vue@latest add reasoning

手动安装

将以下代码复制并粘贴到同一文件夹中。

Reasoning.vue
ReasoningTrigger.vue
ReasoningContent.vue
context.ts
index.ts
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { Collapsible } from '@repo/shadcn-vue/components/ui/collapsible'
import { cn } from '@repo/shadcn-vue/lib/utils'
import { useVModel } from '@vueuse/core'
import { computed, provide, ref, watch } from 'vue'
import { ReasoningKey } from './context'

interface Props {
  class?: HTMLAttributes['class']
  isStreaming?: boolean
  open?: boolean
  defaultOpen?: boolean
  duration?: number
}

const props = withDefaults(defineProps<Props>(), {
  isStreaming: false,
  defaultOpen: true,
  duration: undefined,
})

const emit = defineEmits<{
  (e: 'update:open', value: boolean): void
  (e: 'update:duration', value: number): void
}>()

const isOpen = useVModel(props, 'open', emit, {
  defaultValue: props.defaultOpen,
  passive: true,
})

const internalDuration = ref<number | undefined>(props.duration)

watch(() => props.duration, (newVal) => {
  internalDuration.value = newVal
})

function updateDuration(val: number) {
  internalDuration.value = val
  emit('update:duration', val)
}

const hasAutoClosed = ref(false)
const startTime = ref<number | null>(null)

const MS_IN_S = 1000
const AUTO_CLOSE_DELAY = 1000

watch(() => props.isStreaming, (streaming) => {
  if (streaming) {
    isOpen.value = true

    if (startTime.value === null) {
      startTime.value = Date.now()
    }
  }
  else if (startTime.value !== null) {
    const calculatedDuration = Math.ceil((Date.now() - startTime.value) / MS_IN_S)
    updateDuration(calculatedDuration)
    startTime.value = null
  }
})

watch([() => props.isStreaming, isOpen, () => props.defaultOpen, hasAutoClosed], (_, __, onCleanup) => {
  if (props.defaultOpen && !props.isStreaming && isOpen.value && !hasAutoClosed.value) {
    const timer = setTimeout(() => {
      isOpen.value = false
      hasAutoClosed.value = true
    }, AUTO_CLOSE_DELAY)

    onCleanup(() => clearTimeout(timer))
  }
}, { immediate: true })

provide(ReasoningKey, {
  isStreaming: computed(() => props.isStreaming),
  isOpen,
  setIsOpen: (val: boolean) => { isOpen.value = val },
  duration: computed(() => internalDuration.value),
})
</script>

<template>
  <Collapsible
    v-model:open="isOpen"
    :class="cn('not-prose mb-4', props.class)"
  >
    <slot />
  </Collapsible>
</template>

与 AI SDK 一起使用

使用 Deepseek R1 构建带推理功能的聊天机器人。

将以下组件添加到你的前端:

pages/index.vue
<script setup lang="ts">
import { useChat } from '@ai-sdk/vue'
import { ref } from 'vue'

import {
  Conversation,
  ConversationContent,
  ConversationScrollButton,
} from '@/components/ai-elements/conversation'
import { Loader } from '@/components/ai-elements/loader'
import { Message, MessageContent, MessageResponse } from '@/components/ai-elements/message'
import {
  PromptInput,
  PromptInputSubmit,
  PromptInputTextarea,
} from '@/components/ai-elements/prompt-input'
import {
  Reasoning,
  ReasoningContent,
  ReasoningTrigger,
} from '@/components/ai-elements/reasoning'

const input = ref('')

const { messages, sendMessage, status } = useChat()

function handleSubmit(e: Event) {
  e.preventDefault()

  if (!input.value.trim())
    return

  sendMessage({ text: input.value })
  input.value = ''
}

function isStreamingPart(msgIndex: number, partIndex: number) {
  const lastMsg = messages.value.at(-1)
  const msg = messages.value[msgIndex]

  if (!lastMsg || msg.id !== lastMsg.id)
    return false

  const isLastPart = partIndex === msg.parts.length - 1
  return status.value === 'streaming' && isLastPart
}
</script>

<template>
  <div class="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]">
    <div class="flex flex-col h-full">
      <Conversation>
        <ConversationContent>
          <template v-for="(message, msgIndex) in messages" :key="message.id">
            <Message :from="message.role">
              <MessageContent>
                <template v-for="(part, partIndex) in message.parts" :key="partIndex">
                  <MessageResponse v-if="part.type === 'text'">
                    {{ part.text }}
                  </MessageResponse>

                  <Reasoning
                    v-else-if="part.type === 'reasoning'"
                    class="w-full"
                    :is-streaming="isStreamingPart(msgIndex, partIndex)"
                  >
                    <ReasoningTrigger />
                    <ReasoningContent :text="part.text" />
                  </Reasoning>
                </template>
              </MessageContent>
            </Message>
          </template>

          <Loader v-if="status === 'submitted'" />
        </ConversationContent>

        <ConversationScrollButton />
      </Conversation>

      <PromptInput
        class="mt-4 w-full max-w-2xl mx-auto relative"
        @submit="handleSubmit"
      >
        <PromptInputTextarea
          v-model="input"
          placeholder="Say something..."
          class="pr-12"
        />

        <PromptInputSubmit
          :status="status === 'streaming' ? 'streaming' : 'ready'"
          :disabled="!input.trim()"
          class="absolute bottom-1 right-1"
        />
      </PromptInput>
    </div>
  </div>
</template>

将以下路由添加到你的后端:

server/api/chat.ts
import { convertToModelMessages, streamText, UIMessage } from 'ai'

export const maxDuration = 30

export default defineEventHandler(async (event) => {
  const { model, messages }: { model: string, messages: UIMessage[] }
    = await readBody(event)

  const result = streamText({
    model: model || 'deepseek/deepseek-r1',
    messages: convertToModelMessages(messages),
  })

  return result.toUIMessageStreamResponse({
    sendReasoning: true,
  })
})

特性

  • 在流式传输内容时自动打开,完成后关闭
  • 手动切换控制,供用户交互
  • 由 Reka UI 驱动的平滑动画和过渡
  • 带有脉冲动画的可视化流式传输指示器
  • 可组合架构,具有独立的触发器和内容组件
  • 考虑可访问性构建,包括键盘导航
  • 响应式设计,适用于不同的屏幕尺寸
  • 与浅色和深色主题无缝集成
  • 基于 shadcn-vue Collapsible 原语构建
  • 具有适当类型定义的 TypeScript 支持

Props

<Reasoning />

isStreamingboolean
false
推理是否正在流式传输(自动打开和关闭面板)。
classstring
''
应用于组件的额外 CSS 类。

<ReasoningTrigger />

classstring
''
应用于组件的额外 CSS 类。

<ReasoningContent />

contentstring
在推理面板中显示的内容。
classstring
''
应用于组件的额外 CSS 类。