Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ function onClose(e: Event) {
:user="{ side: 'left', variant: 'naked', avatar: { src: 'https://github.com/benjamincanac.png', loading: 'lazy' as const } }"
:assistant="{ icon: 'i-lucide-bot' }"
>
<template #content="{ message }">
<template #content="message">
<template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
<MDC
v-if="part.type === 'text' && message.role === 'assistant'"
:value="part.text"
:cache-key="`${message.id}-${index}`"
class="[&_.my-5]:my-2.5 *:first:!mt-0 *:last:!mb-0 [&_.leading-7]:!leading-6"
class="[&_.my-5]:my-2.5 *:first:mt-0! *:last:mb-0! [&_.leading-7]:leading-6!"
/>
<p v-else-if="part.type === 'text' && message.role === 'user'" class="whitespace-pre-wrap">
{{ part.text }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ function onSubmit() {
:user="{ side: 'left', variant: 'naked', avatar: { src: 'https://github.com/benjamincanac.png', loading: 'lazy' as const } }"
:assistant="{ icon: 'i-lucide-bot' }"
>
<template #content="{ message }">
<template #content="message">
<template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
<MDC
v-if="part.type === 'text' && message.role === 'assistant'"
:value="part.text"
:cache-key="`${message.id}-${index}`"
class="[&_.my-5]:my-2.5 *:first:!mt-0 *:last:!mb-0 [&_.leading-7]:!leading-6"
class="[&_.my-5]:my-2.5 *:first:mt-0! *:last:mb-0! [&_.leading-7]:leading-6!"
/>
<p v-else-if="part.type === 'text' && message.role === 'user'" class="whitespace-pre-wrap">
{{ part.text }}
Expand Down
4 changes: 2 additions & 2 deletions docs/app/components/search/SearchChat.vue
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,15 @@ const getCachedToolMessage = useMemoize((state: State, toolName: string, input:
:user="{ side: 'left', variant: 'naked', icon: 'i-lucide-user' }"
:assistant="{ icon: 'i-lucide-bot' }"
>
<template #content="{ message }">
<template #content="message">
<template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}${'state' in part ? `-${part.state}` : ''}`">
<MDCCached
v-if="part.type === 'text' && message.role === 'assistant'"
:value="part.text"
:cache-key="`${message.id}-${index}`"
:components="components"
:parser-options="{ highlight: false }"
class="[&_.my-5]:my-2.5 *:first:!mt-0 *:last:!mb-0 [&_.leading-7]:!leading-6"
class="[&_.my-5]:my-2.5 *:first:mt-0! *:last:mb-0! [&_.leading-7]:leading-6!"
/>
<p v-else-if="part.type === 'text' && message.role === 'user'" class="whitespace-pre-wrap">
{{ part.text }}
Expand Down
6 changes: 3 additions & 3 deletions docs/content/blog/how-to-build-an-ai-chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ onMounted(() => {
should-auto-scroll
class="flex-1"
>
<template #content="{ message }">
<template #content="message">
<template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
<MDC v-if="part.type === 'text' && message.role === 'assistant'" :value="part.text" :cache-key="`${message.id}-${index}`" class="*:first:mt-0 *:last:mb-0" />
<p v-else-if="part.type === 'text' && message.role === 'user'" class="whitespace-pre-wrap">{{ part.text }}</p>
Expand Down Expand Up @@ -779,7 +779,7 @@ onMounted(() => {
should-auto-scroll
class="flex-1"
>
<template #content="{ message }">
<template #content="message">
<template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
<MDC v-if="part.type === 'text' && message.role === 'assistant'" :value="part.text" :cache-key="`${message.id}-${index}`" class="*:first:mt-0 *:last:mb-0" />
<p v-else-if="part.type === 'text' && message.role === 'user'" class="whitespace-pre-wrap">{{ part.text }}</p>
Expand Down Expand Up @@ -944,7 +944,7 @@ onMounted(() => {
should-auto-scroll
class="flex-1"
>
<template #content="{ message }">
<template #content="message">
<template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
<MDC v-if="part.type === 'text' && message.role === 'assistant'" :value="part.text" :cache-key="`${message.id}-${index}`" class="*:first:mt-0 *:last:mb-0" />
<p v-else-if="part.type === 'text' && message.role === 'user'" class="whitespace-pre-wrap">{{ part.text }}</p>
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/1.getting-started/3.migration/1.v4.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ This section only applies if you're using the AI SDK and chat components (`ChatM
```vue
<template>
<UChatMessages :messages="chat.messages" :status="chat.status">
<template #content="{ message }">
<template #content="message">
<template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
<MDC v-if="part.type === 'text' && message.role === 'assistant'" :value="part.text" :cache-key="`${message.id}-${index}`" class="*:first:mt-0 *:last:mb-0" />
<p v-else-if="part.type === 'text' && message.role === 'user'" class="whitespace-pre-wrap">{{ part.text }}</p>
Expand Down
4 changes: 2 additions & 2 deletions docs/content/docs/2.components/chat-messages.md
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ function onSubmit() {
<template #body>
<UContainer>
<UChatMessages :messages="chat.messages" :status="chat.status">
<template #content="{ message }">
<template #content="message">
<template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
<MDC v-if="part.type === 'text' && message.role === 'assistant'" :value="part.text" :cache-key="`${message.id}-${index}`" class="*:first:mt-0 *:last:mb-0" />
<p v-else-if="part.type === 'text' && message.role === 'user'" class="whitespace-pre-wrap">{{ part.text }}</p>
Expand Down Expand Up @@ -517,7 +517,7 @@ You can use all the slots of the [`ChatMessage`](/docs/components/chat-message#s
```vue{5-9}
<template>
<UChatMessages :messages="messages" :status="status">
<template #content="{ message }">
<template #content="message">
<template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
<MDC v-if="part.type === 'text' && message.role === 'assistant'" :value="part.text" :cache-key="`${message.id}-${index}`" class="*:first:mt-0 *:last:mb-0" />
<p v-else-if="part.type === 'text' && message.role === 'user'" class="whitespace-pre-wrap">{{ part.text }}</p>
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/2.components/chat-prompt-submit.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ function onSubmit() {
<template #body>
<UContainer>
<UChatMessages :messages="chat.messages" :status="chat.status">
<template #content="{ message }">
<template #content="message">
<template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
<MDC v-if="part.type === 'text' && message.role === 'assistant'" :value="part.text" :cache-key="`${message.id}-${index}`" class="*:first:mt-0 *:last:mb-0" />
<p v-else-if="part.type === 'text' && message.role === 'user'" class="whitespace-pre-wrap">{{ part.text }}</p>
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/2.components/chat-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ function onSubmit() {
<template #body>
<UContainer>
<UChatMessages :messages="chat.messages" :status="chat.status">
<template #content="{ message }">
<template #content="message">
<template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
<MDC v-if="part.type === 'text' && message.role === 'assistant'" :value="part.text" :cache-key="`${message.id}-${index}`" class="*:first:mt-0 *:last:mb-0" />
<p v-else-if="part.type === 'text' && message.role === 'user'" class="whitespace-pre-wrap">{{ part.text }}</p>
Expand Down
2 changes: 1 addition & 1 deletion playgrounds/nuxt/app/pages/chat.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function onSubmit() {
:user="{ avatar: { src: 'https://github.com/benjamincanac.png' } }"
:spacing-offset="48"
>
<template #content="{ message }">
<template #content="message">
<template
v-for="(part, index) in message.parts"
:key="`${message.id}-${part.type}-${index}${'state' in part ? `-${part.state}` : ''}`"
Expand Down
2 changes: 1 addition & 1 deletion skills/nuxt-ui/references/layouts/chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ function onSubmit() {
<template #body>
<UContainer>
<UChatMessages :messages="chat.messages" :status="chat.status">
<template #content="{ message }">
<template #content="message">
<template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
<MDC
v-if="part.type === 'text' && message.role === 'assistant'"
Expand Down
23 changes: 12 additions & 11 deletions src/runtime/components/ChatMessage.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<script lang="ts">
import type { VNode } from 'vue'
import type { AppConfig } from '@nuxt/schema'
import type { UIMessage } from 'ai'
import type { UIDataTypes, UIMessage, UITools } from 'ai'
import theme from '#build/ui/chat-message'
import type { AvatarProps, ButtonProps, IconProps } from '../types'
import type { ComponentConfig } from '../types/tv'

type ChatMessage = ComponentConfig<typeof theme, AppConfig, 'chatMessage'>

export interface ChatMessageProps extends UIMessage {
export interface ChatMessageProps<TMetadata = unknown, TDataParts extends UIDataTypes = UIDataTypes, TTools extends UITools = UITools> extends UIMessage<TMetadata, TDataParts, TTools> {
/**
* The element or component this component should render as.
* @defaultValue 'article'
Expand All @@ -32,7 +32,7 @@ export interface ChatMessageProps extends UIMessage {
* The `label` will be used in a tooltip.
* `{ size: 'xs', color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
*/
actions?: (Omit<ButtonProps, 'onClick'> & { onClick?: (e: MouseEvent, message: UIMessage) => void })[]
actions?: (Omit<ButtonProps, 'onClick'> & { onClick?: (e: MouseEvent, message: UIMessage<TMetadata, TDataParts, TTools>) => void })[]
/**
* Render the message in a compact style.
* This is done automatically when used inside a `UChatPalette`{lang="ts-type"}.
Expand All @@ -48,14 +48,14 @@ export interface ChatMessageProps extends UIMessage {
ui?: ChatMessage['slots']
}

export interface ChatMessageSlots {
leading?(props: { avatar: ChatMessageProps['avatar'], ui: ChatMessage['ui'] }): VNode[]
content?(props: ChatMessageProps): VNode[]
actions?(props: { actions: ChatMessageProps['actions'] }): VNode[]
export interface ChatMessageSlots<TMetadata = unknown, TDataParts extends UIDataTypes = UIDataTypes, TTools extends UITools = UITools> {
leading?(props: { avatar: ChatMessageProps<TMetadata, TDataParts, TTools>['avatar'], ui: ChatMessage['ui'] }): VNode[]
content?(props: Pick<ChatMessageProps<TMetadata, TDataParts, TTools>, 'id' | 'role' | 'parts' | 'metadata' | 'content'>): VNode[]
actions?(props: { actions: ChatMessageProps<TMetadata, TDataParts, TTools>['actions'] }): VNode[]
}
</script>

<script setup lang="ts">
<script setup lang="ts" generic="TMetadata, TDataParts extends UIDataTypes, TTools extends UITools">
import { computed } from 'vue'
import { Primitive } from 'reka-ui'
import { useAppConfig } from '#imports'
Expand All @@ -67,10 +67,10 @@ import UTooltip from './Tooltip.vue'
import UAvatar from './Avatar.vue'
import UIcon from './Icon.vue'

const props = withDefaults(defineProps<ChatMessageProps>(), {
const props = withDefaults(defineProps<ChatMessageProps<TMetadata, TDataParts, TTools>>(), {
as: 'article'
})
const slots = defineSlots<ChatMessageSlots>()
const slots = defineSlots<ChatMessageSlots<TMetadata, TDataParts, TTools>>()

const appConfig = useAppConfig() as ChatMessage['AppConfig']
const uiProp = useComponentUI('chatMessage', props)
Expand Down Expand Up @@ -101,13 +101,14 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.chatMessage
:role="role"
:content="content"
:parts="parts"
:metadata="metadata"
>
<template v-if="content">
{{ content }}
</template>
<template v-else>
<template v-for="(part, index) in parts" :key="`${id}-${part.type}-${index}`">
<template v-if="part.type === 'text'">
<template v-if="part.type === 'text' && 'text' in part">
{{ part.text }}
</template>
</template>
Expand Down
34 changes: 17 additions & 17 deletions src/runtime/components/ChatMessages.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import type { ComponentConfig } from '../types/tv'

type ChatMessages = ComponentConfig<typeof theme, AppConfig, 'chatMessages'>

export interface ChatMessagesProps {
messages?: UIMessage[]
export interface ChatMessagesProps<T extends UIMessage[] = UIMessage[]> {
messages?: T
status?: ChatStatus
/**
* Whether to automatically scroll to the bottom when a message is streaming.
Expand Down Expand Up @@ -59,42 +59,42 @@ export interface ChatMessagesProps {
ui?: ChatMessages['slots']
}

type ExtendSlotWithVersion<K extends keyof ChatMessageSlots>
= Required<ChatMessageSlots>[K] extends (props: infer P) => VNode[]
? (props: P & { message: UIMessage }) => VNode[]
: Required<ChatMessageSlots>[K]
type SlotBase<T extends UIMessage[]>
= T[number] extends UIMessage<infer M, infer D, infer U>
? ChatMessageSlots<M, D, U>
: ChatMessageSlots

export type ChatMessagesSlots = {
[K in keyof ChatMessageSlots]?: ExtendSlotWithVersion<K>
} & {
export type ChatMessagesSlots<T extends UIMessage[] = UIMessage[]> = {
default?(props?: {}): VNode[]
indicator?(props: { ui: ChatMessages['ui'] }): VNode[]
viewport?(props: { ui: ChatMessages['ui'], onClick: () => void }): VNode[]
}
} & SlotBase<T>

</script>

<script setup lang="ts">
<script setup lang="ts" generic="T extends UIMessage[] = UIMessage[]">
import { ref, computed, watch, nextTick, toRef, onMounted } from 'vue'
import { Presence } from 'reka-ui'
import { defu } from 'defu'
import { useElementBounding, useEventListener, watchThrottled } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { useComponentUI } from '../composables/useComponentUI'
import { omit } from '../utils'
import { tv } from '../utils/tv'
import UChatMessage from './ChatMessage.vue'
import UButton from './Button.vue'

const props = withDefaults(defineProps<ChatMessagesProps>(), {
const props = withDefaults(defineProps<ChatMessagesProps<T>>(), {
autoScroll: true,
shouldAutoScroll: false,
shouldScrollToBottom: true,
spacingOffset: 0
})
const slots = defineSlots<ChatMessagesSlots>()
const slots = defineSlots<ChatMessagesSlots<T>>()

const getProxySlots = () => omit(slots, ['default', 'indicator', 'viewport'])
function getSlotsKeys() {
const omitKeys = ['default', 'indicator', 'viewport']
return Object.keys(slots).filter(key => !omitKeys.includes(key)) as (keyof SlotBase<T>)[]
}

const appConfig = useAppConfig() as ChatMessages['AppConfig']
const uiProp = useComponentUI('chatMessages', props)
Expand Down Expand Up @@ -304,8 +304,8 @@ onMounted(() => {
:ref="el => registerMessageRef(message.id, el as ComponentPublicInstance)"
:compact="compact"
>
<template v-for="(_, name) in getProxySlots()" #[name]="slotData">
<slot :name="name" v-bind="(slotData as any)" :message="message" />
<template v-for="name in getSlotsKeys()" #[name]="messageOrData">
<slot :name="name" v-bind="messageOrData" />
</template>
</UChatMessage>
</slot>
Expand Down
Loading