Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/content/docs/2.components/chat-reasoning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
description: A collapsible component to display AI reasoning or thinking process.
category: data
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/v4/src/runtime/components/ChatReasoning.vue
---

::callout{icon="i-lucide-construction" to="/getting-started/roadmap"}
This page is a work in progress.
::
12 changes: 12 additions & 0 deletions docs/content/docs/2.components/chat-shimmer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
description: A text component with an animated shimmer effect.
category: element
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/blob/v4/src/runtime/components/ChatShimmer.vue
---

::callout{icon="i-lucide-construction" to="/getting-started/roadmap"}
This page is a work in progress.
::
Binary file added docs/public/components/dark/chat-reasoning.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/public/components/dark/chat-shimmer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/public/components/light/chat-reasoning.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/public/components/light/chat-shimmer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion playgrounds/nuxt/app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ provide('components', components)
<UDashboardPanel
:ui="{
body: [
'justify-center items-center',
route.path !== '/chat' && 'justify-center items-center',
route.path.startsWith('/components') && 'mt-16',
route.path === '/chat' && 'flex-col p-0! sm:p-0!',
route.path.startsWith('/components/scroll-area') && 'p-0!'
]
}"
Expand Down
1 change: 1 addition & 0 deletions playgrounds/nuxt/app/composables/useNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const components = [
'calendar',
'card',
'carousel',
'chat-shimmer',
'changelog-version',
'changelog-versions',
'checkbox-group',
Expand Down
257 changes: 218 additions & 39 deletions playgrounds/nuxt/app/pages/chat.vue
Original file line number Diff line number Diff line change
@@ -1,71 +1,250 @@
<script setup lang="ts">
import type { UIMessage } from 'ai'
import { Chat } from '@ai-sdk/vue'
import { DefaultChatTransport } from 'ai'

const toast = useToast()
const appConfig = useAppConfig()

const messages: UIMessage[] = [{
id: '1',
role: 'user',
parts: [{ type: 'text', text: 'Hello, how are you?' }]
}, {
id: '2',
role: 'assistant',
parts: [{ type: 'text', text: 'I\'m good, thank you! How can I help you today?' }]
}]
const input = ref('')
const mode = ref<'live' | 'mock'>('mock')

const chat = new Chat({
messages,
onError(error) {
const { message: description } = typeof error.message === 'string' && error.message[0] === '{' ? JSON.parse(error.message) : error

toast.add({
description,
icon: 'i-lucide-alert-circle',
color: 'error',
duration: 0
})
const initialMockMessages: UIMessage[] = [
{
id: 'msg-1',
role: 'user',
parts: [{ type: 'text', text: 'Can you explain how neural networks work?' }]
},
{
id: 'msg-2',
role: 'assistant',
parts: [
{
type: 'reasoning',
text: 'The user is asking about neural networks. Let me break this down:\n\n1. **Start with basics** - neurons, layers, weights\n2. **Use analogies** - compare to biological neurons\n3. **Provide examples** - show practical applications\n\nI should keep it accessible while being technically accurate.',
state: 'done'
},
{
type: 'text',
text: 'Neural networks are computing systems inspired by biological neural networks in the brain.\n\n**Key concepts:**\n\n1. **Neurons** - Basic units that receive inputs and produce outputs\n2. **Layers** - Groups of neurons (input, hidden, output)\n3. **Weights** - Connection strengths between neurons\n4. **Activation functions** - Determine if a neuron should fire'
}
]
}
})
]

function onSubmit() {
chat.sendMessage({ text: input.value })
const mockChat = shallowRef(createMockChat())
const liveChat = shallowRef(createLiveChat())

function createMockChat(messages: UIMessage[] = initialMockMessages) {
return new Chat({
id: `mock-${Date.now()}`,
messages,
transport: new DefaultChatTransport({
api: '/api/chat-mock'
}),
onError(error) {
console.error('Mock chat error:', error)
}
})
}

function createLiveChat() {
return new Chat({
id: `live-${Date.now()}`,
onFinish() {
console.log('Live chat finished')
console.log(liveChat.value.messages)
},
onError(error) {
const { message: description } = typeof error.message === 'string' && error.message[0] === '{' ? JSON.parse(error.message) : error
toast.add({
description,
icon: appConfig.ui.icons.error,
color: 'error',
duration: 0
})
}
})
}

const input = ref('')

const chat = computed(() => mode.value === 'live' ? liveChat.value : mockChat.value)
const messages = computed(() => chat.value.messages)
const status = computed(() => chat.value.status)

function onSubmit() {
if (!input.value.trim()) return
chat.value.sendMessage({ text: input.value })
input.value = ''
}

function handleStop() {
chat.value.stop()
}

function handleReload() {
chat.value.regenerate()
}

function clearMessages() {
if (mode.value === 'mock') {
mockChat.value = createMockChat([])
} else {
liveChat.value = createLiveChat()
}
}

function addUserMessage() {
mockChat.value = createMockChat([
...mockChat.value.messages,
{
id: `msg-${Date.now()}`,
role: 'user',
parts: [{ type: 'text', text: 'This is a test user message.' }]
}
])
}

function addAssistantMessage() {
mockChat.value = createMockChat([
...mockChat.value.messages,
{
id: `msg-${Date.now()}`,
role: 'assistant',
parts: [
{
type: 'reasoning',
text: 'Processing the request...\n\n- Analyzing the context\n- Formulating a response\n- Checking for accuracy',
state: 'done'
},
{
type: 'text',
text: 'This is a test assistant response with **markdown** support.'
}
]
}
])
}

const mockActions = [
{ label: 'Simulate Stream', icon: 'i-lucide-play', onClick: () => chat.value.sendMessage({ text: 'Test streaming' }) },
{ label: 'Add User Message', icon: 'i-lucide-user', onClick: addUserMessage },
{ label: 'Add Assistant Message', icon: 'i-lucide-bot', onClick: addAssistantMessage },
{ type: 'separator' as const },
{ label: 'Clear All', icon: 'i-lucide-trash-2', onClick: clearMessages }
]

const messageActions = [
{ icon: 'i-lucide-copy', label: 'Copy', onClick: () => toast.add({ title: 'Copied!' }) },
{ icon: 'i-lucide-thumbs-up', label: 'Like' },
{ icon: 'i-lucide-thumbs-down', label: 'Dislike' },
{ icon: 'i-lucide-rotate-ccw', label: 'Regenerate' }
]
</script>

<template>
<UDashboardNavbar class="absolute top-0 inset-x-0 z-5 border-b-0 lg:pointer-events-none" />
<UDashboardNavbar class="absolute top-0 inset-x-0 z-10 bg-default">
<template #toggle>
<UDashboardSidebarToggle size="sm" variant="outline" class="ring-default" />
<UDashboardSidebarCollapse size="sm" variant="outline" class="ring-default" />
</template>

<template #title>
Chat
</template>

<template #right>
<UFieldGroup size="sm">
<UButton
label="Mock"
:color="mode === 'mock' ? 'primary' : 'neutral'"
:variant="mode === 'mock' ? 'solid' : 'outline'"
@click="mode = 'mock'"
/>
<UButton
label="Live"
:color="mode === 'live' ? 'primary' : 'neutral'"
:variant="mode === 'live' ? 'solid' : 'outline'"
@click="mode = 'live'"
/>
</UFieldGroup>

<USeparator orientation="vertical" class="h-5" />

<template v-if="mode === 'mock'">
<UDropdownMenu :items="mockActions">
<UButton
icon="i-lucide-flask-conical"
color="neutral"
variant="ghost"
size="sm"
/>
</UDropdownMenu>
</template>

<div class="flex-1 flex flex-col gap-4 sm:gap-6 max-w-xl w-full mx-auto min-h-0">
<template v-else>
<UTooltip text="Clear messages">
<UButton
icon="i-lucide-trash-2"
color="neutral"
variant="ghost"
size="sm"
@click="clearMessages"
/>
</UTooltip>
</template>

<USeparator orientation="vertical" class="h-5" />

<UBadge
:color="status === 'ready' ? 'success' : status === 'error' ? 'error' : 'warning'"
variant="subtle"
size="sm"
>
{{ status }}
</UBadge>
</template>
</UDashboardNavbar>

<div class="flex-1 flex flex-col min-h-0 w-full pt-20">
<UChatMessages
:messages="chat.messages"
:status="chat.status"
should-auto-scroll
:messages="messages"
:status="status"
:user="{ avatar: { src: 'https://github.com/benjamincanac.png' } }"
:spacing-offset="48"
:assistant="{ avatar: { src: 'https://github.com/nuxt.png' }, actions: status !== 'streaming' ? messageActions : [] }"
:spacing-offset="160"
class="max-w-4xl w-full mx-auto px-4 sm:px-6"
>
<template #content="{ message }">
<template
v-for="(part, index) in message.parts"
:key="`${message.id}-${part.type}-${index}${'state' in part ? `-${part.state}` : ''}`"
:key="`${message.id}-${part.type}-${index}`"
>
<UChatReasoning
v-if="part.type === 'reasoning'"
:text="part.text"
:is-streaming="status === 'streaming' && index === message.parts.length - 1 && message.id === messages[messages.length - 1]?.id"
:unmount-on-hide="false"
class="mb-4"
>
<template #body>
<MDC
:value="part.text"
class="*:first:mt-0 *:last:mb-0"
/>
</template>
</UChatReasoning>

<MDC
v-if="part.type === 'text' && message.role === 'assistant'"
:value="part.text"
:cache-key="`${message.id}-${index}`"
:cache-key="`${message.id}-${index}-${part.text.length}`"
class="*:first:mt-0 *:last:mb-0"
/>
<p v-else-if="part.type === 'text' && message.role === 'user'" class="whitespace-pre-wrap">
{{ part.text }}
</p>
<p
v-else-if="part.type === 'reasoning'"
class="text-sm text-muted my-5"
>
{{ part.state === 'done' ? 'Thoughts' : 'Thinking...' }}
</p>
</template>
</template>
</UChatMessages>
Expand All @@ -74,10 +253,10 @@ function onSubmit() {
v-model="input"
:error="chat.error"
variant="subtle"
class="sticky bottom-0"
class="sticky bottom-0 z-10 max-w-4xl w-full mx-auto"
@submit="onSubmit"
>
<UChatPromptSubmit :status="chat.status" @stop="chat.stop()" @reload="chat.regenerate()" />
<UChatPromptSubmit :status="status" @stop="handleStop" @reload="handleReload" />
</UChatPrompt>
</div>
</template>
45 changes: 45 additions & 0 deletions playgrounds/nuxt/app/pages/components/chat-shimmer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script setup lang="ts">
const text = ref('Loading your content...')
const duration = ref(2)
const spread = ref(2)
const asElement = ref('span')
</script>

<template>
<Navbar>
<UInput v-model="text" placeholder="Text" class="w-48" />
<UTooltip text="Duration (seconds) - How long the shimmer animation lasts for one cycle">
<UInputNumber
v-model="duration"
:min="0.5"
:max="10"
:step="0.5"
placeholder="Duration"
class="w-24"
/>
</UTooltip>
<UTooltip text="Spread (pixels/char) - How wide the shimmer highlight appears per character">
<UInputNumber
v-model="spread"
:min="1"
:max="10"
:step="1"
placeholder="Spread"
class="w-24"
/>
</UTooltip>
</Navbar>

<div class="flex flex-col items-center gap-6">
<UChatShimmer
:key="`${duration}-${spread}`"
:text="text"
:duration="duration"
:spread="spread"
:as="asElement"
class="text-xl"
/>
<UChatShimmer text="AI is thinking..." :duration="1.5" class="text-lg" />
<UChatShimmer text="Processing..." :duration="3" class="text-base" />
</div>
</template>
Loading
Loading