Skip to content

Commit b1ad990

Browse files
authored
feat: Add loading screen for Joule (#4058)
* feat: add Welcome screen * fix: adjust text animation * test: add and adjust tests * cleanup * fix: adjustments * test: adjust test * test: adjust to new loading * test: fix ui test * fix: reset * test: adjust after fix * test: adjust * fix: loading & welcome screen after reset * test: adjust tests after fix * fix: adjust size * test: adjust to timestamp change * test: stabilize? * fix: render of word after reset * fix: better fix
1 parent 065bdb0 commit b1ad990

File tree

14 files changed

+564
-132
lines changed

14 files changed

+564
-132
lines changed

public/i18n/en.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,14 @@ kyma-companion:
806806
tabs:
807807
chat: Chat
808808
page-insights: Page Insights
809+
welcome-screen:
810+
hello: Hello
811+
talk-to-me: Talk to me naturally. For example, "what are my tasks for today?"
812+
how-can-i: How can I
813+
you-question: you?
814+
help: help
815+
assist: assist
816+
guide: guide
809817
time:
810818
today: Today
811819
yesterday: Yesterday

src/components/KymaCompanion/components/Chat/Chat.scss

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
.chat-container {
22
height: calc(100vh - 60px - 1rem);
33

4+
&.split {
5+
display: grid;
6+
grid-template-rows: 50% 50%;
7+
grid-template-columns: 100%;
8+
}
9+
410
.chat-list {
511
display: flex;
612
flex-direction: column;
@@ -38,3 +44,16 @@
3844
}
3945
}
4046
}
47+
48+
.chat-loading-screen {
49+
display: flex;
50+
justify-content: center;
51+
align-items: center;
52+
height: calc(100vh - 60px - 1rem);
53+
margin: 0;
54+
background: var(--sapAssistant_BackgroundGradient);
55+
56+
.chat-loading-indicator {
57+
height: 416px;
58+
}
59+
}

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

Lines changed: 123 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import {
2626
import { chatHelpers } from './chatHelper';
2727
import './Chat.scss';
2828
import FeedbackMessage from './FeedbackMessage/FeedbackMessage';
29+
import { WelcomeScreen } from '../WelcomeScreen/WelcomeScreen';
30+
import loadingIcon from './icon_loading.gif';
2931

3032
type ChatProps = {
3133
chatHistory: ChatGroup[];
@@ -38,6 +40,8 @@ type ChatProps = {
3840
setError: React.Dispatch<React.SetStateAction<AIError>>;
3941
hide: boolean;
4042
time: Date | null;
43+
isInitialScreen: boolean;
44+
onInitialLoadingChange?: (isLoading: boolean) => void;
4145
};
4246

4347
export const Chat = ({
@@ -51,10 +55,13 @@ export const Chat = ({
5155
setIsReset,
5256
hide = false,
5357
time,
58+
isInitialScreen,
59+
onInitialLoadingChange = () => {},
5460
}: ChatProps) => {
5561
const { t } = useTranslation();
5662
const chatRef = useRef<HTMLDivElement | null>(null);
5763
const containerRef = useRef<HTMLDivElement | null>(null);
64+
const hasLoadingScreenLoaded = useRef(false);
5865

5966
const sessionID = useRecoilValue<string>(sessionIDState);
6067
const cluster = useRecoilValue<any>(clusterState);
@@ -366,85 +373,129 @@ export const Chat = ({
366373
}, delay);
367374
}, [chatHistory, error]);
368375

376+
const showWelcomeScreen =
377+
chatHistory[0].messages.length === 1 && isInitialScreen;
378+
379+
useEffect(() => {
380+
if (
381+
!initialSuggestionsLoading &&
382+
isInitialScreen &&
383+
!hasLoadingScreenLoaded.current
384+
) {
385+
hasLoadingScreenLoaded.current = true;
386+
}
387+
388+
onInitialLoadingChange?.(
389+
initialSuggestionsLoading &&
390+
isInitialScreen &&
391+
!hasLoadingScreenLoaded.current,
392+
);
393+
}, [initialSuggestionsLoading, isInitialScreen, onInitialLoadingChange]);
394+
369395
return (
370-
<FlexBox
371-
style={hide ? { display: 'none' } : undefined}
372-
direction="Column"
373-
justifyContent="SpaceBetween"
374-
className="chat-container"
375-
ref={containerRef}
376-
>
377-
<div
378-
className="chat-list sap-margin-x-tiny sap-margin-top-tiny"
379-
ref={chatRef}
380-
>
381-
{time && <TimestampLabel time={time} />}
382-
{chatHistory.map((group, groupIndex) => {
383-
const isLastGroup = groupIndex === chatHistory.length - 1;
384-
385-
return (
386-
<div key={groupIndex} className="context-group">
387-
{group.context && (
388-
<ContextLabel labelText={group.context.labelText} />
389-
)}
390-
<div className="message-context">
391-
{group.messages.map((message, messageIndex) => {
392-
const isLastMessage =
393-
isLastGroup && messageIndex === group.messages.length - 1;
394-
395-
return message.author === Author.AI ? (
396-
<>
397-
{message.isFeedback ? (
398-
<FeedbackMessage />
399-
) : (
400-
<React.Fragment key={`${groupIndex}-${messageIndex}`}>
396+
<>
397+
{initialSuggestionsLoading && !hasLoadingScreenLoaded.current ? (
398+
<div className="chat-loading-screen">
399+
<img
400+
src={loadingIcon}
401+
className="chat-loading-indicator"
402+
alt="Loading indicator"
403+
/>
404+
</div>
405+
) : (
406+
<div
407+
className={`chat-container ${showWelcomeScreen ? 'split' : ''}`}
408+
style={hide ? { display: 'none' } : {}}
409+
>
410+
{showWelcomeScreen && <WelcomeScreen />}
411+
<FlexBox
412+
style={{ height: '100%' }}
413+
direction="Column"
414+
justifyContent="SpaceBetween"
415+
ref={containerRef}
416+
>
417+
<div
418+
className="chat-list sap-margin-x-tiny sap-margin-top-tiny"
419+
ref={chatRef}
420+
>
421+
{time && !showWelcomeScreen && <TimestampLabel time={time} />}
422+
{chatHistory.map((group, groupIndex) => {
423+
const isLastGroup = groupIndex === chatHistory.length - 1;
424+
425+
return (
426+
<div key={groupIndex} className="context-group">
427+
{group.context && (
428+
<ContextLabel labelText={group.context.labelText} />
429+
)}
430+
<div className="message-context">
431+
{group.messages.map((message, messageIndex) => {
432+
const isLastMessage =
433+
isLastGroup &&
434+
messageIndex === group.messages.length - 1;
435+
436+
return message.author === Author.AI ? (
437+
<>
438+
{message.isFeedback ? (
439+
<FeedbackMessage />
440+
) : (
441+
<React.Fragment
442+
key={`${groupIndex}-${messageIndex}`}
443+
>
444+
<Message
445+
author={message.author}
446+
messageChunks={message.messageChunks}
447+
isLoading={message.isLoading}
448+
partialAIFailure={message.partialAIFailure}
449+
hasError={message.hasError}
450+
isLatestMessage={isLastMessage}
451+
/>
452+
{isLastMessage && !message.isLoading && (
453+
<Bubbles
454+
onClick={sendPrompt}
455+
suggestions={message.suggestions}
456+
isLoading={
457+
message.suggestionsLoading ?? false
458+
}
459+
/>
460+
)}
461+
</React.Fragment>
462+
)}
463+
</>
464+
) : (
401465
<Message
402-
author={message.author}
466+
author={Author.USER}
467+
key={`${groupIndex}-${messageIndex}`}
403468
messageChunks={message.messageChunks}
404469
isLoading={message.isLoading}
405-
partialAIFailure={message.partialAIFailure}
406470
hasError={message.hasError}
407471
isLatestMessage={isLastMessage}
408472
/>
409-
{isLastMessage && !message.isLoading && (
410-
<Bubbles
411-
onClick={sendPrompt}
412-
suggestions={message.suggestions}
413-
isLoading={message.suggestionsLoading ?? false}
414-
/>
415-
)}
416-
</React.Fragment>
417-
)}
418-
</>
419-
) : (
420-
<Message
421-
author={Author.USER}
422-
key={`${groupIndex}-${messageIndex}`}
423-
messageChunks={message.messageChunks}
424-
isLoading={message.isLoading}
425-
hasError={message.hasError}
426-
isLatestMessage={isLastMessage}
427-
/>
428-
);
429-
})}
430-
</div>
473+
);
474+
})}
475+
</div>
476+
</div>
477+
);
478+
})}
479+
{error.message && (
480+
<ErrorMessage
481+
errorTitle={error?.title}
482+
errorMessage={
483+
error.message ?? t('kyma-companion.error.subtitle')
484+
}
485+
retryPrompt={() => retryPreviousPrompt()}
486+
displayRetry={error.displayRetry}
487+
/>
488+
)}
431489
</div>
432-
);
433-
})}
434-
{error.message && (
435-
<ErrorMessage
436-
errorTitle={error?.title}
437-
errorMessage={error.message ?? t('kyma-companion.error.subtitle')}
438-
retryPrompt={() => retryPreviousPrompt()}
439-
displayRetry={error.displayRetry}
440-
/>
441-
)}
442-
</div>
443-
<QueryInput
444-
loading={loading}
445-
sendPrompt={sendPrompt}
446-
containerRef={containerRef}
447-
/>
448-
</FlexBox>
490+
491+
<QueryInput
492+
loading={loading}
493+
sendPrompt={sendPrompt}
494+
containerRef={containerRef}
495+
/>
496+
</FlexBox>
497+
</div>
498+
)}
499+
</>
449500
);
450501
};
237 KB
Loading

src/components/KymaCompanion/components/KymaCompanion.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
height: 100%;
1111
width: 100%;
1212

13-
&__disclaimer-header {
13+
&__fullscreen-header {
1414
@extend .kyma-companion__header;
1515
background: unset !important;
1616
background-position: unset !important;

src/components/KymaCompanion/components/KymaCompanion.tsx

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from 'react';
1+
import { useEffect, useState } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import { Button, Card, Title } from '@ui5/webcomponents-react';
44
import { useRecoilState } from 'recoil';
@@ -20,6 +20,8 @@ export default function KymaCompanion() {
2020
);
2121
const [showDisclaimer, setShowDisclaimer] = useState<boolean>(false);
2222
const [loading, setLoading] = useState<boolean>(false);
23+
const [isInitialScreen, setIsInitialScreen] = useState<boolean>(true);
24+
const [initialLoading, setInitialLoading] = useState<boolean>(true);
2325
const [isReset, setIsReset] = useState<boolean>(false);
2426
const [chatHistory, setChatHistory] = useState<ChatGroup[]>(
2527
chatHelpers.createInitialState(t('kyma-companion.introduction')),
@@ -40,23 +42,33 @@ export default function KymaCompanion() {
4042
});
4143
setTime(new Date());
4244
setIsReset(true);
45+
setIsInitialScreen(false);
4346
}
4447

48+
useEffect(() => {
49+
if (chatHistory[0].messages.length > 1) {
50+
setIsInitialScreen(false);
51+
return;
52+
} else {
53+
setIsInitialScreen(true);
54+
}
55+
}, [chatHistory]);
56+
4557
return (
4658
<div id="companion_wrapper">
4759
<Card
4860
className="kyma-companion"
4961
header={
5062
<div
5163
className={`kyma-companion__${
52-
showDisclaimer ? 'disclaimer-' : ''
64+
showDisclaimer || isInitialScreen ? 'fullscreen-' : ''
5365
}header`}
5466
>
5567
<Title level="H5" size="H5" className="companion-title">
5668
{t('kyma-companion.name')}
5769
</Title>
5870
<div className="actions-container">
59-
{!showDisclaimer && (
71+
{!showDisclaimer && !isInitialScreen && (
6072
<Button
6173
design="Transparent"
6274
icon="restart"
@@ -66,20 +78,24 @@ export default function KymaCompanion() {
6678
onClick={() => handleRefresh()}
6779
/>
6880
)}
69-
<Button
70-
design="Transparent"
71-
icon={
72-
showCompanion.fullScreen ? 'exit-full-screen' : 'full-screen'
73-
}
74-
className="action"
75-
onClick={() =>
76-
setShowCompanion({
77-
show: true,
78-
fullScreen: !showCompanion.fullScreen,
79-
})
80-
}
81-
/>
82-
{!showDisclaimer && (
81+
{!initialLoading && (
82+
<Button
83+
design="Transparent"
84+
icon={
85+
showCompanion.fullScreen
86+
? 'exit-full-screen'
87+
: 'full-screen'
88+
}
89+
className="action"
90+
onClick={() =>
91+
setShowCompanion({
92+
show: true,
93+
fullScreen: !showCompanion.fullScreen,
94+
})
95+
}
96+
/>
97+
)}
98+
{!showDisclaimer && !initialLoading && (
8399
<Button
84100
design="Transparent"
85101
icon="hint"
@@ -112,6 +128,8 @@ export default function KymaCompanion() {
112128
setError={setError}
113129
hide={showDisclaimer}
114130
time={time}
131+
isInitialScreen={isInitialScreen}
132+
onInitialLoadingChange={setInitialLoading}
115133
/>
116134
{showDisclaimer && (
117135
<Disclaimer hideDisclaimer={() => setShowDisclaimer(false)} />

0 commit comments

Comments
 (0)