diff --git a/apps/www/content/3.components/1.chatbot/reasoning.md b/apps/www/content/3.components/1.chatbot/reasoning.md new file mode 100644 index 0000000..52beb34 --- /dev/null +++ b/apps/www/content/3.components/1.chatbot/reasoning.md @@ -0,0 +1,443 @@ +--- +title: Reasoning +description: A collapsible component that displays AI reasoning content, automatically opening during streaming and closing when finished. +icon: lucide:brain-circuit +--- + +The `Reasoning` component is a collapsible component that displays AI reasoning content, automatically opening during streaming and closing when finished. + +:::ComponentLoader{label="Preview" componentName="Reasoning"} +::: + +## Install using CLI + +::tabs{variant="card"} + ::div{label="ai-elements-vue"} + ```sh + npx ai-elements-vue@latest add reasoning + ``` + :: + ::div{label="shadcn-vue"} + + ```sh + npx shadcn-vue@latest add https://registry.ai-elements-vue.com/reasoning.json + ``` + :: +:: + +## Install Manually + +Copy and paste the following code in the same folder. + +:::code-group +```vue [Reasoning.vue] height=300 collapse + + + +``` + +```vue [ReasoningTrigger.vue] height=300 collapse + + + +``` + +```vue [ReasoningContent.vue] height=300 collapse + + + +``` + +```ts [context.ts] height=300 collapse +import type { InjectionKey, Ref } from 'vue' +import { inject, provide } from 'vue' + +export interface ReasoningContextValue { + isStreaming: Ref + isOpen: Ref + setIsOpen: (open: boolean) => void + duration: Ref +} + +export const ReasoningKey: InjectionKey + = Symbol('ReasoningContext') + +export function useReasoningContext() { + const ctx = inject(ReasoningKey) + if (!ctx) + throw new Error('Reasoning components must be used within ') + return ctx +} +``` + +```ts [index.ts] height=300 collapse +export { default as Reasoning } from './Reasoning.vue' +export { default as ReasoningContent } from './ReasoningContent.vue' +export { default as ReasoningTrigger } from './ReasoningTrigger.vue' +``` +::: + +## Usage with AI SDK + +Build a chatbot with reasoning using Deepseek R1. + +Add the following component to your frontend: + +```vue [pages/index.vue] height=300 collapse + + + +``` + +Add the following route to your backend: + +```ts [server/api/chat/route.ts] height=300 collapse +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, + }) +}) +``` + +## Features + +- Automatically opens when streaming content and closes when finished +- Manual toggle control for user interaction +- Smooth animations and transitions powered by Radix UI +- Visual streaming indicator with pulsing animation +- Composable architecture with separate trigger and content components +- Built with accessibility in mind including keyboard navigation +- Responsive design that works across different screen sizes +- Seamlessly integrates with both light and dark themes +- Built on top of shadcn/ui Collapsible primitives +- TypeScript support with proper type definitions + +## Props + +### `` + +:::field-group + ::field{name="isStreaming" type="boolean" defaultValue="false"} + Whether the reasoning is currently streaming (auto-opens and closes the panel). + :: + + + + ::field{name="class" type="string" defaultValue="''"} + Additional CSS classes to apply to the component. + :: +::: + +### `` + +:::field-group + ::field{name="class" type="string" defaultValue="''"} + Additional CSS classes to apply to the component. + :: +::: + +### `` + +:::field-group + ::field{name="content" type="string"} + The content to display in the reasoning panel. + :: + + ::field{name="class" type="string" defaultValue="''"} + Additional CSS classes to apply to the component. + :: +::: diff --git a/apps/www/plugins/ai-elements.ts b/apps/www/plugins/ai-elements.ts index f28cbb7..fb175b2 100644 --- a/apps/www/plugins/ai-elements.ts +++ b/apps/www/plugins/ai-elements.ts @@ -7,11 +7,11 @@ import { Checkpoint, CodeBlock, CodeBlockDark, - Context, Confirmation, ConfirmationAccepted, ConfirmationRejected, ConfirmationRequest, + Context, Conversation, Image, InlineCitation, @@ -27,6 +27,7 @@ import { Queue, QueueCustom, QueuePromptInput, + Reasoning, Response, Shimmer, ShimmerCustomElements, @@ -96,4 +97,5 @@ export default defineNuxtPlugin((nuxtApp) => { vueApp.component('ConfirmationAccepted', ConfirmationAccepted) vueApp.component('ConfirmationRejected', ConfirmationRejected) vueApp.component('ConfirmationRequest', ConfirmationRequest) + vueApp.component('Reasoning', Reasoning) }) diff --git a/packages/elements/src/index.ts b/packages/elements/src/index.ts index 5384f73..20e53bd 100644 --- a/packages/elements/src/index.ts +++ b/packages/elements/src/index.ts @@ -4,8 +4,8 @@ export * from './branch' export * from './chain-of-thought' export * from './checkpoint' export * from './code-block' -export * from './context' export * from './confirmation' +export * from './context' export * from './conversation' export * from './image' export * from './inline-citation' @@ -16,6 +16,7 @@ export * from './open-in-chat' export * from './plan' export * from './prompt-input' export * from './queue' +export * from './reasoning' export * from './response' export * from './shimmer' export * from './sources' diff --git a/packages/elements/src/reasoning/Reasoning.vue b/packages/elements/src/reasoning/Reasoning.vue new file mode 100644 index 0000000..bf3a20e --- /dev/null +++ b/packages/elements/src/reasoning/Reasoning.vue @@ -0,0 +1,94 @@ + + + diff --git a/packages/elements/src/reasoning/ReasoningContent.vue b/packages/elements/src/reasoning/ReasoningContent.vue new file mode 100644 index 0000000..7190ef4 --- /dev/null +++ b/packages/elements/src/reasoning/ReasoningContent.vue @@ -0,0 +1,41 @@ + + + diff --git a/packages/elements/src/reasoning/ReasoningTrigger.vue b/packages/elements/src/reasoning/ReasoningTrigger.vue new file mode 100644 index 0000000..0b14517 --- /dev/null +++ b/packages/elements/src/reasoning/ReasoningTrigger.vue @@ -0,0 +1,61 @@ + + + diff --git a/packages/elements/src/reasoning/context.ts b/packages/elements/src/reasoning/context.ts new file mode 100644 index 0000000..619d86e --- /dev/null +++ b/packages/elements/src/reasoning/context.ts @@ -0,0 +1,19 @@ +import type { InjectionKey, Ref } from 'vue' +import { inject } from 'vue' + +export interface ReasoningContextValue { + isStreaming: Ref + isOpen: Ref + setIsOpen: (open: boolean) => void + duration: Ref +} + +export const ReasoningKey: InjectionKey + = Symbol('ReasoningContext') + +export function useReasoningContext() { + const ctx = inject(ReasoningKey) + if (!ctx) + throw new Error('Reasoning components must be used within ') + return ctx +} diff --git a/packages/elements/src/reasoning/index.ts b/packages/elements/src/reasoning/index.ts new file mode 100644 index 0000000..f71d435 --- /dev/null +++ b/packages/elements/src/reasoning/index.ts @@ -0,0 +1,3 @@ +export { default as Reasoning } from './Reasoning.vue' +export { default as ReasoningContent } from './ReasoningContent.vue' +export { default as ReasoningTrigger } from './ReasoningTrigger.vue' diff --git a/packages/examples/src/index.ts b/packages/examples/src/index.ts index 113a3c5..aa2a4b8 100644 --- a/packages/examples/src/index.ts +++ b/packages/examples/src/index.ts @@ -6,11 +6,11 @@ export { default as ChainOfThought } from './chain-of-thought.vue' export { default as Checkpoint } from './checkpoint.vue' export { default as CodeBlockDark } from './code-block-dark.vue' export { default as CodeBlock } from './code-block.vue' -export { default as Context } from './context.vue' export { default as ConfirmationAccepted } from './confirmation-accepted.vue' export { default as ConfirmationRejected } from './confirmation-rejected.vue' export { default as ConfirmationRequest } from './confirmation-request.vue' export { default as Confirmation } from './confirmation.vue' +export { default as Context } from './context.vue' export { default as Conversation } from './conversation.vue' export { default as Image } from './image.vue' export { default as InlineCitation } from './inline-citation.vue' @@ -26,6 +26,7 @@ export { default as PromptInput } from './prompt-input.vue' export { default as QueueCustom } from './queue-custom.vue' export { default as QueuePromptInput } from './queue-prompt-input.vue' export { default as Queue } from './queue.vue' +export { default as Reasoning } from './reasoning.vue' export { default as Response } from './response.vue' export { default as ShimmerCustomElements } from './shimmer-custom-elements.vue' export { default as ShimmerDurations } from './shimmer-durations.vue' diff --git a/packages/examples/src/reasoning.vue b/packages/examples/src/reasoning.vue new file mode 100644 index 0000000..e654cce --- /dev/null +++ b/packages/examples/src/reasoning.vue @@ -0,0 +1,62 @@ + + +