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
24 changes: 13 additions & 11 deletions src/runtime/components/ChatMessage.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
<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<METADATA = unknown, DATA_PARTS extends UIDataTypes = UIDataTypes, TOOLS extends UITools = UITools>
extends UIMessage<METADATA, DATA_PARTS, TOOLS> {
/**
* The element or component this component should render as.
* @defaultValue 'article'
Expand All @@ -31,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<METADATA, DATA_PARTS, TOOLS>) => void })[]
/**
* Render the message in a compact style.
* This is done automatically when used inside a `UChatPalette`{lang="ts-type"}.
Expand All @@ -47,14 +48,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<METADATA = unknown, DATA_PARTS extends UIDataTypes = UIDataTypes, TOOLS extends UITools = UITools> {
leading(props: { avatar: ChatMessageProps<METADATA, DATA_PARTS, TOOLS>['avatar'], ui: ChatMessage['ui'] }): any
content(props: Pick<ChatMessageProps<METADATA, DATA_PARTS, TOOLS>, 'id' | 'role' | 'parts' | 'metadata' | 'content'>): any
actions(props: { actions: ChatMessageProps<METADATA, DATA_PARTS, TOOLS>['actions'] }): any
}
</script>

<script setup lang="ts">
<script setup lang="ts" generic="METADATA = unknown, DATA_PARTS extends UIDataTypes = UIDataTypes, TOOLS extends UITools = UITools">
import { computed } from 'vue'
import { Primitive } from 'reka-ui'
import { useAppConfig } from '#imports'
Expand All @@ -65,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<METADATA, DATA_PARTS, TOOLS>>(), {
as: 'article'
})
const slots = defineSlots<ChatMessageSlots>()
const slots = defineSlots<ChatMessageSlots<METADATA, DATA_PARTS, TOOLS>>()

const appConfig = useAppConfig() as ChatMessage['AppConfig']

Expand Down Expand Up @@ -98,13 +99,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
18 changes: 9 additions & 9 deletions src/runtime/components/ChatMessages.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,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 @@ -57,21 +57,21 @@ export interface ChatMessagesProps {
ui?: ChatMessages['slots']
}

type ExtendSlotWithVersion<K extends keyof ChatMessageSlots>
type ExtendSlotWithVersion<K extends keyof ChatMessageSlots, T extends UIMessage[]>
= ChatMessageSlots[K] extends (props: infer P) => any
? (props: P & { message: UIMessage }) => any
? (props: P & { message: T[number] }) => any
: ChatMessageSlots[K]

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

<script setup lang="ts">
<script setup lang="ts" generic="T extends UIMessage[] = UIMessage[]">
import type { ComponentPublicInstance } from 'vue'
import { ref, computed, watch, nextTick, toRef, onMounted } from 'vue'
import { Presence } from 'reka-ui'
Expand All @@ -83,13 +83,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
Loading