Skip to content

Commit 9aa705f

Browse files
Adding an option for positioning the suggestions (#1803)
* feat(*): Adding an option to control suggestions position * chore(*): Adding a new test for suggestions * test(*): Adding tests for suggestions positioning * fix(*): Fixed the failing suggestions test * chore(*): Remove supabase stuff * fix(*): Removed 'above-input' option for suggestions * chore(*): Removed irrelevant content * chore(*): Deleted leftover 'above-input'. * chore(*): remove duplicated tests --------- Co-authored-by: igdmdimitrov <[email protected]>
1 parent 62a8879 commit 9aa705f

File tree

6 files changed

+111
-45
lines changed

6 files changed

+111
-45
lines changed

src/components/chat/chat-input.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,6 @@ export default class IgcChatInputComponent extends LitElement {
339339
this._chatState?.inputAttachments.length > 0
340340
? this.renderAttachmentsArea()
341341
: nothing}
342-
343342
<div part="input-wrapper">
344343
${this._chatState?.options?.templates?.textInputTemplate
345344
? this._chatState.options.templates.textInputTemplate(

src/components/chat/chat-state.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export class ChatState {
3636
wildcardTypes: Set<string>;
3737
} | null = null;
3838

39+
private _suggestionsPosition: 'below-input' | 'below-messages' =
40+
'below-messages';
3941
//#endregion
4042

4143
//#region Public properties
@@ -87,6 +89,12 @@ export class ChatState {
8789
return this._options?.currentUserId ?? 'user';
8890
}
8991

92+
/**
93+
* Gets the current suggestionsPosition from options or returns the default value 'below-messages'.
94+
*/
95+
public get suggestionsPosition(): string {
96+
return this._options?.suggestionsPosition ?? this._suggestionsPosition;
97+
}
9098
/**
9199
* Gets the text area component.
92100
*/

src/components/chat/chat.spec.ts

Lines changed: 71 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ describe('Chat', () => {
160160
new File(['image data'], 'image.png', { type: 'image/png' }),
161161
];
162162

163+
const GAP = 40; // Default gap between elements in the chat container
164+
163165
let chat: IgcChatComponent;
164166
let clock: SinonFakeTimers;
165167

@@ -181,7 +183,7 @@ describe('Chat', () => {
181183

182184
it('is rendered correctly', () => {
183185
expect(chat).dom.to.equal(
184-
`<igc-chat>
186+
`<igc-chat>
185187
</igc-chat>`
186188
);
187189

@@ -414,39 +416,6 @@ describe('Chat', () => {
414416
expect(textArea?.placeholder).to.equal('Type message here...');
415417
});
416418

417-
it('should render suggestions', async () => {
418-
chat.options = {
419-
suggestions: ['Suggestion 1', 'Suggestion 2'],
420-
};
421-
await elementUpdated(chat);
422-
423-
const suggestionsContainer = chat.shadowRoot?.querySelector(
424-
'div[part="suggestions-container"]'
425-
);
426-
427-
expect(suggestionsContainer).dom.to.equal(
428-
`<div aria-label="Suggestions" part="suggestions-container" role="list">
429-
<slot name="suggestions-header" part="suggestions-header"> </slot>
430-
<slot name="suggestions" part="suggestions">
431-
<slot name="suggestion" part="suggestion" role="listitem">
432-
<igc-chip>
433-
<span>
434-
Suggestion 1
435-
</span>
436-
</igc-chip>
437-
</slot>
438-
<slot name="suggestion" part="suggestion" role="listitem">
439-
<igc-chip>
440-
<span>
441-
Suggestion 2
442-
</span>
443-
</igc-chip>
444-
</slot>
445-
</slot>
446-
</div>`
447-
);
448-
});
449-
450419
it('should enable/disable the send button properly', async () => {
451420
const inputArea = chat.shadowRoot?.querySelector('igc-chat-input');
452421
const sendButton =
@@ -683,7 +652,15 @@ describe('Chat', () => {
683652
});
684653
});
685654

686-
it('should render suggestions', async () => {
655+
it('should not render container if suggestions are not provided', async () => {
656+
const suggestionsContainer = chat.shadowRoot?.querySelector(
657+
`div[part='suggestions-container']`
658+
);
659+
660+
expect(suggestionsContainer).to.be.null;
661+
});
662+
663+
it('should render suggestions if provided', async () => {
687664
chat.options = {
688665
suggestions: ['Suggestion 1', 'Suggestion 2'],
689666
};
@@ -712,10 +689,69 @@ describe('Chat', () => {
712689
</igc-chip>
713690
</slot>
714691
</slot>
692+
<slot name="suggestions-actions" part="suggestions-actions"> </slot>
715693
</div>`
716694
);
717695
});
718696

697+
it('should render suggestions below empty state by default', async () => {
698+
chat.options = {
699+
suggestions: ['Suggestion 1', 'Suggestion 2'],
700+
};
701+
await elementUpdated(chat);
702+
const suggestionsContainer = chat.shadowRoot?.querySelector(
703+
`div[part='suggestions-container']`
704+
);
705+
706+
expect(suggestionsContainer?.previousElementSibling?.part[0]).to.equal(
707+
'empty-state'
708+
);
709+
});
710+
711+
it('should render suggestions below messages by default', async () => {
712+
chat.options = {
713+
suggestions: ['Suggestion 1', 'Suggestion 2'],
714+
};
715+
chat.messages.push({
716+
id: '5',
717+
text: 'New message',
718+
sender: 'user',
719+
timestamp: new Date(),
720+
});
721+
await elementUpdated(chat);
722+
723+
const suggestionsContainer = chat.shadowRoot?.querySelector(
724+
`div[part='suggestions-container']`
725+
)!;
726+
727+
const messageList = chat.shadowRoot?.querySelector(
728+
'igc-chat-message-list'
729+
)!;
730+
731+
const diff =
732+
suggestionsContainer.getBoundingClientRect().top -
733+
messageList.getBoundingClientRect().bottom;
734+
expect(diff).to.equal(GAP);
735+
});
736+
737+
it("should render suggestions below input area when position is 'below-input'", async () => {
738+
chat.options = {
739+
suggestions: ['Suggestion 1', 'Suggestion 2'],
740+
suggestionsPosition: 'below-input',
741+
};
742+
await elementUpdated(chat);
743+
744+
const suggestionsContainer = chat.shadowRoot?.querySelector(
745+
`div[part='suggestions-container']`
746+
)!;
747+
748+
const inputArea = chat.shadowRoot?.querySelector('igc-chat-input')!;
749+
const diff =
750+
suggestionsContainer.getBoundingClientRect().top -
751+
inputArea.getBoundingClientRect().bottom;
752+
expect(diff).to.equal(GAP);
753+
});
754+
719755
it('should render composing indicator if `isComposing` is true', async () => {
720756
chat.messages = [messages[0]];
721757
chat.options = {

src/components/chat/chat.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ export interface IgcChatComponentEventMap {
115115
* @slot prefix - Slot for injecting content (e.g., avatar or icon) before the chat title.
116116
* @slot title - Slot for overriding the chat title content.
117117
* @slot actions - Slot for injecting header actions (e.g., buttons, menus).
118+
* @slot suggestions-header - Slot for rendering a custom header for the suggestions list.
118119
* @slot suggestions - Slot for rendering a custom list of quick reply suggestions.
120+
* @slot suggestions-actions - Slot for rendering additional actions.
119121
* @slot suggestion - Slot for rendering a single suggestion item.
120122
* @slot empty-state - Slot shown when there are no messages.
121123
*
@@ -278,6 +280,7 @@ export default class IgcChatComponent extends EventEmitterMixin<
278280
`
279281
)}
280282
</slot>
283+
<slot name="suggestions-actions" part="suggestions-actions"></slot>
281284
</div>`;
282285
}
283286

@@ -297,6 +300,9 @@ export default class IgcChatComponent extends EventEmitterMixin<
297300
}
298301

299302
protected override render() {
303+
const hasSuggestions =
304+
this._chatState.options?.suggestions &&
305+
this._chatState.options.suggestions.length > 0;
300306
return html`
301307
<div part="chat-container">
302308
${this._hasContent || this._chatState.options?.headerText
@@ -307,11 +313,15 @@ export default class IgcChatComponent extends EventEmitterMixin<
307313
<slot name="empty-state"> </slot>
308314
</div>`
309315
: html`<igc-chat-message-list> </igc-chat-message-list>`}
310-
${this._chatState.options?.suggestions &&
311-
this._chatState.options?.suggestions?.length > 0
316+
${hasSuggestions &&
317+
this._chatState.suggestionsPosition === 'below-messages'
318+
? this.renderSuggestions()
319+
: nothing}
320+
<igc-chat-input> </igc-chat-input>
321+
${hasSuggestions &&
322+
this._chatState.suggestionsPosition === 'below-input'
312323
? this.renderSuggestions()
313324
: nothing}
314-
<igc-chat-input></igc-chat-input>
315325
</div>
316326
`;
317327
}

src/components/chat/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,16 @@ export type IgcChatOptions = {
132132
* Suggested text snippets or quick replies that can be shown as user-selectable options.
133133
*/
134134
suggestions?: string[];
135+
/**
136+
* Controls the position of the chat suggestions within the component layout.
137+
*
138+
* - `"below-input"`: Renders suggestions below the chat input area.
139+
* - `"below-messages"`: Renders suggestions below the chat messages area.
140+
*
141+
* Default is `"below-messages"`.
142+
*/
143+
suggestionsPosition?: 'below-input' | 'below-messages';
144+
135145
/**
136146
* A set of template override functions used to customize rendering of messages, attachments, etc.
137147
*/

stories/chat.stories.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite';
22
import { html } from 'lit';
33

44
import { GoogleGenAI, Modality } from '@google/genai';
5-
import { createClient } from '@supabase/supabase-js';
5+
// import { createClient } from '@supabase/supabase-js';
66
import {
77
IgcChatComponent,
88
defineComponents,
@@ -13,12 +13,12 @@ import type {
1313
IgcMessageAttachment,
1414
} from '../src/components/chat/types.js';
1515

16-
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
17-
const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
18-
const supabase = createClient(supabaseUrl, supabaseKey);
16+
// const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
17+
// const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
18+
const supabase = ''; // createClient(supabaseUrl, supabaseKey);
1919

2020
const ai = new GoogleGenAI({
21-
apiKey: googleGenAIKey,
21+
apiKey: 'googleGenAIKey',
2222
});
2323

2424
defineComponents(IgcChatComponent);
@@ -146,7 +146,10 @@ const ai_chat_options = {
146146

147147
const chat_options = {
148148
disableAutoScroll: false,
149-
disableAttachments: true,
149+
disableAttachments: false,
150+
suggestions: ['Hello', 'Hi', 'How are you?'],
151+
inputPlaceholder: 'Type your message here...',
152+
headerText: 'Chat',
150153
};
151154

152155
function handleCustomSendClick() {

0 commit comments

Comments
 (0)