Skip to content

Commit b9f17da

Browse files
committed
Merge branch 'dmdimitrov/chat-ai-component' of https://github.com/IgniteUI/igniteui-webcomponents into dmdimitrov/chat-ai-component
2 parents 10d31be + 7c0882a commit b9f17da

File tree

17 files changed

+460
-412
lines changed

17 files changed

+460
-412
lines changed

src/components/chat/chat-input.ts

Lines changed: 55 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,20 @@ import type { ChatState } from './chat-state.js';
1818
import { styles } from './themes/input.base.css.js';
1919
import { all } from './themes/input.js';
2020
import { styles as shared } from './themes/shared/input/input.common.css.js';
21-
import type { ChatTemplateRenderer, IgcMessageAttachment } from './types.js';
21+
import type {
22+
ChatRendererContext,
23+
ChatTemplateRenderer,
24+
IgcMessageAttachment,
25+
InputRendererContext,
26+
} from './types.js';
2227
import { getChatAcceptedFiles, getIconName } from './utils.js';
2328

2429
type DefaultInputRenderers = {
25-
input: ChatTemplateRenderer<string>;
26-
inputActions: ChatTemplateRenderer<void>;
27-
inputAttachments: ChatTemplateRenderer<IgcMessageAttachment[]>;
28-
fileUploadButton: ChatTemplateRenderer<void>;
29-
sendButton: ChatTemplateRenderer<void>;
30+
input: ChatTemplateRenderer<InputRendererContext>;
31+
inputActions: ChatTemplateRenderer<ChatRendererContext>;
32+
inputAttachments: ChatTemplateRenderer<InputRendererContext>;
33+
fileUploadButton: ChatTemplateRenderer<ChatRendererContext>;
34+
sendButton: ChatTemplateRenderer<ChatRendererContext>;
3035
};
3136
/**
3237
* A web component that provides the input area for the `igc-chat` interface.
@@ -72,6 +77,14 @@ export default class IgcChatInputComponent extends LitElement {
7277
);
7378
}
7479

80+
private readonly _defaults: Readonly<DefaultInputRenderers> = Object.freeze({
81+
fileUploadButton: () => this._renderFileUploadButton(),
82+
input: () => this._renderTextArea(),
83+
inputActions: () => this._renderActionsArea(),
84+
inputAttachments: (ctx) => this._renderAttachmentsArea(ctx.attachments),
85+
sendButton: () => this._renderSendButton(),
86+
});
87+
7588
@consume({ context: chatContext, subscribe: true })
7689
private readonly _state!: ChatState;
7790

@@ -91,14 +104,6 @@ export default class IgcChatInputComponent extends LitElement {
91104
return this._state.acceptedFileTypes;
92105
}
93106

94-
private readonly _defaults: Readonly<DefaultInputRenderers> = Object.freeze({
95-
input: () => this._renderTextArea(),
96-
inputActions: () => this.renderActionsArea(),
97-
inputAttachments: (ctx) => this.renderAttachmentsArea(ctx.param),
98-
fileUploadButton: () => this._renderFileUploadButton(),
99-
sendButton: () => this._renderSendButton(),
100-
});
101-
102107
constructor() {
103108
super();
104109
addThemingController(this, all);
@@ -109,11 +114,12 @@ export default class IgcChatInputComponent extends LitElement {
109114
this._state.textArea = this._textInputElement;
110115
}
111116

112-
private _getRenderer(
113-
name: keyof DefaultInputRenderers
114-
): ChatTemplateRenderer<any> {
117+
private _getRenderer<U extends keyof DefaultInputRenderers>(
118+
name: U
119+
): DefaultInputRenderers[U] {
115120
return this._state.options?.renderers
116-
? (this._state.options.renderers[name] ?? this._defaults[name])
121+
? ((this._state.options.renderers[name] ??
122+
this._defaults[name]) as DefaultInputRenderers[U])
117123
: this._defaults[name];
118124
}
119125

@@ -207,7 +213,7 @@ export default class IgcChatInputComponent extends LitElement {
207213
* Renders the list of input attachments as chips.
208214
* @returns TemplateResult containing the attachments area
209215
*/
210-
private renderAttachmentsArea(attachments: IgcMessageAttachment[]) {
216+
private _renderAttachmentsArea(attachments: IgcMessageAttachment[]) {
211217
return html`${attachments?.map(
212218
(attachment, index) => html`
213219
<div part="attachment-wrapper" role="listitem">
@@ -235,6 +241,7 @@ export default class IgcChatInputComponent extends LitElement {
235241
return html`
236242
<igc-textarea
237243
part="text-input"
244+
aria-label="Chat text input"
238245
placeholder=${ifDefined(this._state.options?.inputPlaceholder)}
239246
resize="auto"
240247
rows="1"
@@ -262,6 +269,7 @@ export default class IgcChatInputComponent extends LitElement {
262269
: html`
263270
<label for="input_attachments" part="upload-button">
264271
<igc-icon-button
272+
aria-label="Attach files"
265273
variant="flat"
266274
name="attach_file"
267275
@click=${this._handleFileInputClick}
@@ -270,6 +278,7 @@ export default class IgcChatInputComponent extends LitElement {
270278
type="file"
271279
id="input_attachments"
272280
name="input_attachments"
281+
aria-label="Upload button"
273282
multiple
274283
accept=${bindIf(accepted, accepted)}
275284
@change=${this._handleFileUpload}
@@ -300,23 +309,28 @@ export default class IgcChatInputComponent extends LitElement {
300309
`;
301310
}
302311

303-
private renderActionsArea() {
304-
return html` ${this._getRenderer('fileUploadButton')({
305-
param: undefined,
306-
defaults: this._defaults,
307-
options: this._state.options,
308-
})}
309-
${this._getRenderer('sendButton')({
310-
param: undefined,
312+
private _renderActionsArea() {
313+
const ctx: ChatRendererContext = {
311314
defaults: this._defaults,
312-
options: this._state.options,
313-
})}`;
315+
instance: this._state.host,
316+
};
317+
318+
return html`
319+
${this._getRenderer('fileUploadButton')(ctx)}
320+
${this._getRenderer('sendButton')(ctx)}
321+
`;
314322
}
315323

316324
protected override render() {
317-
const partialCtx = {
325+
const ctx: ChatRendererContext = {
318326
defaults: this._defaults,
319-
options: this._state.options,
327+
instance: this._state.host,
328+
};
329+
330+
const inputCtx: InputRendererContext = {
331+
...ctx,
332+
attachments: this._state.inputAttachments,
333+
value: this._state.inputValue,
320334
};
321335

322336
return html`
@@ -327,32 +341,20 @@ export default class IgcChatInputComponent extends LitElement {
327341
@dragleave=${this._handleDragLeave}
328342
@drop=${this._handleDrop}
329343
>
330-
${this._state.inputAttachments &&
331-
this._state.inputAttachments.length > 0
332-
? html` <div part="attachments" role="list" aria-label="Attachments">
333-
${until(
334-
this._getRenderer('inputAttachments')({
335-
...partialCtx,
336-
param: this._state.inputAttachments,
337-
})
338-
)}
339-
</div>`
344+
${this._state.hasInputAttachments
345+
? html`
346+
<div part="attachments" role="list" aria-label="Attachments">
347+
${until(this._getRenderer('inputAttachments')(inputCtx))}
348+
</div>
349+
`
340350
: nothing}
351+
341352
<div part="input-wrapper">
342-
${until(
343-
this._getRenderer('input')({
344-
...partialCtx,
345-
param: this._state.inputValue,
346-
})
347-
)}
353+
${until(this._getRenderer('input')(inputCtx))}
348354
</div>
355+
349356
<div part="buttons-container">
350-
${until(
351-
this._getRenderer('inputActions')({
352-
...partialCtx,
353-
param: undefined,
354-
})
355-
)}
357+
${until(this._getRenderer('inputActions')(ctx))}
356358
</div>
357359
</div>
358360
`;

src/components/chat/chat-message.ts

Lines changed: 40 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { consume } from '@lit/context';
22
import { html, LitElement, nothing } from 'lit';
33
import { property } from 'lit/decorators.js';
4+
import { cache } from 'lit/directives/cache.js';
45
import { until } from 'lit/directives/until.js';
56
import { addThemingController } from '../../theming/theming-controller.js';
6-
import IgcAvatarComponent from '../avatar/avatar.js';
7+
import IgcIconButtonComponent from '../button/icon-button.js';
78
import { chatContext } from '../common/context.js';
89
import { registerComponent } from '../common/definitions/register.js';
910
import { partMap } from '../common/part-map.js';
@@ -14,7 +15,11 @@ import IgcMessageAttachmentsComponent from './message-attachments.js';
1415
import { styles } from './themes/message.base.css.js';
1516
import { all } from './themes/message.js';
1617
import { styles as shared } from './themes/shared/chat-message/chat-message.common.css.js';
17-
import type { ChatTemplateRenderer, IgcMessage } from './types.js';
18+
import type {
19+
ChatTemplateRenderer,
20+
IgcMessage,
21+
MessageRendererContext,
22+
} from './types.js';
1823
import { chatMessageAdoptPageStyles, showChatActionsTooltip } from './utils.js';
1924

2025
const LIKE_INACTIVE = 'thumb_up_inactive';
@@ -23,11 +28,11 @@ const DISLIKE_INACTIVE = 'thumb_down_inactive';
2328
const DISLIKE_ACTIVE = 'thumb_down_active';
2429

2530
type DefaultMessageRenderers = {
26-
message: ChatTemplateRenderer<IgcMessage>;
27-
messageHeader: ChatTemplateRenderer<IgcMessage>;
28-
messageContent: ChatTemplateRenderer<IgcMessage>;
29-
messageAttachments: ChatTemplateRenderer<IgcMessage>;
30-
messageActions: ChatTemplateRenderer<IgcMessage>;
31+
message: ChatTemplateRenderer<MessageRendererContext>;
32+
messageHeader: ChatTemplateRenderer<MessageRendererContext>;
33+
messageContent: ChatTemplateRenderer<MessageRendererContext>;
34+
messageAttachments: ChatTemplateRenderer<MessageRendererContext>;
35+
messageActions: ChatTemplateRenderer<MessageRendererContext>;
3136
};
3237

3338
/**
@@ -57,17 +62,11 @@ export default class IgcChatMessageComponent extends LitElement {
5762
registerComponent(
5863
IgcChatMessageComponent,
5964
IgcMessageAttachmentsComponent,
60-
IgcAvatarComponent,
65+
IgcIconButtonComponent,
6166
IgcTooltipComponent
6267
);
6368
}
6469

65-
/**
66-
* Injected chat state context. Provides message data, user info, and options.
67-
*/
68-
@consume({ context: chatContext, subscribe: true })
69-
private readonly _state!: ChatState;
70-
7170
private readonly _defaults = Object.freeze<DefaultMessageRenderers>({
7271
message: () => this._renderMessage(),
7372
messageHeader: () => this._renderHeader(),
@@ -76,6 +75,9 @@ export default class IgcChatMessageComponent extends LitElement {
7675
messageActions: () => this._renderActions(),
7776
});
7877

78+
@consume({ context: chatContext, subscribe: true })
79+
private readonly _state!: ChatState;
80+
7981
/**
8082
* The chat message to render.
8183
*/
@@ -99,7 +101,7 @@ export default class IgcChatMessageComponent extends LitElement {
99101

100102
private _handleMessageActionClick(event: PointerEvent): void {
101103
const targetButton = event.target as HTMLElement;
102-
const button = targetButton.closest('igc-icon-button');
104+
const button = targetButton.closest(IgcIconButtonComponent.tagName);
103105
if (!button) return;
104106

105107
let reaction = button.getAttribute('name');
@@ -197,56 +199,43 @@ export default class IgcChatMessageComponent extends LitElement {
197199
`;
198200
}
199201

200-
/**
201-
* Default message template used when no custom template is provided.
202-
* Renders the message text, sanitized for security.
203-
* @param message The chat message to render
204-
* @returns TemplateResult containing the rendered message
205-
*/
206202
private _renderMessage() {
207203
return this.message
208204
? html`${this._renderHeader()}${this._renderContent()}${this._renderAttachments()}${this._renderActions()}`
209205
: nothing;
210206
}
211207

212-
/**
213-
* Renders the chat message template.
214-
* - Applies 'sent' CSS class if the message sender matches current user.
215-
* - Uses markdown rendering if configured.
216-
* - Renders attachments and custom templates if provided.
217-
*/
218208
protected override render() {
219-
if (!this.message) {
220-
return nothing;
221-
}
209+
const ctx: MessageRendererContext = {
210+
message: this.message!,
211+
defaults: this._defaults,
212+
instance: this._state.host,
213+
};
222214

223215
const parts = {
224216
'message-container': true,
225217
sent: this._state.isCurrentUserMessage(this.message),
226218
};
227219

228-
const options = this._state.options;
229-
const ctx = {
230-
param: this.message,
231-
defaults: this._defaults,
232-
options,
233-
};
234-
235-
if (options?.renderers?.message) {
236-
return html`
237-
<div part=${partMap(parts)}>
238-
${until(options.renderers.message(ctx))}
239-
</div>
240-
`;
241-
}
242-
243220
return html`
244-
<div part=${partMap(parts)}>
245-
${until(this._getRenderer('messageHeader')(ctx))}
246-
${until(this._getRenderer('messageContent')(ctx))}
247-
${until(this._getRenderer('messageAttachments')(ctx))}
248-
${until(this._getRenderer('messageActions')(ctx))}
249-
</div>
221+
${cache(
222+
this.message
223+
? html`
224+
<div part=${partMap(parts)}>
225+
${cache(
226+
this._state.options?.renderers?.message
227+
? html`${until(this._getRenderer('message')(ctx))}`
228+
: html`
229+
${until(this._getRenderer('messageHeader')(ctx))}
230+
${until(this._getRenderer('messageContent')(ctx))}
231+
${until(this._getRenderer('messageAttachments')(ctx))}
232+
${until(this._getRenderer('messageActions')(ctx))}
233+
`
234+
)}
235+
</div>
236+
`
237+
: nothing
238+
)}
250239
`;
251240
}
252241
}

0 commit comments

Comments
 (0)