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
23 changes: 23 additions & 0 deletions docs/content/docs/2.components/chat-message.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,29 @@ props:
When using the [`ChatMessages`](/docs/components/chat-messages) component, the `variant` prop is set to `naked` for `assistant` messages and `soft` for `user` messages.
::

### Color :badge{label="Soon" class="align-text-top"}

Use the `color` prop to change the color of the message.

::component-code
---
prettier: true
ignore:
- parts
- role
- id
props:
variant: 'soft'
color: 'primary'
parts:
- type: 'text'
id: '1'
text: 'Hello! Tell me more about building AI chatbots with Nuxt UI.'
role: 'user'
id: '1'
---
::

### Icon

Use the `icon` prop to display an [Icon](/docs/components/icon) component next to the message.
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-message',
'chat-reasoning',
'chat-shimmer',
'chat-tool',
Expand Down
22 changes: 22 additions & 0 deletions playgrounds/nuxt/app/pages/components/chat-message.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<script setup lang="ts">
import theme from '#build/ui/chat-message'

const colors = Object.keys(theme.variants.color)
const variants = Object.keys(theme.variants.variant)

const attrs = reactive({
color: [theme.defaultVariants.color],
variant: [theme.defaultVariants.variant]
})
</script>

<template>
<Navbar>
<USelect v-model="attrs.color" :items="colors" multiple />
<USelect v-model="attrs.variant" :items="variants" multiple />
</Navbar>

<Matrix v-slot="props" :attrs="attrs">
<UChatMessage id="1" role="user" :parts="[{ type: 'text', text: 'Hello, how are you?' }]" v-bind="props" />
</Matrix>
</template>
14 changes: 12 additions & 2 deletions src/runtime/components/ChatMessage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export interface ChatMessageProps<TMetadata = unknown, TDataParts extends UIData
* @defaultValue 'naked'
*/
variant?: ChatMessage['variants']['variant']
/**
* @defaultValue 'neutral'
*/
color?: ChatMessage['variants']['color']
/**
* @defaultValue 'left'
*/
Expand All @@ -49,6 +53,7 @@ export interface ChatMessageProps<TMetadata = unknown, TDataParts extends UIData
}

export interface ChatMessageSlots<TMetadata = unknown, TDataParts extends UIDataTypes = UIDataTypes, TTools extends UITools = UITools> {
header?(props: UIMessage<TMetadata, TDataParts, TTools>): VNode[]
leading?(props: UIMessage<TMetadata, TDataParts, TTools> & { avatar: ChatMessageProps<TMetadata, TDataParts, TTools>['avatar'], ui: ChatMessage['ui'] }): VNode[]
files?(props: Omit<UIMessage<TMetadata, TDataParts, TTools>, 'parts'> & { parts: FileUIPart[] }): VNode[]
content?(props: UIMessage<TMetadata, TDataParts, TTools> & { content?: string }): VNode[]
Expand Down Expand Up @@ -83,6 +88,7 @@ const messageProps = computed(() => omit(props, ['as', 'icon', 'avatar', 'varian

const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.chatMessage || {}) })({
variant: props.variant,
color: props.color,
side: props.side,
leading: !!props.icon || !!props.avatar || !!slots.leading,
actions: !!props.actions || !!slots.actions,
Expand All @@ -92,8 +98,12 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.chatMessage

<template>
<Primitive :as="as" :data-role="role" data-slot="root" :class="ui.root({ class: [uiProp?.root, props.class] })">
<div v-if="!!slots.files && fileParts.length" data-slot="files" :class="ui.files({ class: uiProp?.files })">
<slot name="files" v-bind="{ ...messageProps, parts: fileParts }" />
<div v-if="(!!slots.files && fileParts.length) || !!slots.header" data-slot="header" :class="ui.header({ class: uiProp?.header })">
<slot name="header" v-bind="{ ...messageProps }">
<div v-if="!!slots.files && fileParts.length" data-slot="files" :class="ui.files({ class: uiProp?.files })">
<slot name="files" v-bind="{ ...messageProps, parts: fileParts }" />
</div>
</slot>
</div>

<div data-slot="container" :class="ui.container({ class: uiProp?.container })">
Expand Down
76 changes: 60 additions & 16 deletions src/theme/chat-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ModuleOptions } from '../module'
export default (options: Required<ModuleOptions>) => ({
slots: {
root: 'group/message relative w-full',
header: '',
container: 'relative flex items-start',
leading: 'inline-flex items-center justify-center min-h-6',
leadingIcon: 'shrink-0',
Expand All @@ -14,21 +15,15 @@ export default (options: Required<ModuleOptions>) => ({
},
variants: {
variant: {
solid: {
content: 'bg-inverted text-inverted'
},
outline: {
content: 'bg-default ring ring-default'
},
soft: {
content: 'bg-elevated/50'
},
subtle: {
content: 'bg-elevated/50 ring ring-default'
},
naked: {
content: ''
}
solid: '',
outline: '',
soft: '',
subtle: '',
naked: ''
},
color: {
...Object.fromEntries((options.theme.colors || []).map((color: string) => [color, ''])),
neutral: ''
},
side: {
left: {},
Expand Down Expand Up @@ -100,8 +95,57 @@ export default (options: Required<ModuleOptions>) => ({
class: {
content: 'w-full'
}
}, ...(options.theme.colors || []).map((color: string) => ({
color,
variant: 'solid',
class: {
content: `bg-${color} text-inverted`
}
})), ...(options.theme.colors || []).map((color: string) => ({
color,
variant: 'outline',
class: {
content: `text-${color} ring ring-${color}/25`
}
})), ...(options.theme.colors || []).map((color: string) => ({
color,
variant: 'soft',
class: {
content: `bg-${color}/10 text-${color}`
}
})), ...(options.theme.colors || []).map((color: string) => ({
color,
variant: 'subtle',
class: {
content: `bg-${color}/10 text-${color} ring ring-${color}/25`
}
})), {
color: 'neutral',
variant: 'solid',
class: {
content: 'bg-inverted text-inverted'
}
}, {
color: 'neutral',
variant: 'outline',
class: {
content: 'bg-default ring ring-default'
}
}, {
color: 'neutral',
variant: 'soft',
class: {
content: 'bg-elevated/50'
}
}, {
color: 'neutral',
variant: 'subtle',
class: {
content: 'bg-elevated/50 ring ring-default'
}
}],
defaultVariants: {
variant: 'naked'
variant: 'naked',
color: 'neutral'
}
})
4 changes: 3 additions & 1 deletion test/components/ChatMessage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ describe('ChatMessage', () => {
['with role assistant', { props: { ...props, role: 'assistant' } }],
['with side right', { props: { ...props, side: 'right' } }],
['with compact', { props: { ...props, compact: true } }],
...variants.map((variant: string) => [`with variant ${variant}`, { props: { ...props, variant } }]),
...variants.map((variant: string) => [`with neutral variant ${variant}`, { props: { ...props, variant } }]),
...variants.map((variant: string) => [`with primary variant ${variant}`, { props: { ...props, variant, color: 'primary' } }]),
['with as', { props: { ...props, as: 'section' } }],
['with class', { props: { ...props, class: '' } }],
['with ui', { props: { ...props, ui: {} } }],
// Slots
['with header slot', { props, slots: { header: () => 'Header slot' } }],
['with leading slot', { props, slots: { leading: () => 'Leading slot' } }],
['with files slot', { props: { ...props, parts: [...props.parts, { type: 'file' as const, mediaType: 'text/plain', url: 'https://example.com/test.txt', filename: 'test.txt' }] }, slots: { files: () => 'Files slot' } }],
['with content slot', { props, slots: { content: () => 'Content slot' } }],
Expand Down
Loading
Loading