Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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 @@ -65,7 +65,7 @@ function onClose(e: Event) {
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 @@ -32,7 +32,7 @@ function onSubmit() {
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
2 changes: 1 addition & 1 deletion docs/app/components/search/SearchChat.vue
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ const getCachedToolMessage = useMemoize((state: State, toolName: string, input:
: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
23 changes: 12 additions & 11 deletions src/runtime/components/ChatMessage.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<script lang="ts">
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 @@ -31,7 +31,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 @@ -47,14 +47,14 @@ export interface ChatMessageProps extends UIMessage {
ui?: ChatMessage['slots']
}

export interface ChatMessageSlots {
leading(props: { avatar: ChatMessageProps['avatar'], ui: ChatMessage['ui'] }): any
content(props: ChatMessageProps): any
actions(props: { actions: ChatMessageProps['actions'] }): any
export interface ChatMessageSlots<TMetadata = unknown, TDataParts extends UIDataTypes = UIDataTypes, TTools extends UITools = UITools> {
leading(props: { avatar: ChatMessageProps<TMetadata, TDataParts, TTools>['avatar'], ui: ChatMessage['ui'] }): any
content(props: Pick<ChatMessageProps<TMetadata, TDataParts, TTools>, 'id' | 'role' | 'parts' | 'metadata' | 'content'>): any
actions(props: { actions: ChatMessageProps<TMetadata, TDataParts, TTools>['actions'] }): any
}
</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 @@ -66,10 +66,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 @@ -100,13 +100,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
29 changes: 17 additions & 12 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,13 +59,18 @@ export interface ChatMessagesProps {
ui?: ChatMessages['slots']
}

type ExtendSlotWithVersion<K extends keyof ChatMessageSlots>
= ChatMessageSlots[K] extends (props: infer P) => any
? (props: P & { message: UIMessage }) => any
: 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>
type ExtendSlotWithVersion<K extends keyof SlotBase<T>, T extends UIMessage[] = UIMessage[]>
= SlotBase<T>[K] extends (props: infer P) => any
? (props: P & { message: T[number] }) => any
: SlotBase<T>[K]

export type ChatMessagesSlots<T extends UIMessage[] = UIMessage[]> = {
[K in keyof SlotBase<T>]: ExtendSlotWithVersion<K, T>
} & {
default(props?: {}): any
indicator(props: { ui: ChatMessages['ui'] }): any
Expand All @@ -74,7 +79,7 @@ export type ChatMessagesSlots = {

</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'
Expand All @@ -86,13 +91,13 @@ 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'])

Expand Down Expand Up @@ -305,7 +310,7 @@ onMounted(() => {
:compact="compact"
>
<template v-for="(_, name) in getProxySlots()" #[name]="slotData">
<slot :name="name" v-bind="(slotData as any)" :message="message" />
<slot :name="(name as keyof ChatMessagesSlots<T>)" v-bind="(slotData as any)" :message="message" />
</template>
</UChatMessage>
</slot>
Expand Down
Loading