Skip to content

Commit e002594

Browse files
committed
feat(chat): add message reactions buttons for responses
1 parent c5de30e commit e002594

File tree

5 files changed

+355
-228
lines changed

5 files changed

+355
-228
lines changed

src/components/chat/chat-message.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,28 @@ import { addThemingController } from '../../theming/theming-controller.js';
66
import IgcAvatarComponent from '../avatar/avatar.js';
77
import { chatContext } from '../common/context.js';
88
import { registerComponent } from '../common/definitions/register.js';
9+
import { registerIconFromText } from '../icon/icon.registry.js';
910
import type { ChatState } from './chat-state.js';
1011
import { renderMarkdown } from './markdown-util.js';
1112
import IgcMessageAttachmentsComponent from './message-attachments.js';
1213
import { styles } from './themes/message.base.css.js';
1314
import { all } from './themes/message.js';
1415
import { styles as shared } from './themes/shared/chat-message/chat-message.common.css.js';
15-
import type { IgcMessage } from './types.js';
16+
import {
17+
copyIcon,
18+
type IgcMessage,
19+
regenerateIcon,
20+
thumbDownIcon,
21+
thumbUpIcon,
22+
} from './types.js';
1623

1724
/**
1825
* A chat message component for displaying individual messages in `<igc-chat>`.
1926
*
2027
* @element igc-chat-message
2128
*
29+
* @fires igcMessageReact - Fired when a message is reacted to.
30+
*
2231
* This component renders a single chat message including:
2332
* - Message text (sanitized)
2433
* - Attachments (if any)
@@ -76,6 +85,51 @@ export default class IgcChatMessageComponent extends LitElement {
7685
constructor() {
7786
super();
7887
addThemingController(this, all);
88+
registerIconFromText('copy', copyIcon, 'material');
89+
registerIconFromText('thumb_up', thumbUpIcon, 'material');
90+
registerIconFromText('thumb_down', thumbDownIcon, 'material');
91+
registerIconFromText('regenerate', regenerateIcon, 'material');
92+
}
93+
94+
private get defaultMessageActionsTemplate() {
95+
const isLastMessage = this.message === this._chatState?.messages.at(-1);
96+
return this.message?.sender !== 'user' &&
97+
this.message?.text.trim() &&
98+
(!isLastMessage || !this._chatState?.options?.isTyping)
99+
? html`<div>
100+
<igc-icon-button
101+
name="copy"
102+
collection="material"
103+
variant="flat"
104+
@click=${this.handleMessageActionClick}
105+
></igc-icon-button>
106+
<igc-icon-button
107+
name="thumb_up"
108+
collection="material"
109+
variant="flat"
110+
@click=${this.handleMessageActionClick}
111+
></igc-icon-button>
112+
<igc-icon-button
113+
name="thumb_down"
114+
variant="flat"
115+
collection="material"
116+
@click=${this.handleMessageActionClick}
117+
></igc-icon-button>
118+
<igc-icon-button
119+
name="regenerate"
120+
variant="flat"
121+
collection="material"
122+
@click=${this.handleMessageActionClick}
123+
></igc-icon-button>
124+
</div>`
125+
: nothing;
126+
}
127+
128+
private handleMessageActionClick(event: MouseEvent): void {
129+
const reaction = (event.target as HTMLElement).getAttribute('name');
130+
this._chatState?.emitEvent('igcMessageReact', {
131+
detail: { message: this.message, reaction: reaction },
132+
});
79133
}
80134

81135
/**
@@ -109,7 +163,7 @@ export default class IgcChatMessageComponent extends LitElement {
109163
? this._chatState.options.templates.messageActionsTemplate(
110164
this.message
111165
)
112-
: nothing}
166+
: this.defaultMessageActionsTemplate}
113167
</div>
114168
</div>
115169
`;

src/components/chat/chat.spec.ts

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,37 @@ describe('Chat', () => {
163163

164164
const GAP = 24; // Default gap between elements in the chat container
165165

166+
const messageReactions = `<div>
167+
<igc-icon-button
168+
collection="material"
169+
name="copy"
170+
type="button"
171+
variant="flat"
172+
>
173+
</igc-icon-button>
174+
<igc-icon-button
175+
collection="material"
176+
name="thumb_up"
177+
type="button"
178+
variant="flat"
179+
>
180+
</igc-icon-button>
181+
<igc-icon-button
182+
collection="material"
183+
name="thumb_down"
184+
type="button"
185+
variant="flat"
186+
>
187+
</igc-icon-button>
188+
<igc-icon-button
189+
collection="material"
190+
name="regenerate"
191+
type="button"
192+
variant="flat"
193+
>
194+
</igc-icon-button>
195+
</div>`;
196+
166197
let chat: IgcChatComponent;
167198
let clock: SinonFakeTimers;
168199

@@ -275,14 +306,15 @@ describe('Chat', () => {
275306

276307
expect(chat.messages.length).to.equal(4);
277308

278-
expect(
279-
messageContainer?.querySelectorAll('igc-chat-message')[0]
280-
).shadowDom.to.equal(
309+
const firstMessage =
310+
messageContainer?.querySelectorAll('igc-chat-message')[0];
311+
expect(firstMessage).shadowDom.to.equal(
281312
`<div part="message-container ">
282313
<div part="bubble">
283314
<div>
284315
<p>Hello! How can I help you today?</p>
285316
</div>
317+
${firstMessage?.message?.sender !== 'user' ? messageReactions : ''}
286318
</div>
287319
</div>`
288320
);
@@ -319,14 +351,15 @@ describe('Chat', () => {
319351

320352
expect(chat.messages.length).to.equal(1);
321353

322-
expect(
323-
messageContainer?.querySelectorAll('igc-chat-message')[0]
324-
).shadowDom.to.equal(
354+
const firstMessage =
355+
messageContainer?.querySelectorAll('igc-chat-message')[0];
356+
expect(firstMessage).shadowDom.to.equal(
325357
`<div part="message-container ">
326358
<div part="bubble">
327359
<div>
328360
<p>Hello!</p>
329361
</div>
362+
${firstMessage?.message?.sender !== 'user' ? messageReactions : ''}
330363
</div>
331364
</div>`
332365
);
@@ -621,6 +654,7 @@ describe('Chat', () => {
621654
</div>
622655
<igc-message-attachments>
623656
</igc-message-attachments>
657+
${chat.messages[index].sender !== 'user' ? messageReactions : ''}
624658
</div>`
625659
);
626660

@@ -948,6 +982,7 @@ describe('Chat', () => {
948982
</div>
949983
<igc-message-attachments>
950984
</igc-message-attachments>
985+
${chat.messages[index].sender !== 'user' ? messageReactions : ''}
951986
</div>`
952987
);
953988
});
@@ -1432,6 +1467,27 @@ describe('Chat', () => {
14321467
}
14331468
});
14341469

1470+
it('emits igcMessageReact', async () => {
1471+
const eventSpy = spy(chat, 'emitEvent');
1472+
chat.messages = [messages[0]];
1473+
await elementUpdated(chat);
1474+
await aTimeout(500);
1475+
1476+
const messageElement = chat.shadowRoot
1477+
?.querySelector('igc-chat-message-list')
1478+
?.shadowRoot?.querySelector(`div[part='message-list'`)
1479+
?.querySelector('igc-chat-message');
1480+
1481+
const thumbUpIcon = messageElement?.shadowRoot?.querySelector(
1482+
'igc-icon-button[name="thumb_up"]'
1483+
) as HTMLElement;
1484+
1485+
simulateClick(thumbUpIcon);
1486+
expect(eventSpy).calledWith('igcMessageReact', {
1487+
detail: { message: messages[0], reaction: 'thumb_up' },
1488+
});
1489+
});
1490+
14351491
it('can cancel `igcMessageCreated` event', async () => {
14361492
const inputArea = chat.shadowRoot?.querySelector('igc-chat-input');
14371493
const sendButton = inputArea?.shadowRoot?.querySelector(

src/components/chat/chat.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ export interface IgcChatComponentEventMap {
3434
*/
3535
igcMessageCreated: CustomEvent<IgcMessage>;
3636

37+
/**
38+
* Dispatched when a new chat message is created (sent).
39+
*
40+
* @event igcMessageCreated
41+
* @type {CustomEvent<IgcMessage>}
42+
* @detail The message that was reacted to and the reaction.
43+
*/
44+
igcMessageReact: CustomEvent<{ message: IgcMessage; reaction: string }>;
45+
3746
/**
3847
* Dispatched when a chat message attachment is clicked.
3948
*

src/components/chat/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,11 @@ export const fileImageIcon =
231231
'<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><path d="M6.75 1.5L5.3775 3H3C2.175 3 1.5 3.675 1.5 4.5V13.5C1.5 14.325 2.175 15 3 15H15C15.825 15 16.5 14.325 16.5 13.5V4.5C16.5 3.675 15.825 3 15 3H12.6225L11.25 1.5H6.75ZM9 12.75C6.93 12.75 5.25 11.07 5.25 9C5.25 6.93 6.93 5.25 9 5.25C11.07 5.25 12.75 6.93 12.75 9C12.75 11.07 11.07 12.75 9 12.75Z"/></svg>';
232232
export const fileDocumentIcon =
233233
'<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><path d="M4.5 1.5C3.675 1.5 3.0075 2.175 3.0075 3L3 15C3 15.825 3.6675 16.5 4.4925 16.5H13.5C14.325 16.5 15 15.825 15 15V6L10.5 1.5H4.5ZM9.75 6.75V2.625L13.875 6.75H9.75Z"/></svg>';
234+
export const copyIcon =
235+
'<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M360-240q-33 0-56.5-23.5T280-320v-480q0-33 23.5-56.5T360-880h360q33 0 56.5 23.5T800-800v480q0 33-23.5 56.5T720-240H360Zm0-80h360v-480H360v480ZM200-80q-33 0-56.5-23.5T120-160v-560h80v560h440v80H200Zm160-240v-480 480Z"/></svg>';
236+
export const thumbUpIcon =
237+
'<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M720-120H280v-520l280-280 50 50q7 7 11.5 19t4.5 23v14l-44 174h258q32 0 56 24t24 56v80q0 7-2 15t-4 15L794-168q-9 20-30 34t-44 14Zm-360-80h360l120-280v-80H480l54-220-174 174v406Zm0-406v406-406Zm-80-34v80H160v360h120v80H80v-520h200Z"/></svg>';
238+
export const thumbDownIcon =
239+
'<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M240-840h440v520L400-40l-50-50q-7-7-11.5-19t-4.5-23v-14l44-174H120q-32 0-56-24t-24-56v-80q0-7 2-15t4-15l120-282q9-20 30-34t44-14Zm360 80H240L120-480v80h360l-54 220 174-174v-406Zm0 406v-406 406Zm80 34v-80h120v-360H680v-80h200v520H680Z"/></svg>';
240+
export const regenerateIcon =
241+
'<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z"/></svg>';

0 commit comments

Comments
 (0)