Skip to content
Draft
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
27 changes: 25 additions & 2 deletions dev/messages-ai-chat.html
Original file line number Diff line number Diff line change
Expand Up @@ -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.",

Check failure on line 128 in dev/messages-ai-chat.html

View workflow job for this annotation

GitHub Actions / Lint

Replace `"Can·you·help·me·analyze·these·Q3·financial·documents?·I·need·a·summary·of·the·key·findings·and·any·concerns·you·spot."` with `'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
Expand Down
18 changes: 17 additions & 1 deletion packages/message-list/src/vaadin-message-list-mixin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,6 +21,7 @@ export interface MessageListItem {
userColorIndex?: number;
theme?: string;
className?: string;
attachments?: MessageListItemAttachment[];
}

export declare function MessageListMixin<T extends Constructor<HTMLElement>>(
Expand All @@ -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;

Expand Down
67 changes: 65 additions & 2 deletions packages/message-list/src/vaadin-message-list-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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`<vaadin-markdown .content=${item.text}></vaadin-markdown>`
: item.text}<vaadin-avatar slot="avatar"></vaadin-avatar
></vaadin-message>
Expand All @@ -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`
<div slot="attachments" class="vaadin-message-attachments">
${attachments.map((attachment) => this.__renderAttachment(attachment))}
</div>
`;
}

/**
* 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`
<a
class="vaadin-message-attachment vaadin-message-attachment-image"
href="${attachment.url}"
target="_blank"
rel="noopener noreferrer"
>
<img src="${attachment.url}" alt="${attachment.name || ''}" />
</a>
`;
}

return html`
<a
class="vaadin-message-attachment vaadin-message-attachment-file"
href="${attachment.url}"
target="_blank"
rel="noopener noreferrer"
>
<span class="vaadin-message-attachment-icon"></span>
<span class="vaadin-message-attachment-name">${attachment.name || attachment.url}</span>
</a>
`;
}

/** @private */
_scrollToLastMessage() {
if (this.items.length > 0) {
Expand Down
2 changes: 1 addition & 1 deletion packages/message-list/src/vaadin-message-list.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
* `<vaadin-message-list>` is a Web Component for showing an ordered list of messages. The messages are rendered as <vaadin-message>
Expand Down
39 changes: 39 additions & 0 deletions packages/message-list/src/vaadin-message-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></svg>');
mask-size: contain;
mask-repeat: no-repeat;
flex-shrink: 0;
}

${tag} .vaadin-message-attachment-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`,
];
}
Expand Down
7 changes: 7 additions & 0 deletions packages/message-list/src/vaadin-message.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.</vaadin-message>
* ```
*
* ### 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:
Expand Down Expand Up @@ -70,6 +76,7 @@ class Message extends MessageMixin(ElementMixin(ThemableMixin(PolylitMixin(LumoI
<span part="name">${this.userName}</span>
<span part="time">${this.time}</span>
</div>
<slot name="attachments"></slot>
<div part="message"><slot></slot></div>
</div>
`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ snapshots["vaadin-message default"] =
<span part="time">
</span>
</div>
<slot name="attachments">
</slot>
<div part="message">
<slot>
</slot>
Expand All @@ -30,6 +32,8 @@ snapshots["vaadin-message userName"] =
<span part="time">
</span>
</div>
<slot name="attachments">
</slot>
<div part="message">
<slot>
</slot>
Expand All @@ -49,6 +53,8 @@ snapshots["vaadin-message time"] =
long ago
</span>
</div>
<slot name="attachments">
</slot>
<div part="message">
<slot>
</slot>
Expand Down
9 changes: 8 additions & 1 deletion packages/message-list/test/typings/message-list.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <TExpected>(value: TExpected) => value;

Expand All @@ -24,6 +24,13 @@ assertType<string | undefined>(item.userImg);
assertType<number | undefined>(item.userColorIndex);
assertType<string | undefined>(item.theme);
assertType<string | undefined>(item.className);
assertType<MessageListItemAttachment[] | undefined>(item.attachments);

// Attachment properties
const attachment: MessageListItemAttachment = item.attachments ? item.attachments[0] : {};
assertType<string | undefined>(attachment.name);
assertType<string | undefined>(attachment.url);
assertType<string | undefined>(attachment.type);

// Mixins
assertType<ElementMixinClass>(list);
Expand Down
Loading