diff --git a/dev/messages-ai-chat.html b/dev/messages-ai-chat.html index a8e92a54b29..e3989deae61 100644 --- a/dev/messages-ai-chat.html +++ b/dev/messages-ai-chat.html @@ -124,8 +124,31 @@ // Set initial messages list.items = [ - createItem('Hello! Can you help me with a question?'), - createItem("Of course! I'm here to help. What's your question?", true), + { + text: "Can you help me analyze these Q3 financial documents? I need a summary of the key findings and any concerns you spot.", + time: 'Yesterday', + userName: 'User', + userColorIndex: 1, + attachments: [ + { name: 'proposal.pdf', url: '#proposal.pdf', type: 'application/pdf' }, + { + name: 'budget.xlsx', + url: '#budget.xlsx', + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }, + { + name: 'chart.png', + url: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=300', + type: 'image/png', + }, + ], + }, + { + text: "I've reviewed your Q3 financial documents. Here's a summary:\n\n**Key Findings:**\n- Revenue increased 12% compared to Q2\n- Operating costs remained stable\n- The chart shows positive growth trends\n\n**Concerns:**\n- Marketing spend is 15% over budget\n- Cash flow projections need revision\n\nWould you like me to elaborate on any of these points?", + time: 'Yesterday', + userName: 'Assistant', + userColorIndex: 2, + }, ]; // Handle new messages from user diff --git a/packages/message-list/src/vaadin-message-list-mixin.d.ts b/packages/message-list/src/vaadin-message-list-mixin.d.ts index 0013b8afc7e..61a29669808 100644 --- a/packages/message-list/src/vaadin-message-list-mixin.d.ts +++ b/packages/message-list/src/vaadin-message-list-mixin.d.ts @@ -6,6 +6,12 @@ import type { Constructor } from '@open-wc/dedupe-mixin'; import type { KeyboardDirectionMixinClass } from '@vaadin/a11y-base/src/keyboard-direction-mixin.js'; +export interface MessageListItemAttachment { + name?: string; + url?: string; + type?: string; +} + export interface MessageListItem { text?: string; time?: string; @@ -15,6 +21,7 @@ export interface MessageListItem { userColorIndex?: number; theme?: string; className?: string; + attachments?: MessageListItemAttachment[]; } export declare function MessageListMixin>( @@ -34,9 +41,18 @@ export declare class MessageListMixinClass { * userImg: string, * userColorIndex: number, * className: string, - * theme: string + * theme: string, + * attachments: Array<{ + * name: string, + * url: string, + * type: string + * }> * }> * ``` + * + * When a message has attachments, they are rendered in the attachments slot. + * Image attachments (type starting with "image/") show a thumbnail preview, + * while other attachments show a document icon with the file name. */ items: MessageListItem[] | null | undefined; diff --git a/packages/message-list/src/vaadin-message-list-mixin.js b/packages/message-list/src/vaadin-message-list-mixin.js index 9d05324e5a4..aa8494242a1 100644 --- a/packages/message-list/src/vaadin-message-list-mixin.js +++ b/packages/message-list/src/vaadin-message-list-mixin.js @@ -27,9 +27,18 @@ export const MessageListMixin = (superClass) => * userImg: string, * userColorIndex: number, * className: string, - * theme: string + * theme: string, + * attachments: Array<{ + * name: string, + * url: string, + * type: string + * }> * }> * ``` + * + * When a message has attachments, they are rendered in the attachments slot. + * Image attachments (type starting with "image/") show a thumbnail preview, + * while other attachments show a document icon with the file name. */ items: { type: Array, @@ -141,7 +150,7 @@ export const MessageListMixin = (superClass) => class="${ifDefined(item.className)}" @focusin="${this._onMessageFocusIn}" style="${ifDefined(loadingMarkdown ? 'visibility: hidden' : undefined)}" - >${this.markdown + >${this.__renderAttachments(item)}${this.markdown ? html`` : item.text} @@ -153,6 +162,60 @@ export const MessageListMixin = (superClass) => ); } + /** + * Renders attachments for a message item. + * @param {Object} item - The message item + * @return {import('lit').TemplateResult | string} + * @private + */ + __renderAttachments(item) { + const attachments = item.attachments; + if (!attachments || attachments.length === 0) { + return ''; + } + + return html` +
+ ${attachments.map((attachment) => this.__renderAttachment(attachment))} +
+ `; + } + + /** + * Renders a single attachment. + * @param {Object} attachment - The attachment object with name, url, and type properties + * @return {import('lit').TemplateResult} + * @private + */ + __renderAttachment(attachment) { + const isImage = attachment.type && attachment.type.startsWith('image/'); + + if (isImage) { + return html` + + ${attachment.name || ''} + + `; + } + + return html` + + + ${attachment.name || attachment.url} + + `; + } + /** @private */ _scrollToLastMessage() { if (this.items.length > 0) { diff --git a/packages/message-list/src/vaadin-message-list.d.ts b/packages/message-list/src/vaadin-message-list.d.ts index 4555ecbc610..b1445a5d737 100644 --- a/packages/message-list/src/vaadin-message-list.d.ts +++ b/packages/message-list/src/vaadin-message-list.d.ts @@ -8,7 +8,7 @@ import { SlotStylesMixin } from '@vaadin/component-base/src/slot-styles-mixin.js import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js'; import { MessageListMixin } from './vaadin-message-list-mixin.js'; -export { MessageListItem } from './vaadin-message-list-mixin.js'; +export { MessageListItem, MessageListItemAttachment } from './vaadin-message-list-mixin.js'; /** * `` is a Web Component for showing an ordered list of messages. The messages are rendered as diff --git a/packages/message-list/src/vaadin-message-list.js b/packages/message-list/src/vaadin-message-list.js index 239edb12d0b..e2753e04e86 100644 --- a/packages/message-list/src/vaadin-message-list.js +++ b/packages/message-list/src/vaadin-message-list.js @@ -92,6 +92,45 @@ class MessageList extends SlotStylesMixin(MessageListMixin(ElementMixin(Themable ${tag} :where(vaadin-markdown > :is(h1, h2, h3, h4, h5, h6, p, ul, ol):last-child) { margin-bottom: 0; } + + ${tag} .vaadin-message-attachments { + display: flex; + flex-wrap: wrap; + gap: 6px; + } + + ${tag} .vaadin-message-attachment { + display: inline-flex; + align-items: center; + color: inherit; + } + + ${tag} .vaadin-message-attachment-image img { + display: block; + max-width: 200px; + max-height: 150px; + } + + ${tag} .vaadin-message-attachment-file { + gap: 6px; + } + + ${tag} .vaadin-message-attachment-icon { + display: inline-block; + width: 1em; + height: 1em; + background: currentColor; + mask-image: url('data:image/svg+xml;utf8,'); + mask-size: contain; + mask-repeat: no-repeat; + flex-shrink: 0; + } + + ${tag} .vaadin-message-attachment-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } `, ]; } diff --git a/packages/message-list/src/vaadin-message.js b/packages/message-list/src/vaadin-message.js index ea993fd326c..51126c70868 100644 --- a/packages/message-list/src/vaadin-message.js +++ b/packages/message-list/src/vaadin-message.js @@ -23,6 +23,12 @@ import { MessageMixin } from './vaadin-message-mixin.js'; * user-img = "/static/img/avatar.jpg">There is no real ending. It's just the place where you stop the story. * ``` * + * ### Slots + * + * Slot name | Description + * --------------|---------------- + * `attachments` | Content to be displayed above the message text. Used for file and image attachments. + * * ### Styling * * The following shadow DOM parts are available for styling: @@ -70,6 +76,7 @@ class Message extends MessageMixin(ElementMixin(ThemableMixin(PolylitMixin(LumoI ${this.userName} ${this.time} +
`; diff --git a/packages/message-list/test/dom/__snapshots__/message.test.snap.js b/packages/message-list/test/dom/__snapshots__/message.test.snap.js index 5694b873e8d..02a54a41729 100644 --- a/packages/message-list/test/dom/__snapshots__/message.test.snap.js +++ b/packages/message-list/test/dom/__snapshots__/message.test.snap.js @@ -11,6 +11,8 @@ snapshots["vaadin-message default"] = + +
@@ -30,6 +32,8 @@ snapshots["vaadin-message userName"] =
+ +
@@ -49,6 +53,8 @@ snapshots["vaadin-message time"] = long ago
+ +
diff --git a/packages/message-list/test/typings/message-list.types.ts b/packages/message-list/test/typings/message-list.types.ts index 6e7f414603f..98126efe1ef 100644 --- a/packages/message-list/test/typings/message-list.types.ts +++ b/packages/message-list/test/typings/message-list.types.ts @@ -3,7 +3,7 @@ import '../../vaadin-message-list.js'; import type { FocusMixinClass } from '@vaadin/a11y-base/src/focus-mixin.js'; import type { ElementMixinClass } from '@vaadin/component-base/src/element-mixin.js'; import type { ThemableMixinClass } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js'; -import type { MessageListItem } from '../../vaadin-message-list.js'; +import type { MessageListItem, MessageListItemAttachment } from '../../vaadin-message-list.js'; const assertType = (value: TExpected) => value; @@ -24,6 +24,13 @@ assertType(item.userImg); assertType(item.userColorIndex); assertType(item.theme); assertType(item.className); +assertType(item.attachments); + +// Attachment properties +const attachment: MessageListItemAttachment = item.attachments ? item.attachments[0] : {}; +assertType(attachment.name); +assertType(attachment.url); +assertType(attachment.type); // Mixins assertType(list);