聊天信息
用法
ChatMessages组件使用默认插槽或messages属性显示ChatMessage组件的列表。
<template>
<UChatMessages>
<UChatMessage
v-for="(message, index) in messages"
:key="index"
v-bind="message"
/>
</UChatMessages>
</template>
- 加载时初始滚动到底部(
shouldScrollToBottom)。 - 在新邮件到达时持续向下滚动(
shouldAutoScroll)。 - 向上滚动时会出现“自动滚动”按钮,允许用户跳回到最新消息(
autoScroll)。 - 助手正在处理时显示加载指示器(
status)。 - 提交的消息滚动到视口的顶部,最后一条用户消息的高度动态调整。
留言
使用messages属性显示聊天消息列表。
<script setup lang="ts">
const messages = ref([
{
id: '6045235a-a435-46b8-989d-2df38ca2eb47',
role: 'user',
parts: [
{
type: 'text',
text: 'Hello, how are you?'
}
]
},
{
id: '7a92b3c1-d5f8-4e76-b8a9-3c1e5fb2e0d8',
role: 'assistant',
parts: [
{
type: 'text',
text: 'I am doing well, thank you for asking! How can I assist you today?'
}
]
},
{
id: '9c84d6a7-8b23-4f12-a1d5-e7f3b9c05e2a',
role: 'user',
parts: [
{
type: 'text',
text: 'What is the current weather in Tokyo?'
}
]
},
{
id: 'b2e5f8c3-a1d9-4e67-b3f2-c9d8e7a6b5f4',
role: 'assistant',
parts: [
{
type: 'text',
text: "Based on the latest data, Tokyo is currently experiencing sunny weather with temperatures around 24°C (75°F). It's a beautiful day with clear skies."
}
]
}
])
</script>
<template>
<UChatMessages :messages="messages" />
</template><script setup lang="ts">
import { ref } from 'vue'
const messages = ref([
{
id: '6045235a-a435-46b8-989d-2df38ca2eb47',
role: 'user',
parts: [
{
type: 'text',
text: 'Hello, how are you?'
}
]
},
{
id: '7a92b3c1-d5f8-4e76-b8a9-3c1e5fb2e0d8',
role: 'assistant',
parts: [
{
type: 'text',
text: 'I am doing well, thank you for asking! How can I assist you today?'
}
]
},
{
id: '9c84d6a7-8b23-4f12-a1d5-e7f3b9c05e2a',
role: 'user',
parts: [
{
type: 'text',
text: 'What is the current weather in Tokyo?'
}
]
},
{
id: 'b2e5f8c3-a1d9-4e67-b3f2-c9d8e7a6b5f4',
role: 'assistant',
parts: [
{
type: 'text',
text: "Based on the latest data, Tokyo is currently experiencing sunny weather with temperatures around 24°C (75°F). It's a beautiful day with clear skies."
}
]
}
])
</script>
<template>
<UChatMessages :messages="messages" />
</template>状态
使用status道具在助手处理时显示可视指示器。
<script setup lang="ts">
const messages = ref([
{
id: '6045235a-a435-46b8-989d-2df38ca2eb47',
role: 'user',
parts: [
{
type: 'text',
text: 'Hello, how are you?'
}
]
}
])
</script>
<template>
<UChatMessages status="submitted" :messages="messages" />
</template><script setup lang="ts">
import { ref } from 'vue'
const messages = ref([
{
id: '6045235a-a435-46b8-989d-2df38ca2eb47',
role: 'user',
parts: [
{
type: 'text',
text: 'Hello, how are you?'
}
]
}
])
</script>
<template>
<UChatMessages status="submitted" :messages="messages" />
</template>useChat组合的不同状态的详细信息:submitted:消息已发送到API,我们正在等待响应流的开始。streaming:响应正在从API主动流入,接收数据块。ready:已收到并处理完整响应;可以提交新用户消息。error:请求API时出错,无法成功完成。
用户
使用user prop更改user消息的ChatMessage prop。请更改为:
side: 'right'variant: 'soft'
<script setup lang="ts">
const messages = ref([
{
id: '6045235a-a435-46b8-989d-2df38ca2eb47',
role: 'user',
parts: [
{
type: 'text',
text: 'Hello, how are you?'
}
]
},
{
id: '7a92b3c1-d5f8-4e76-b8a9-3c1e5fb2e0d8',
role: 'assistant',
parts: [
{
type: 'text',
text: 'I am doing well, thank you for asking! How can I assist you today?'
}
]
},
{
id: '9c84d6a7-8b23-4f12-a1d5-e7f3b9c05e2a',
role: 'user',
parts: [
{
type: 'text',
text: 'What is the current weather in Tokyo?'
}
]
},
{
id: 'b2e5f8c3-a1d9-4e67-b3f2-c9d8e7a6b5f4',
role: 'assistant',
parts: [
{
type: 'text',
text: "Based on the latest data, Tokyo is currently experiencing sunny weather with temperatures around 24°C (75°F). It's a beautiful day with clear skies."
}
]
}
])
</script>
<template>
<UChatMessages
:user="{
side: 'left',
variant: 'solid',
avatar: {
src: 'https://github.com/benjamincanac.png',
loading: 'lazy'
}
}"
:messages="messages"
/>
</template><script setup lang="ts">
import { ref } from 'vue'
const messages = ref([
{
id: '6045235a-a435-46b8-989d-2df38ca2eb47',
role: 'user',
parts: [
{
type: 'text',
text: 'Hello, how are you?'
}
]
},
{
id: '7a92b3c1-d5f8-4e76-b8a9-3c1e5fb2e0d8',
role: 'assistant',
parts: [
{
type: 'text',
text: 'I am doing well, thank you for asking! How can I assist you today?'
}
]
},
{
id: '9c84d6a7-8b23-4f12-a1d5-e7f3b9c05e2a',
role: 'user',
parts: [
{
type: 'text',
text: 'What is the current weather in Tokyo?'
}
]
},
{
id: 'b2e5f8c3-a1d9-4e67-b3f2-c9d8e7a6b5f4',
role: 'assistant',
parts: [
{
type: 'text',
text: "Based on the latest data, Tokyo is currently experiencing sunny weather with temperatures around 24°C (75°F). It's a beautiful day with clear skies."
}
]
}
])
</script>
<template>
<UChatMessages
:user="{
side: 'left',
variant: 'solid',
avatar: {
src: 'https://github.com/benjamincanac.png',
loading: 'lazy'
}
}"
:messages="messages"
/>
</template>助手
使用assistant prop更改assistant消息的ChatMessage prop。请更改为:
side: 'left'variant: 'naked'
<script setup lang="ts">
const messages = ref([
{
id: '6045235a-a435-46b8-989d-2df38ca2eb47',
role: 'user',
parts: [
{
type: 'text',
text: 'Hello, how are you?'
}
]
},
{
id: '7a92b3c1-d5f8-4e76-b8a9-3c1e5fb2e0d8',
role: 'assistant',
parts: [
{
type: 'text',
text: 'I am doing well, thank you for asking! How can I assist you today?'
}
]
},
{
id: '9c84d6a7-8b23-4f12-a1d5-e7f3b9c05e2a',
role: 'user',
parts: [
{
type: 'text',
text: 'What is the current weather in Tokyo?'
}
]
},
{
id: 'b2e5f8c3-a1d9-4e67-b3f2-c9d8e7a6b5f4',
role: 'assistant',
parts: [
{
type: 'text',
text: "Based on the latest data, Tokyo is currently experiencing sunny weather with temperatures around 24°C (75°F). It's a beautiful day with clear skies."
}
]
}
])
</script>
<template>
<UChatMessages
:assistant="{
side: 'left',
variant: 'outline',
avatar: {
icon: 'i-lucide-bot'
},
actions: [
{
label: 'Copy to clipboard',
icon: 'i-lucide-copy'
}
]
}"
:messages="messages"
/>
</template><script setup lang="ts">
import { ref } from 'vue'
const messages = ref([
{
id: '6045235a-a435-46b8-989d-2df38ca2eb47',
role: 'user',
parts: [
{
type: 'text',
text: 'Hello, how are you?'
}
]
},
{
id: '7a92b3c1-d5f8-4e76-b8a9-3c1e5fb2e0d8',
role: 'assistant',
parts: [
{
type: 'text',
text: 'I am doing well, thank you for asking! How can I assist you today?'
}
]
},
{
id: '9c84d6a7-8b23-4f12-a1d5-e7f3b9c05e2a',
role: 'user',
parts: [
{
type: 'text',
text: 'What is the current weather in Tokyo?'
}
]
},
{
id: 'b2e5f8c3-a1d9-4e67-b3f2-c9d8e7a6b5f4',
role: 'assistant',
parts: [
{
type: 'text',
text: "Based on the latest data, Tokyo is currently experiencing sunny weather with temperatures around 24°C (75°F). It's a beautiful day with clear skies."
}
]
}
])
</script>
<template>
<UChatMessages
:assistant="{
side: 'left',
variant: 'outline',
avatar: {
icon: 'i-lucide-bot'
},
actions: [
{
label: 'Copy to clipboard',
icon: 'i-lucide-copy'
}
]
}"
:messages="messages"
/>
</template>自动滚动
使用auto-scroll道具自定义或隐藏自动滚动按钮(使用false值),当滚动到聊天顶部时显示。
color: 'neutral'variant: 'outline'
您可以从Button组件传递任何属性来对其进行自定义。
<script setup lang="ts">
const messages = ref([
{
id: '6045235a-a435-46b8-989d-2df38ca2eb47',
role: 'user',
parts: [
{
type: 'text',
text: 'Hello, how are you?'
}
]
},
{
id: '7a92b3c1-d5f8-4e76-b8a9-3c1e5fb2e0d8',
role: 'assistant',
parts: [
{
type: 'text',
text: 'I am doing well, thank you for asking! How can I assist you today?'
}
]
},
{
id: '9c84d6a7-8b23-4f12-a1d5-e7f3b9c05e2a',
role: 'user',
parts: [
{
type: 'text',
text: 'What is the current weather in Tokyo?'
}
]
},
{
id: 'b2e5f8c3-a1d9-4e67-b3f2-c9d8e7a6b5f4',
role: 'assistant',
parts: [
{
type: 'text',
text: "Based on the latest data, Tokyo is currently experiencing sunny weather with temperatures around 24°C (75°F). It's a beautiful day with clear skies. The forecast for the rest of the week shows a slight chance of rain on Thursday, with temperatures gradually rising to 28°C by the weekend. Humidity levels are moderate at around 65%, and wind speeds are light at 8 km/h from the southeast. Air quality is good with an index of 42. The UV index is high at 7, so it's recommended to wear sunscreen if you're planning to spend time outdoors. Sunrise was at 5:24 AM and sunset will be at 6:48 PM, giving Tokyo approximately 13 hours and 24 minutes of daylight today. The moon is currently in its waxing gibbous phase."
}
]
},
{
id: 'c3e5f8c3-a1d9-4e67-b3f2-c9d8e7a6b5f4',
role: 'user',
parts: [
{
type: 'text',
text: 'Can you recommend some popular tourist attractions in Kyoto?'
}
]
},
{
id: 'd4f5g8c3-a1d9-4e67-b3f2-c9d8e7a6b5f4',
role: 'assistant',
parts: [
{
type: 'text',
text: 'Kyoto is known for its beautiful temples, traditional tea houses, and gardens. Some popular attractions include Kinkaku-ji (Golden Pavilion) with its stunning gold leaf exterior reflecting in the mirror pond, Fushimi Inari Shrine with its thousands of vermilion torii gates winding up the mountainside, Arashiyama Bamboo Grove where towering stalks create an otherworldly atmosphere, Kiyomizu-dera Temple perched on a hillside offering panoramic views of the city, and the historic Gion district where you might spot geisha hurrying to evening appointments through narrow stone-paved streets lined with traditional wooden machiya houses.'
}
]
}
])
</script>
<template>
<UChatMessages
:auto-scroll="{
color: 'neutral',
variant: 'outline'
}"
:should-scroll-to-bottom="false"
:messages="messages"
/>
</template><script setup lang="ts">
import { ref, h } from 'vue'
const messages = ref([
{
id: '6045235a-a435-46b8-989d-2df38ca2eb47',
role: 'user',
parts: [
{
type: 'text',
text: 'Hello, how are you?'
}
]
},
{
id: '7a92b3c1-d5f8-4e76-b8a9-3c1e5fb2e0d8',
role: 'assistant',
parts: [
{
type: 'text',
text: 'I am doing well, thank you for asking! How can I assist you today?'
}
]
},
{
id: '9c84d6a7-8b23-4f12-a1d5-e7f3b9c05e2a',
role: 'user',
parts: [
{
type: 'text',
text: 'What is the current weather in Tokyo?'
}
]
},
{
id: 'b2e5f8c3-a1d9-4e67-b3f2-c9d8e7a6b5f4',
role: 'assistant',
parts: [
{
type: 'text',
text: "Based on the latest data, Tokyo is currently experiencing sunny weather with temperatures around 24°C (75°F). It's a beautiful day with clear skies. The forecast for the rest of the week shows a slight chance of rain on Thursday, with temperatures gradually rising to 28°C by the weekend. Humidity levels are moderate at around 65%, and wind speeds are light at 8 km/h from the southeast. Air quality is good with an index of 42. The UV index is high at 7, so it's recommended to wear sunscreen if you're planning to spend time outdoors. Sunrise was at 5:24 AM and sunset will be at 6:48 PM, giving Tokyo approximately 13 hours and 24 minutes of daylight today. The moon is currently in its waxing gibbous phase."
}
]
},
{
id: 'c3e5f8c3-a1d9-4e67-b3f2-c9d8e7a6b5f4',
role: 'user',
parts: [
{
type: 'text',
text: 'Can you recommend some popular tourist attractions in Kyoto?'
}
]
},
{
id: 'd4f5g8c3-a1d9-4e67-b3f2-c9d8e7a6b5f4',
role: 'assistant',
parts: [
{
type: 'text',
text: 'Kyoto is known for its beautiful temples, traditional tea houses, and gardens. Some popular attractions include Kinkaku-ji (Golden Pavilion) with its stunning gold leaf exterior reflecting in the mirror pond, Fushimi Inari Shrine with its thousands of vermilion torii gates winding up the mountainside, Arashiyama Bamboo Grove where towering stalks create an otherworldly atmosphere, Kiyomizu-dera Temple perched on a hillside offering panoramic views of the city, and the historic Gion district where you might spot geisha hurrying to evening appointments through narrow stone-paved streets lined with traditional wooden machiya houses.'
}
]
}
])
</script>
<template>
<UChatMessages
:auto-scroll="{
color: 'neutral',
variant: 'outline'
}"
:should-scroll-to-bottom="false"
:messages="messages"
/>
</template>自动滚动图标
使用auto-scroll-icon道具自定义自动滚动按钮Icon。
<script setup lang="ts">
const messages = ref([
{
id: '6045235a-a435-46b8-989d-2df38ca2eb47',
role: 'user',
parts: [
{
type: 'text',
text: 'Hello, how are you?'
}
]
},
{
id: '7a92b3c1-d5f8-4e76-b8a9-3c1e5fb2e0d8',
role: 'assistant',
parts: [
{
type: 'text',
text: 'I am doing well, thank you for asking! How can I assist you today?'
}
]
},
{
id: '9c84d6a7-8b23-4f12-a1d5-e7f3b9c05e2a',
role: 'user',
parts: [
{
type: 'text',
text: 'What is the current weather in Tokyo?'
}
]
},
{
id: 'b2e5f8c3-a1d9-4e67-b3f2-c9d8e7a6b5f4',
role: 'assistant',
parts: [
{
type: 'text',
text: "Based on the latest data, Tokyo is currently experiencing sunny weather with temperatures around 24°C (75°F). It's a beautiful day with clear skies. The forecast for the rest of the week shows a slight chance of rain on Thursday, with temperatures gradually rising to 28°C by the weekend. Humidity levels are moderate at around 65%, and wind speeds are light at 8 km/h from the southeast. Air quality is good with an index of 42. The UV index is high at 7, so it's recommended to wear sunscreen if you're planning to spend time outdoors. Sunrise was at 5:24 AM and sunset will be at 6:48 PM, giving Tokyo approximately 13 hours and 24 minutes of daylight today. The moon is currently in its waxing gibbous phase."
}
]
},
{
id: 'c3e5f8c3-a1d9-4e67-b3f2-c9d8e7a6b5f4',
role: 'user',
parts: [
{
type: 'text',
text: 'Can you recommend some popular tourist attractions in Kyoto?'
}
]
},
{
id: 'd4f5g8c3-a1d9-4e67-b3f2-c9d8e7a6b5f4',
role: 'assistant',
parts: [
{
type: 'text',
text: 'Kyoto is known for its beautiful temples, traditional tea houses, and gardens. Some popular attractions include Kinkaku-ji (Golden Pavilion) with its stunning gold leaf exterior reflecting in the mirror pond, Fushimi Inari Shrine with its thousands of vermilion torii gates winding up the mountainside, Arashiyama Bamboo Grove where towering stalks create an otherworldly atmosphere, Kiyomizu-dera Temple perched on a hillside offering panoramic views of the city, and the historic Gion district where you might spot geisha hurrying to evening appointments through narrow stone-paved streets lined with traditional wooden machiya houses.'
}
]
}
])
</script>
<template>
<UChatMessages
auto-scroll-icon="i-lucide-chevron-down"
:should-scroll-to-bottom="false"
:messages="messages"
/>
</template><script setup lang="ts">
import { ref, h } from 'vue'
const messages = ref([
{
id: '6045235a-a435-46b8-989d-2df38ca2eb47',
role: 'user',
parts: [
{
type: 'text',
text: 'Hello, how are you?'
}
]
},
{
id: '7a92b3c1-d5f8-4e76-b8a9-3c1e5fb2e0d8',
role: 'assistant',
parts: [
{
type: 'text',
text: 'I am doing well, thank you for asking! How can I assist you today?'
}
]
},
{
id: '9c84d6a7-8b23-4f12-a1d5-e7f3b9c05e2a',
role: 'user',
parts: [
{
type: 'text',
text: 'What is the current weather in Tokyo?'
}
]
},
{
id: 'b2e5f8c3-a1d9-4e67-b3f2-c9d8e7a6b5f4',
role: 'assistant',
parts: [
{
type: 'text',
text: "Based on the latest data, Tokyo is currently experiencing sunny weather with temperatures around 24°C (75°F). It's a beautiful day with clear skies. The forecast for the rest of the week shows a slight chance of rain on Thursday, with temperatures gradually rising to 28°C by the weekend. Humidity levels are moderate at around 65%, and wind speeds are light at 8 km/h from the southeast. Air quality is good with an index of 42. The UV index is high at 7, so it's recommended to wear sunscreen if you're planning to spend time outdoors. Sunrise was at 5:24 AM and sunset will be at 6:48 PM, giving Tokyo approximately 13 hours and 24 minutes of daylight today. The moon is currently in its waxing gibbous phase."
}
]
},
{
id: 'c3e5f8c3-a1d9-4e67-b3f2-c9d8e7a6b5f4',
role: 'user',
parts: [
{
type: 'text',
text: 'Can you recommend some popular tourist attractions in Kyoto?'
}
]
},
{
id: 'd4f5g8c3-a1d9-4e67-b3f2-c9d8e7a6b5f4',
role: 'assistant',
parts: [
{
type: 'text',
text: 'Kyoto is known for its beautiful temples, traditional tea houses, and gardens. Some popular attractions include Kinkaku-ji (Golden Pavilion) with its stunning gold leaf exterior reflecting in the mirror pond, Fushimi Inari Shrine with its thousands of vermilion torii gates winding up the mountainside, Arashiyama Bamboo Grove where towering stalks create an otherworldly atmosphere, Kiyomizu-dera Temple perched on a hillside offering panoramic views of the city, and the historic Gion district where you might spot geisha hurrying to evening appointments through narrow stone-paved streets lined with traditional wooden machiya houses.'
}
]
}
])
</script>
<template>
<UChatMessages
auto-scroll-icon="i-lucide-chevron-down"
:should-scroll-to-bottom="false"
:messages="messages"
/>
</template>应自动滚动
使用should-auto-scroll prop来启用/禁用连续自动滚动,而消息是流。安装到false。
<template>
<UChatMessages :messages="messages" should-auto-scroll />
</template>
应滚动到底部
使用should-scroll-to-bottom prop来启用/禁用底部自动滚动时,组件是挂载。安装到true。
<template>
<UChatMessages :messages="messages" :should-scroll-to-bottom="false" />
</template>
示例
带指示灯槽
使用#indicator插槽自定义具有ChatShimmer效果的加载指示器。
<script setup lang="ts">
import type { UIMessage } from 'ai'
import { useChat } from '@ai-sdk/vue'
const initialMessages: UIMessage[] = [{
id: '1',
role: 'user',
parts: [{ type: 'text', text: 'Hello! Can you help me with something?' }]
}]
const { messages } = useChat({
messages: initialMessages
})
const size = 4
const gap = 2
const totalDots = size * size
const patterns = [
[[0], [1], [2], [3], [7], [11], [15], [14], [13], [12], [8], [4], [5], [6], [10], [9]],
[[0, 4, 8, 12], [1, 5, 9, 13], [2, 6, 10, 14], [3, 7, 11, 15]],
[[5, 6, 9, 10], [1, 4, 7, 8, 11, 14], [0, 3, 12, 15], [1, 4, 7, 8, 11, 14], [5, 6, 9, 10]],
[[0], [1, 4], [2, 5, 8], [3, 6, 9, 12], [7, 10, 13], [11, 14], [15]]
]
const activeDots = ref<Set<number>>(new Set())
let patternIndex = 0
let stepIndex = 0
function nextStep() {
const pattern = patterns[patternIndex]
if (!pattern) return
activeDots.value = new Set(pattern[stepIndex])
stepIndex++
if (stepIndex >= pattern.length) {
stepIndex = 0
patternIndex = (patternIndex + 1) % patterns.length
}
}
const statusMessages = ['Searching...', 'Reading...', 'Analyzing...', 'Thinking...']
const currentIndex = ref(0)
const displayedText = ref(statusMessages[0]!)
const chars = 'abcdefghijklmnopqrstuvwxyz'
function scramble(from: string, to: string) {
const maxLength = Math.max(from.length, to.length)
let frame = 0
const totalFrames = 15
const step = () => {
frame++
let result = ''
const progress = (frame / totalFrames) * maxLength
for (let i = 0; i < maxLength; i++) {
if (i < progress - 2) {
result += to[i] || ''
} else if (i < progress) {
result += chars[Math.floor(Math.random() * chars.length)]
} else {
result += from[i] || ''
}
}
displayedText.value = result
if (frame < totalFrames) {
requestAnimationFrame(step)
} else {
displayedText.value = to
}
}
requestAnimationFrame(step)
}
let matrixInterval: ReturnType<typeof setInterval> | undefined
let textInterval: ReturnType<typeof setInterval> | undefined
onMounted(() => {
nextStep()
matrixInterval = setInterval(nextStep, 120)
textInterval = setInterval(() => {
const prev = displayedText.value
currentIndex.value = (currentIndex.value + 1) % statusMessages.length
scramble(prev, statusMessages[currentIndex.value]!)
}, 3000)
})
onUnmounted(() => {
clearInterval(matrixInterval)
clearInterval(textInterval)
})
</script>
<template>
<UChatMessages
:messages="messages"
status="submitted"
:should-scroll-to-bottom="false"
>
<template #indicator>
<div class="flex items-center gap-2 text-muted overflow-hidden">
<div
class="shrink-0 grid size-4"
:style="{
gridTemplateColumns: `repeat(${size}, 1fr)`,
gap: `${gap}px`
}"
>
<span
v-for="i in totalDots"
:key="i"
class="rounded-sm bg-current transition-opacity duration-100"
:class="activeDots.has(i - 1) ? 'opacity-100' : 'opacity-20'"
/>
</div>
<UChatShimmer :text="displayedText" class="text-sm font-mono" />
</div>
</template>
</UChatMessages>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import type { UIMessage } from 'ai'
import { useChat } from '@ai-sdk/vue'
const initialMessages: UIMessage[] = [{
id: '1',
role: 'user',
parts: [{ type: 'text', text: 'Hello! Can you help me with something?' }]
}]
const { messages } = useChat({
messages: initialMessages
})
const size = 4
const gap = 2
const totalDots = size * size
const patterns = [
[[0], [1], [2], [3], [7], [11], [15], [14], [13], [12], [8], [4], [5], [6], [10], [9]],
[[0, 4, 8, 12], [1, 5, 9, 13], [2, 6, 10, 14], [3, 7, 11, 15]],
[[5, 6, 9, 10], [1, 4, 7, 8, 11, 14], [0, 3, 12, 15], [1, 4, 7, 8, 11, 14], [5, 6, 9, 10]],
[[0], [1, 4], [2, 5, 8], [3, 6, 9, 12], [7, 10, 13], [11, 14], [15]]
]
const activeDots = ref<Set<number>>(new Set())
let patternIndex = 0
let stepIndex = 0
function nextStep() {
const pattern = patterns[patternIndex]
if (!pattern) return
activeDots.value = new Set(pattern[stepIndex])
stepIndex++
if (stepIndex >= pattern.length) {
stepIndex = 0
patternIndex = (patternIndex + 1) % patterns.length
}
}
const statusMessages = ['Searching...', 'Reading...', 'Analyzing...', 'Thinking...']
const currentIndex = ref(0)
const displayedText = ref(statusMessages[0]!)
const chars = 'abcdefghijklmnopqrstuvwxyz'
function scramble(from: string, to: string) {
const maxLength = Math.max(from.length, to.length)
let frame = 0
const totalFrames = 15
const step = () => {
frame++
let result = ''
const progress = (frame / totalFrames) * maxLength
for (let i = 0; i < maxLength; i++) {
if (i < progress - 2) {
result += to[i] || ''
} else if (i < progress) {
result += chars[Math.floor(Math.random() * chars.length)]
} else {
result += from[i] || ''
}
}
displayedText.value = result
if (frame < totalFrames) {
requestAnimationFrame(step)
} else {
displayedText.value = to
}
}
requestAnimationFrame(step)
}
let matrixInterval: ReturnType<typeof setInterval> | undefined
let textInterval: ReturnType<typeof setInterval> | undefined
onMounted(() => {
nextStep()
matrixInterval = setInterval(nextStep, 120)
textInterval = setInterval(() => {
const prev = displayedText.value
currentIndex.value = (currentIndex.value + 1) % statusMessages.length
scramble(prev, statusMessages[currentIndex.value]!)
}, 3000)
})
onUnmounted(() => {
clearInterval(matrixInterval)
clearInterval(textInterval)
})
</script>
<template>
<UChatMessages
:messages="messages"
status="submitted"
:should-scroll-to-bottom="false"
>
<template #indicator>
<div class="flex items-center gap-2 text-muted overflow-hidden">
<div
class="shrink-0 grid size-4"
:style="{
gridTemplateColumns: `repeat(${size}, 1fr)`,
gap: `${gap}px`
}"
>
<span
v-for="i in totalDots"
:key="i"
class="rounded-sm bg-current transition-opacity duration-100"
:class="activeDots.has(i - 1) ? 'opacity-100' : 'opacity-20'"
/>
</div>
<UChatShimmer :text="displayedText" class="text-sm font-mono" />
</div>
</template>
</UChatMessages>
</template>
API
Props
| Prop | Default | Type |
|---|---|---|
messages | T | |
status | "submitted" | "streaming" | "ready" | "error" | |
shouldAutoScroll | false | boolean Whether to automatically scroll to the bottom when a message is streaming. |
shouldScrollToBottom | true | boolean Whether to scroll to the bottom on mounted. |
autoScroll | true | boolean | Omit<ButtonProps, LinkPropsKeys> Display an auto scroll button.
|
autoScrollIcon | appConfig.ui.icons.arrowDown | anyThe icon displayed in the auto scroll button. |
user | Pick<PropsBase<T>, "actions" | "ui" | "color" | "variant" | "icon" | "avatar" | "side">The | |
assistant | Pick<PropsBase<T>, "actions" | "ui" | "color" | "variant" | "icon" | "avatar" | "side">The | |
compact | false | boolean Render the messages in a compact style.
This is done automatically when used inside a |
spacingOffset | 0 | numberThe spacing offset for the last message in px. Can be useful when the prompt is sticky for example. |
ui | { root?: SlotClass; indicator?: SlotClass; viewport?: SlotClass; autoScroll?: SlotClass; } |
Slots
| Slot | Type |
|---|---|
default | { registerMessageRef: (id: string, element: ComponentPublicInstance | null) => void; } |
indicator | { ui: object; } |
viewport | { ui: object; } |
header | UIMessage<unknown, UIDataTypes, UITools> & { message: MessageBase<T>; } |
leading | UIMessage<unknown, UIDataTypes, UITools> & { avatar: (AvatarProps & { [key: string]: any; }) | undefined; ui: object; } & { message: MessageBase<T>; } |
files | Omit<UIMessage<unknown, UIDataTypes, UITools>, "parts"> & { parts: FileUIPart[]; } & { message: MessageBase<T>; } |
body | UIMessage<unknown, UIDataTypes, UITools> & { message: MessageBase<T>; } |
content | UIMessage<unknown, UIDataTypes, UITools> & { content?: string | undefined; } & { message: MessageBase<T>; } |
actions | UIMessage<unknown, UIDataTypes, UITools> & { actions: (Omit<ButtonProps, "onClick"> & { onClick?: ((e: MouseEvent, message: UIMessage<unknown, UIDataTypes, UITools>) => void) | undefined; })[] | undefined; } & { message: MessageBase<T>; } |
ChatMessage组件的所有插槽,它们会自动转发,因此您可以在使用messages道具时自定义单个消息。<script setup lang="ts">
import { isTextUIPart } from 'ai'
</script>
<template>
<UChatMessages :messages="messages" :status="status">
<template #content="{ message }">
<template
v-for="(part, index) in message.parts"
:key="`${message.id}-${part.type}-${index}`"
>
<p v-if="isTextUIPart(part)" class="whitespace-pre-wrap">
{{ part.text }}
</p>
</template>
</template>
</UChatMessages>
</template>
曝光
通过模板引用访问组件时,可以使用以下命令:
| 名称 | 类型 |
|---|---|
registerMessageRef(id: string, element: ComponentPublicInstance | null) | void |
Theme
export default defineAppConfig({
ui: {
chatMessages: {
slots: {
root: 'w-full flex flex-col gap-1 flex-1 px-2.5 [&>article]:last-of-type:min-h-(--last-message-height)',
indicator: 'h-6 flex items-center gap-1 py-3 *:size-2 *:rounded-full *:bg-elevated motion-safe:[&>*:nth-child(1)]:animate-[bounce_1s_infinite] motion-safe:[&>*:nth-child(2)]:animate-[bounce_1s_0.15s_infinite] motion-safe:[&>*:nth-child(3)]:animate-[bounce_1s_0.3s_infinite]',
viewport: 'absolute inset-x-0 top-[86%] data-[state=open]:animate-[fade-in_200ms_var(--ease-out)] data-[state=closed]:animate-[fade-out_200ms_var(--ease-out)]',
autoScroll: 'rounded-full absolute right-1/2 translate-x-1/2 bottom-0'
},
variants: {
compact: {
true: '',
false: ''
}
}
}
}
})import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
ui: {
chatMessages: {
slots: {
root: 'w-full flex flex-col gap-1 flex-1 px-2.5 [&>article]:last-of-type:min-h-(--last-message-height)',
indicator: 'h-6 flex items-center gap-1 py-3 *:size-2 *:rounded-full *:bg-elevated motion-safe:[&>*:nth-child(1)]:animate-[bounce_1s_infinite] motion-safe:[&>*:nth-child(2)]:animate-[bounce_1s_0.15s_infinite] motion-safe:[&>*:nth-child(3)]:animate-[bounce_1s_0.3s_infinite]',
viewport: 'absolute inset-x-0 top-[86%] data-[state=open]:animate-[fade-in_200ms_var(--ease-out)] data-[state=closed]:animate-[fade-out_200ms_var(--ease-out)]',
autoScroll: 'rounded-full absolute right-1/2 translate-x-1/2 bottom-0'
},
variants: {
compact: {
true: '',
false: ''
}
}
}
}
})
]
})