Skip to content

Commit f30fc16

Browse files
committed
feat: add context label to chat interface
1 parent 2e90627 commit f30fc16

File tree

4 files changed

+159
-53
lines changed

4 files changed

+159
-53
lines changed

src/components/KymaCompanion/components/Chat/Chat.tsx

Lines changed: 120 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useTranslation } from 'react-i18next';
2-
import React, { useEffect, useRef, useState } from 'react';
2+
import React, { useCallback, useEffect, useRef, useState } from 'react';
33
import { useRecoilValue } from 'recoil';
44
import { FlexBox, Icon, Text, TextArea } from '@ui5/webcomponents-react';
55
import Message, {
@@ -16,14 +16,25 @@ import getFollowUpQuestions from 'components/KymaCompanion/api/getFollowUpQuesti
1616
import getChatResponse from 'components/KymaCompanion/api/getChatResponse';
1717
import { usePromptSuggestions } from 'components/KymaCompanion/hooks/usePromptSuggestions';
1818
import { AIError } from '../KymaCompanion';
19+
import ContextLabel from './ContextLabel/ContextLabel';
1920
import './Chat.scss';
2021

21-
enum Author {
22+
export enum Author {
2223
USER = 'user',
2324
AI = 'ai',
2425
}
2526

26-
export interface MessageType {
27+
export enum ChatItemType {
28+
MESSAGE = 'message',
29+
CONTEXT = 'context',
30+
}
31+
32+
interface BaseChatItem {
33+
type: ChatItemType;
34+
}
35+
36+
interface MessageChatItem extends BaseChatItem {
37+
type: ChatItemType.MESSAGE;
2738
author: Author;
2839
messageChunks: MessageChunk[];
2940
isLoading: boolean;
@@ -32,9 +43,16 @@ export interface MessageType {
3243
hasError?: boolean | undefined;
3344
}
3445

46+
interface ContextChatItem extends BaseChatItem {
47+
type: ChatItemType.CONTEXT;
48+
labelText: string;
49+
}
50+
51+
export type ChatItem = MessageChatItem | ContextChatItem;
52+
3553
type ChatProps = {
36-
chatHistory: MessageType[];
37-
setChatHistory: React.Dispatch<React.SetStateAction<MessageType[]>>;
54+
chatHistory: ChatItem[];
55+
setChatHistory: React.Dispatch<React.SetStateAction<ChatItem[]>>;
3856
loading: boolean;
3957
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
4058
isReset: boolean;
@@ -66,22 +84,54 @@ export const Chat = ({
6684
initialSuggestionsLoading,
6785
currentResource,
6886
} = usePromptSuggestions(isReset, setIsReset, {
69-
skip: chatHistory.length > 1,
87+
skip:
88+
chatHistory.filter(item => item.type === ChatItemType.MESSAGE).length > 1,
7089
});
7190

72-
const addMessage = ({ author, messageChunks, isLoading }: MessageType) => {
73-
setChatHistory(prevItems =>
74-
prevItems.concat({ author, messageChunks, isLoading }),
75-
);
91+
const handleContextChange = useCallback(() => {
92+
const newContext = currentResource.resourceName
93+
? `${currentResource.resourceType} > ${currentResource.resourceName}`
94+
: currentResource.resourceType;
95+
96+
const lastContextItem = chatHistory
97+
.slice()
98+
.reverse()
99+
.find(
100+
(item): item is ContextChatItem => item.type === ChatItemType.CONTEXT,
101+
);
102+
103+
if (!lastContextItem || lastContextItem.labelText !== newContext) {
104+
const contextItem: ContextChatItem = {
105+
type: ChatItemType.CONTEXT,
106+
labelText: newContext,
107+
};
108+
setChatHistory(prevItems => prevItems.concat(contextItem));
109+
}
110+
}, [currentResource, chatHistory, setChatHistory]);
111+
112+
const addMessage = ({
113+
author,
114+
messageChunks,
115+
isLoading,
116+
}: Omit<MessageChatItem, 'type'>) => {
117+
const messageItem: MessageChatItem = {
118+
type: ChatItemType.MESSAGE,
119+
author,
120+
messageChunks,
121+
isLoading,
122+
};
123+
setChatHistory(prevItems => prevItems.concat(messageItem));
76124
};
77125

78-
const updateLatestMessage = (updates: Partial<MessageType>) => {
79-
setChatHistory(prevMessages => {
80-
if (prevMessages.length === 0) return prevMessages;
126+
const updateLatestMessage = (updates: Partial<MessageChatItem>) => {
127+
setChatHistory(prevItems => {
128+
if (prevItems.length === 0) return prevItems;
129+
130+
const [latestItem] = prevItems.slice(-1);
131+
if (latestItem.type !== ChatItemType.MESSAGE) return prevItems;
81132

82-
const [latestMessage] = prevMessages.slice(-1);
83-
return prevMessages.slice(0, -1).concat({
84-
...latestMessage,
133+
return prevItems.slice(0, -1).concat({
134+
...latestItem,
85135
...updates,
86136
});
87137
});
@@ -91,35 +141,42 @@ export const Chat = ({
91141
response: MessageChunk,
92142
isLoading: boolean,
93143
) => {
94-
setChatHistory(prevMessages => {
95-
const [latestMessage] = prevMessages.slice(-1);
96-
return prevMessages.slice(0, -1).concat({
97-
...latestMessage,
98-
messageChunks: latestMessage.messageChunks.concat(response),
144+
setChatHistory(prevItems => {
145+
const [latestItem] = prevItems.slice(-1);
146+
if (latestItem.type !== ChatItemType.MESSAGE) return prevItems;
147+
148+
return prevItems.slice(0, -1).concat({
149+
...latestItem,
150+
messageChunks: latestItem.messageChunks.concat(response),
99151
isLoading,
100152
});
101153
});
102154
};
103155

104156
const removeLastMessage = () => {
105-
setChatHistory(prevMessages => prevMessages.slice(0, -1));
157+
setChatHistory(prevItems => prevItems.slice(0, -1));
106158
};
107159

108160
const setErrorOnLastUserMsg = () => {
109-
setChatHistory(prevMessages => {
110-
const lastUserMsgIdx = prevMessages.findLastIndex(msg => {
111-
return msg.author === 'user';
112-
});
161+
setChatHistory(prevItems => {
162+
const lastUserMsgIdx = prevItems.findLastIndex(
163+
item =>
164+
item.type === ChatItemType.MESSAGE && item.author === Author.USER,
165+
);
166+
113167
if (lastUserMsgIdx === -1) {
114-
return prevMessages;
168+
return prevItems;
115169
}
116170

117-
return prevMessages.map((msg, idx) => {
118-
if (idx === lastUserMsgIdx) {
119-
msg.hasError = true;
120-
msg.isLoading = false;
171+
return prevItems.map((item, idx) => {
172+
if (idx === lastUserMsgIdx && item.type === ChatItemType.MESSAGE) {
173+
return {
174+
...item,
175+
hasError: true,
176+
isLoading: false,
177+
};
121178
}
122-
return msg;
179+
return item;
123180
});
124181
});
125182
};
@@ -220,11 +277,14 @@ export const Chat = ({
220277
};
221278

222279
const retryPreviousPrompt = () => {
223-
const lastUserMsgIdx = chatHistory.findLastIndex(
224-
v => v.author === Author.USER,
280+
const messageItems = chatHistory.filter(
281+
(item): item is MessageChatItem => item.type === ChatItemType.MESSAGE,
282+
);
283+
const lastUserMsg = messageItems.findLast(
284+
msg => msg.author === Author.USER,
225285
);
226-
const previousPrompt = chatHistory.at(lastUserMsgIdx)?.messageChunks[0].data
227-
.answer.content;
286+
const previousPrompt = lastUserMsg?.messageChunks[0].data.answer.content;
287+
228288
if (previousPrompt) {
229289
removeLastMessage();
230290
sendPrompt(previousPrompt);
@@ -234,6 +294,9 @@ export const Chat = ({
234294
const sendPrompt = (query: string) => {
235295
setError({ message: null, displayRetry: false });
236296
setLoading(true);
297+
298+
handleContextChange();
299+
237300
addMessage({
238301
author: Author.USER,
239302
messageChunks: [
@@ -285,7 +348,11 @@ export const Chat = ({
285348
};
286349

287350
useEffect(() => {
288-
if (chatHistory.length === 1) {
351+
const messageCount = chatHistory.filter(
352+
item => item.type === ChatItemType.MESSAGE,
353+
).length;
354+
355+
if (messageCount === 1) {
289356
if (initialSuggestionsLoading) {
290357
updateLatestMessage({
291358
messageChunks: [
@@ -338,32 +405,36 @@ export const Chat = ({
338405
className="chat-list sap-margin-x-tiny sap-margin-top-small"
339406
ref={containerRef}
340407
>
341-
{chatHistory.map((message, index) => {
408+
{chatHistory.map((item, index) => {
409+
if (item.type === ChatItemType.CONTEXT) {
410+
return <ContextLabel key={index} labelText={item.labelText} />;
411+
}
412+
342413
const isLast = index === chatHistory.length - 1;
343-
return message.author === Author.AI ? (
414+
return item.author === Author.AI ? (
344415
<React.Fragment key={index}>
345416
<Message
346-
author={message.author}
347-
messageChunks={message.messageChunks}
348-
isLoading={message.isLoading}
349-
hasError={message?.hasError ?? false}
417+
author={item.author}
418+
messageChunks={item.messageChunks}
419+
isLoading={item.isLoading}
420+
hasError={item?.hasError ?? false}
350421
isLatestMessage={isLast}
351422
/>
352-
{isLast && !message.isLoading && (
423+
{isLast && !item.isLoading && (
353424
<Bubbles
354425
onClick={sendPrompt}
355-
suggestions={message.suggestions}
356-
isLoading={message.suggestionsLoading ?? false}
426+
suggestions={item.suggestions}
427+
isLoading={item.suggestionsLoading ?? false}
357428
/>
358429
)}
359430
</React.Fragment>
360431
) : (
361432
<Message
362433
author={Author.USER}
363434
key={index}
364-
messageChunks={message.messageChunks}
365-
isLoading={message.isLoading}
366-
hasError={message?.hasError ?? false}
435+
messageChunks={item.messageChunks}
436+
isLoading={item.isLoading}
437+
hasError={item?.hasError ?? false}
367438
isLatestMessage={isLast}
368439
/>
369440
);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.context-label {
2+
display: flex;
3+
align-items: center;
4+
padding: 0.5rem 0;
5+
margin-inline: 0.5rem;
6+
width: calc(100% - 1rem);
7+
8+
&::before,
9+
&::after {
10+
content: '';
11+
flex-grow: 1;
12+
height: 1.25px;
13+
background-color: var(--sapNeutralColor);
14+
}
15+
}
16+
17+
.context-label-text {
18+
padding: 0 1rem;
19+
color: var(--sapNeutralColor);
20+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Text } from '@ui5/webcomponents-react';
2+
import './ContextLabel.scss';
3+
4+
interface ContextLabelProps {
5+
labelText: string;
6+
}
7+
8+
export default function ContextLabel({ labelText }: ContextLabelProps) {
9+
return (
10+
<div className="context-label">
11+
<Text className="context-label-text">{labelText}</Text>
12+
</div>
13+
);
14+
}

src/components/KymaCompanion/components/KymaCompanion.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
ShowKymaCompanion,
77
showKymaCompanionState,
88
} from 'state/companion/showKymaCompanionAtom';
9-
import { Chat, MessageType } from './Chat/Chat';
9+
import { Author, Chat, ChatItem, ChatItemType } from './Chat/Chat';
1010
import './KymaCompanion.scss';
1111

1212
export interface AIError {
@@ -17,9 +17,10 @@ export interface AIError {
1717
export default function KymaCompanion() {
1818
const { t } = useTranslation();
1919

20-
const initialChatHistory: MessageType[] = [
20+
const initialChatHistory: ChatItem[] = [
2121
{
22-
author: 'ai',
22+
type: ChatItemType.MESSAGE,
23+
author: Author.AI,
2324
messageChunks: [
2425
{
2526
data: {
@@ -40,7 +41,7 @@ export default function KymaCompanion() {
4041
);
4142
const [loading, setLoading] = useState<boolean>(false);
4243
const [isReset, setIsReset] = useState<boolean>(false);
43-
const [chatHistory, setChatHistory] = useState<MessageType[]>(
44+
const [chatHistory, setChatHistory] = useState<ChatItem[]>(
4445
initialChatHistory,
4546
);
4647
const [error, setError] = useState<AIError>({

0 commit comments

Comments
 (0)