Skip to content

Commit 6bff107

Browse files
committed
feat: add loading indicator, and refresh when navigate to different resource
1 parent 55fd928 commit 6bff107

File tree

6 files changed

+103
-42
lines changed

6 files changed

+103
-42
lines changed

backend/companion/TokenManager.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ export class TokenManager {
3535
getExpirationFromJWT(token) {
3636
try {
3737
// Split the token and get the payload
38+
const PAYLOAD_INDEX = 1;
3839
const payload = JSON.parse(
39-
Buffer.from(token.split('.')[1], 'base64').toString(),
40+
Buffer.from(token.split('.')[PAYLOAD_INDEX], 'base64').toString(),
4041
);
4142
// exp is in seconds, convert to milliseconds
4243
return payload.exp * 1000;

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

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,19 @@ interface MessageType {
1616
author: 'user' | 'ai';
1717
messageChunks: { step: string; result: string }[];
1818
isLoading: boolean;
19-
suggestions?: any[];
19+
suggestions?: string[];
20+
suggestionsLoading?: boolean;
2021
}
2122

22-
export default function Chat({ suggestions }: { suggestions: string[] }) {
23+
interface ChatProps {
24+
initialSuggestions: string[];
25+
initialSuggestionsLoading: boolean;
26+
}
27+
28+
export default function Chat({
29+
initialSuggestions,
30+
initialSuggestionsLoading,
31+
}: ChatProps) {
2332
const { t } = useTranslation();
2433
const containerRef = useRef<HTMLDivElement | null>(null);
2534
const [inputValue, setInputValue] = useState<string>('');
@@ -30,6 +39,7 @@ export default function Chat({ suggestions }: { suggestions: string[] }) {
3039
{ step: 'output', result: t('kyma-companion.introduction') },
3140
],
3241
isLoading: false,
42+
suggestionsLoading: true,
3343
},
3444
]);
3545
const [errorOccured, setErrorOccured] = useState<boolean>(false);
@@ -46,6 +56,7 @@ export default function Chat({ suggestions }: { suggestions: string[] }) {
4656
const handleChatResponse = (response: any) => {
4757
const isLoading = response?.step !== 'output';
4858
if (!isLoading) {
59+
setFollowUpLoading();
4960
getFollowUpQuestions({
5061
sessionID,
5162
handleFollowUpQuestions,
@@ -65,12 +76,24 @@ export default function Chat({ suggestions }: { suggestions: string[] }) {
6576
});
6677
};
6778

68-
const handleFollowUpQuestions = (questions: any) => {
79+
const setFollowUpLoading = () => {
6980
setChatHistory(prevMessages => {
7081
const [latestMessage] = prevMessages.slice(-1);
71-
return prevMessages
72-
.slice(0, -1)
73-
.concat({ ...latestMessage, suggestions: questions });
82+
return prevMessages.slice(0, -1).concat({
83+
...latestMessage,
84+
suggestionsLoading: true,
85+
});
86+
});
87+
};
88+
89+
const handleFollowUpQuestions = (questions: string[]) => {
90+
setChatHistory(prevMessages => {
91+
const [latestMessage] = prevMessages.slice(-1);
92+
return prevMessages.slice(0, -1).concat({
93+
...latestMessage,
94+
suggestions: questions,
95+
suggestionsLoading: false,
96+
});
7497
});
7598
};
7699

@@ -115,11 +138,15 @@ export default function Chat({ suggestions }: { suggestions: string[] }) {
115138
};
116139

117140
useEffect(() => {
118-
if (suggestions.length && chatHistory.length === 1) {
119-
handleFollowUpQuestions(suggestions);
141+
if (chatHistory.length === 1) {
142+
if (initialSuggestionsLoading) {
143+
setFollowUpLoading();
144+
} else if (initialSuggestions.length) {
145+
handleFollowUpQuestions(initialSuggestions);
146+
}
120147
}
121148
// eslint-disable-next-line react-hooks/exhaustive-deps
122-
}, [suggestions]);
149+
}, [initialSuggestions, initialSuggestionsLoading]);
123150

124151
useEffect(() => {
125152
const delay = errorOccured ? 500 : 0;
@@ -150,6 +177,7 @@ export default function Chat({ suggestions }: { suggestions: string[] }) {
150177
<Bubbles
151178
onClick={sendPrompt}
152179
suggestions={message.suggestions}
180+
isLoading={message.suggestionsLoading ?? false}
153181
/>
154182
)}
155183
</React.Fragment>

src/components/KymaCompanion/components/Chat/messages/Bubbles.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
.suggestions-loading-indicator {
2+
align-self: flex-start;
3+
}
4+
15
.bubbles-container {
26
max-width: 90%;
37
gap: 8px;

src/components/KymaCompanion/components/Chat/messages/Bubbles.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,28 @@
1-
import { Button, FlexBox } from '@ui5/webcomponents-react';
1+
import { BusyIndicator, Button, FlexBox } from '@ui5/webcomponents-react';
22
import './Bubbles.scss';
33

44
interface BubblesProps {
55
suggestions: any[] | undefined;
6+
isLoading: boolean;
67
onClick: (suggestion: string) => void;
78
}
89

910
export default function Bubbles({
1011
suggestions,
12+
isLoading,
1113
onClick,
1214
}: BubblesProps): JSX.Element {
15+
if (isLoading) {
16+
return (
17+
<BusyIndicator
18+
className="suggestions-loading-indicator sap-margin-begin-tiny"
19+
active
20+
size="M"
21+
delay={0}
22+
/>
23+
);
24+
}
25+
1326
return suggestions ? (
1427
<FlexBox
1528
wrap="Wrap"

src/components/KymaCompanion/components/KymaCompanion.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export default function KymaCompanion() {
1414
const [showCompanion, setShowCompanion] = useRecoilState<ShowKymaCompanion>(
1515
showKymaCompanionState,
1616
);
17-
const suggestions = usePromptSuggestions();
17+
const { suggestions, loading: suggestionsLoading } = usePromptSuggestions();
1818

1919
return (
2020
<div id="companion_wrapper" className="sap-margin-tiny">
@@ -29,6 +29,7 @@ export default function KymaCompanion() {
2929
<Button
3030
design="Transparent"
3131
icon="restart"
32+
tooltip={t('common.buttons.reset')}
3233
className="action"
3334
onClick={() => {}}
3435
/>
@@ -48,6 +49,7 @@ export default function KymaCompanion() {
4849
<Button
4950
design="Transparent"
5051
icon="decline"
52+
tooltip={t('common.buttons.close')}
5153
className="action"
5254
onClick={() =>
5355
setShowCompanion({ show: false, fullScreen: false })
@@ -57,7 +59,10 @@ export default function KymaCompanion() {
5759
</div>
5860
}
5961
>
60-
<Chat suggestions={suggestions} />
62+
<Chat
63+
initialSuggestions={suggestions}
64+
initialSuggestionsLoading={suggestionsLoading}
65+
/>
6166
</Card>
6267
</div>
6368
);
Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,32 @@
1-
import { useState, useEffect } from 'react';
1+
import { useState, useEffect, useRef } from 'react';
22
import { useRecoilValue, useSetRecoilState } from 'recoil';
33
import { sessionIDState } from '../../../state/companion/sessionIDAtom';
44
import getPromptSuggestions from '../api/getPromptSuggestions';
55
import { ColumnLayoutState, columnLayoutState } from 'state/columnLayoutAtom';
66
import { prettifyNameSingular } from 'shared/utils/helpers';
77
import { usePost } from 'shared/hooks/BackendAPI/usePost';
88

9+
const getResourceFromColumnnLayout = (columnLayout: ColumnLayoutState) => {
10+
const column =
11+
columnLayout?.endColumn ??
12+
columnLayout?.midColumn ??
13+
columnLayout?.startColumn;
14+
return {
15+
namespace: column?.namespaceId ?? '',
16+
resourceType: prettifyNameSingular(column?.resourceType ?? ''),
17+
apiGroup: column?.apiGroup ?? '',
18+
apiVersion: column?.apiVersion ?? '',
19+
resourceName: column?.resourceName ?? '',
20+
};
21+
};
22+
923
export function usePromptSuggestions() {
24+
const post = usePost();
1025
const [suggestions, setSuggestions] = useState<string[]>([]);
1126
const setSessionID = useSetRecoilState(sessionIDState);
1227
const columnLayout = useRecoilValue(columnLayoutState);
13-
const post = usePost();
14-
15-
const getResourceFromColumnnLayout = (columnLayout: ColumnLayoutState) => {
16-
const column =
17-
columnLayout?.endColumn ??
18-
columnLayout?.midColumn ??
19-
columnLayout?.startColumn;
20-
return {
21-
namespace: column?.namespaceId ?? '',
22-
resourceType: prettifyNameSingular(column?.resourceType ?? ''),
23-
apiGroup: column?.apiGroup ?? '',
24-
apiVersion: column?.apiVersion ?? '',
25-
resourceName: column?.resourceName ?? '',
26-
};
27-
};
28+
const [loading, setLoading] = useState(false);
29+
const fetchedResourceRef = useRef('');
2830

2931
useEffect(() => {
3032
const {
@@ -36,24 +38,32 @@ export function usePromptSuggestions() {
3638
} = getResourceFromColumnnLayout(columnLayout);
3739
const groupVersion = apiGroup ? `${apiGroup}/${apiVersion}` : apiVersion;
3840

41+
const resourceKey = `${namespace}|${resourceType}|${groupVersion}|${resourceName}`.toLocaleLowerCase();
42+
3943
async function fetchSuggestions() {
40-
const result = await getPromptSuggestions({
41-
post,
42-
namespace: namespace,
43-
resourceType: resourceType,
44-
groupVersion: groupVersion,
45-
resourceName: resourceName,
46-
});
47-
if (result) {
48-
setSuggestions(result.promptSuggestions);
49-
setSessionID(result.conversationId);
44+
setLoading(true);
45+
try {
46+
const result = await getPromptSuggestions({
47+
post,
48+
namespace: namespace,
49+
resourceType: resourceType,
50+
groupVersion: groupVersion,
51+
resourceName: resourceName,
52+
});
53+
if (result) {
54+
setSuggestions(result.promptSuggestions);
55+
setSessionID(result.conversationId);
56+
}
57+
} finally {
58+
setLoading(false);
5059
}
5160
}
5261

53-
if (resourceType && suggestions.length === 0) {
62+
if (resourceType && fetchedResourceRef.current !== resourceKey) {
63+
fetchedResourceRef.current = resourceKey;
5464
fetchSuggestions();
5565
}
56-
}, [columnLayout, suggestions, post, setSessionID]);
66+
}, [columnLayout, post, setSessionID]);
5767

58-
return suggestions;
68+
return { suggestions, loading };
5969
}

0 commit comments

Comments
 (0)