Skip to content

Commit c44b35b

Browse files
committed
feat(chat-ai): add support for streaming response parts
1 parent b4661b9 commit c44b35b

4 files changed

Lines changed: 73 additions & 20 deletions

File tree

src/components/chat/chat-input.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ export default class IgcChatInputComponent extends LitElement {
6767
private handleKeyDown(e: KeyboardEvent) {
6868
if (e.key === 'Enter' && !e.shiftKey) {
6969
e.preventDefault();
70-
this.sendMessage();
70+
if (!this.isAiResponding) {
71+
this.sendMessage();
72+
}
7173
}
7274
}
7375

@@ -156,8 +158,8 @@ export default class IgcChatInputComponent extends LitElement {
156158
collection="material"
157159
variant="contained"
158160
class="small"
159-
?disabled=${!this.inputValue.trim() &&
160-
this.attachments.length === 0}
161+
?disabled=${this.isAiResponding ||
162+
(!this.inputValue.trim() && this.attachments.length === 0)}
161163
@click=${this.sendMessage}
162164
></igc-icon-button>
163165
</div>

src/components/chat/chat-message.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,6 @@ export default class IgcChatMessageComponent extends LitElement {
3333
@property({ reflect: true, attribute: false })
3434
public isResponse = false;
3535

36-
private formatTime(date: Date | undefined): string | undefined {
37-
return date?.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
38-
}
39-
4036
protected override render() {
4137
const containerClass = `message-container ${!this.isResponse ? 'sent' : ''}`;
4238

src/components/chat/chat.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,39 @@ export default class IgcChatComponent extends EventEmitterMixin<
8383
);
8484
}
8585

86+
/** Starts the response */
87+
public startResponse() {
88+
this.isAiResponding = true;
89+
}
90+
91+
/** Show response part. */
92+
public showResponsePart(part: string) {
93+
if (!this.isAiResponding) {
94+
return false;
95+
}
96+
97+
let responseMessage = this.messages[this.messages.length - 1];
98+
responseMessage = {
99+
...responseMessage,
100+
text: `${responseMessage.text} ${part}`,
101+
};
102+
this.messages[this.messages.length - 1] = responseMessage;
103+
this.messages = [...this.messages];
104+
105+
return true;
106+
}
107+
108+
/** Ends the response */
109+
public endResponse(attachments?: IgcMessageAttachment[]) {
110+
this.isAiResponding = false;
111+
112+
const response = this.messages[this.messages.length - 1];
113+
response.timestamp = new Date();
114+
if (attachments) {
115+
response.attachments = attachments;
116+
}
117+
}
118+
86119
private handleSendMessage(e: CustomEvent) {
87120
const text = e.detail.text;
88121
const attachments = e.detail.attachments || [];

stories/chat.stories.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ type Story = StoryObj<IgcChatArgs>;
5959

6060
// endregion
6161

62-
const messages: any[] = [
62+
let messages: any[] = [
6363
{
6464
id: '1',
6565
text: "Hello! I'm an AI assistant created with Lit. How can I help you today?",
@@ -76,21 +76,32 @@ function handleMessageSend(e: CustomEvent) {
7676
return;
7777
}
7878

79+
// create empty response
80+
const emptyResponse = {
81+
id: Date.now().toString(),
82+
text: '',
83+
isResponse: true,
84+
timestamp: new Date(),
85+
attachments: [],
86+
};
87+
messages = [...messages, emptyResponse];
7988
chat.messages = [...messages];
8089

81-
chat.isAiResponding = true;
90+
chat.startResponse();
8291
setTimeout(() => {
83-
const aiResponse = {
84-
id: (Date.now() + 1).toString(),
85-
text: generateAIResponse(e.detail.text),
86-
isResponse: true,
87-
timestamp: new Date(),
88-
};
89-
90-
chat.isAiResponding = false;
91-
chat.messages = [...messages, aiResponse];
92-
messages.push(aiResponse);
93-
}, 1500);
92+
const responseParts = generateAIResponse(e.detail.text).split(' ');
93+
showResponse(chat, responseParts).then(() => {
94+
messages = chat.messages;
95+
chat.endResponse();
96+
});
97+
}, 1000);
98+
}
99+
100+
async function showResponse(chat: any, responseParts: any) {
101+
for (let i = 0; i < responseParts.length; i++) {
102+
await new Promise((resolve) => setTimeout(resolve, 500));
103+
chat.showResponsePart(responseParts[i]);
104+
}
94105
}
95106

96107
function generateAIResponse(message: string): string {
@@ -117,6 +128,17 @@ function generateAIResponse(message: string): string {
117128
if (lowerMessage.includes('list') || lowerMessage.includes('items')) {
118129
return "Here's an example of a list:\n\n- First item\n- Second item\n- Third item with **bold text**";
119130
}
131+
if (lowerMessage.includes('heading') || lowerMessage.includes('headings')) {
132+
return `Here's how you can use different headings in Markdown:
133+
134+
# Heading 1
135+
## Heading 2
136+
### Heading 3
137+
#### Heading 4
138+
##### Heading 5
139+
###### Heading 6
140+
`;
141+
}
120142

121143
return 'Thanks for your message. This is a demonstration of a chat interface built with Lit components. Feel free to ask about features or try different types of messages!';
122144
}

0 commit comments

Comments
 (0)