Skip to content

Commit b1b77d8

Browse files
authored
feat: Reuse POC's UI and state (#3512)
* feat: reuse basic framework of the POC * added feature flag and hide companion based on its value * remove API and utility related code from PR for easier reviewing * suppress unused vars check temporarily * fix: ui5 errors after merge * fix: removve toolbar
1 parent afd2d19 commit b1b77d8

File tree

25 files changed

+892
-30
lines changed

25 files changed

+892
-30
lines changed

kyma/environments/dev/config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ config:
7070
isEnabled: false
7171
EXTENSIBILITY_WIZARD:
7272
isEnabled: true
73+
KYMA_COMPANION:
74+
isEnabled: false
7375
TRACKING:
7476
isEnabled: false
7577
EVENTING:

kyma/environments/prod/config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ config:
7272
isEnabled: false
7373
EXTENSIBILITY_WIZARD:
7474
isEnabled: true
75+
KYMA_COMPANION:
76+
isEnabled: false
7577
EVENTING:
7678
isEnabled: true
7779
selectors:

kyma/environments/stage/config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ config:
7070
isEnabled: false
7171
EXTENSIBILITY_WIZARD:
7272
isEnabled: true
73+
KYMA_COMPANION:
74+
isEnabled: false
7375
EVENTING:
7476
isEnabled: true
7577
selectors:

public/defaultConfig.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ config:
5353
isEnabled: false
5454
EXTENSIBILITY_WIZARD:
5555
isEnabled: true
56+
KYMA_COMPANION:
57+
isEnabled: false
5658
TRACKING:
5759
isEnabled: false
5860
EVENTING:

public/i18n/en.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ common:
239239
remove-all: Remove all
240240
reset: Reset
241241
restart: Restart
242+
retry: Retry
242243
save: Save
243244
submit: Submit
244245
update: Update
@@ -748,6 +749,22 @@ jobs:
748749
kubeconfig-id:
749750
error: "Couldn't load kubeconfig ID; configuration not changed (Error: ${{error}})"
750751
must-be-an-object: Kubeconfig must be a JSON or YAML object.
752+
kyma-companion:
753+
name: Joule
754+
opener:
755+
use-ai: AI Companion
756+
suggestions: Suggestions
757+
input-placeholder: Ask about this resource
758+
error-message: Couldn't fetch suggestions. Please try again.
759+
error:
760+
title: Service is interrupted
761+
subtitle: A temporary interruption occured. Please try again.
762+
introduction1: Hello there,
763+
introduction2: How can I help you?
764+
placeholder: Type something
765+
tabs:
766+
chat: Chat
767+
page-insights: Page Insights
751768
kyma-modules:
752769
associated-resources: Associated Resources
753770
unmanaged-modules-info: One of the modules is not managed and may not work properly. We cannot guarantee any service level agreement (SLA) or provide updates and maintenance for the module.

src/components/App/App.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
#splitter-layout {
2+
position: absolute;
3+
top: 0;
4+
right: 0;
5+
bottom: 0;
6+
left: 0;
7+
}
8+
19
#html-wrap {
210
position: absolute;
311
top: 0;

src/components/App/App.tsx

Lines changed: 59 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ import useSidebarCondensed from 'sidebar/useSidebarCondensed';
2828
import { useGetValidationEnabledSchemas } from 'state/validationEnabledSchemasAtom';
2929
import { useGetKymaResources } from 'state/kymaResourcesAtom';
3030

31+
import { SplitterElement, SplitterLayout } from '@ui5/webcomponents-react';
32+
import { showKymaCompanionState } from 'components/KymaCompanion/state/showKymaCompanionAtom';
33+
import KymaCompanion from 'components/KymaCompanion/components/KymaCompanion';
3134
import { Preferences } from 'components/Preferences/Preferences';
3235
import { Header } from 'header/Header';
3336
import { ContentWrapper } from './ContentWrapper/ContentWrapper';
@@ -77,43 +80,69 @@ export default function App() {
7780
useAfterInitHook(kubeconfigIdState);
7881
useGetKymaResources();
7982

83+
const showCompanion = useRecoilValue(showKymaCompanionState);
84+
8085
if (isLoading) {
8186
return <Spinner />;
8287
}
8388

8489
initTheme(theme);
8590

8691
return (
87-
<div id="html-wrap">
88-
<Header />
89-
<div id="page-wrap">
90-
<Sidebar key={cluster?.name} />
91-
<ContentWrapper>
92-
<Routes key={cluster?.name}>
93-
<Route
94-
path="*"
95-
element={
96-
<IncorrectPath
97-
to="clusters"
98-
message={t('components.incorrect-path.message.clusters')}
92+
<SplitterLayout id="splitter-layout">
93+
<SplitterElement
94+
resizable={showCompanion.show}
95+
size={
96+
showCompanion.show
97+
? showCompanion.fullScreen
98+
? '0%'
99+
: '70%'
100+
: '100%'
101+
}
102+
>
103+
<div id="html-wrap">
104+
<Header />
105+
<div id="page-wrap">
106+
<Sidebar key={cluster?.name} />
107+
<ContentWrapper>
108+
<Routes key={cluster?.name}>
109+
<Route
110+
path="*"
111+
element={
112+
<IncorrectPath
113+
to="clusters"
114+
message={t('components.incorrect-path.message.clusters')}
115+
/>
116+
}
117+
/>
118+
<Route path="/" />
119+
<Route path="clusters" element={<ClusterList />} />
120+
<Route
121+
path="cluster/:currentClusterName"
122+
element={<Navigate to="overview" />}
123+
/>
124+
<Route
125+
path="cluster/:currentClusterName/*"
126+
element={<ClusterRoutes />}
99127
/>
100-
}
101-
/>
102-
<Route path="/" />
103-
<Route path="clusters" element={<ClusterList />} />
104-
<Route
105-
path="cluster/:currentClusterName"
106-
element={<Navigate to="overview" />}
107-
/>
108-
<Route
109-
path="cluster/:currentClusterName/*"
110-
element={<ClusterRoutes />}
111-
/>
112-
{makeGardenerLoginRoute()}
113-
</Routes>
114-
<Preferences />
115-
</ContentWrapper>
116-
</div>
117-
</div>
128+
{makeGardenerLoginRoute()}
129+
</Routes>
130+
<Preferences />
131+
</ContentWrapper>
132+
</div>
133+
</div>
134+
</SplitterElement>
135+
{showCompanion.show ? (
136+
<SplitterElement
137+
resizable={!showCompanion.fullScreen}
138+
size={showCompanion.fullScreen ? '100%' : '30%'}
139+
minSize={350}
140+
>
141+
<KymaCompanion />
142+
</SplitterElement>
143+
) : (
144+
<></>
145+
)}
146+
</SplitterLayout>
118147
);
119148
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
.chat-container {
2+
height: 100%;
3+
overflow: hidden;
4+
5+
.chat-list {
6+
display: flex;
7+
flex-direction: column;
8+
overflow: auto;
9+
gap: 8px;
10+
11+
&::-webkit-scrollbar {
12+
display: none;
13+
}
14+
15+
.left-aligned {
16+
align-self: flex-start;
17+
background-color: var(--sapBackgroundColor);
18+
border-radius: 8px 8px 8px 0;
19+
}
20+
21+
.right-aligned {
22+
align-self: flex-end;
23+
background-color: var(--sapContent_Illustrative_Color1);
24+
border-radius: 8px 8px 0 8px;
25+
26+
.text {
27+
color: white;
28+
}
29+
}
30+
}
31+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// TODO: uncomment when API changes are added
2+
/* eslint-disable @typescript-eslint/no-unused-vars */
3+
import { useTranslation } from 'react-i18next';
4+
import React, { useEffect, useRef, useState } from 'react';
5+
import { useRecoilValue } from 'recoil';
6+
import { FlexBox, Icon, Input } from '@ui5/webcomponents-react';
7+
import { initialPromptState } from 'components/KymaCompanion/state/initalPromptAtom';
8+
import Message from './messages/Message';
9+
import Bubbles from './messages/Bubbles';
10+
import ErrorMessage from './messages/ErrorMessage';
11+
import { sessionIDState } from 'components/KymaCompanion/state/sessionIDAtom';
12+
import { clusterState } from 'state/clusterAtom';
13+
import { authDataState } from 'state/authDataAtom';
14+
import './Chat.scss';
15+
16+
interface MessageType {
17+
author: 'user' | 'ai';
18+
messageChunks: { step: string; result: string }[];
19+
isLoading: boolean;
20+
suggestions?: any[];
21+
}
22+
23+
export default function Chat() {
24+
const { t } = useTranslation();
25+
const containerRef = useRef<HTMLDivElement | null>(null);
26+
const [inputValue, setInputValue] = useState<string>('');
27+
const [chatHistory, setChatHistory] = useState<MessageType[]>([]);
28+
const [errorOccured, setErrorOccured] = useState<boolean>(false);
29+
const initialPrompt = useRecoilValue<string>(initialPromptState);
30+
const sessionID = useRecoilValue<string>(sessionIDState);
31+
const cluster = useRecoilValue<any>(clusterState);
32+
const authData = useRecoilValue<any>(authDataState);
33+
34+
const addMessage = ({ author, messageChunks, isLoading }: MessageType) => {
35+
setChatHistory(prevItems =>
36+
prevItems.concat({ author, messageChunks, isLoading }),
37+
);
38+
};
39+
40+
const handleChatResponse = (response: any) => {
41+
const isLoading = response?.step !== 'output';
42+
if (!isLoading) {
43+
// TODO: uncomment when API changes are added
44+
/*getFollowUpQuestions({ sessionID, handleFollowUpQuestions, clusterUrl: cluster.currentContext.cluster.cluster.server, token: authData.token, certificateAuthorityData: cluster.currentContext.cluster.cluster['certificate-authority-data']});*/
45+
}
46+
setChatHistory(prevMessages => {
47+
const [latestMessage] = prevMessages.slice(-1);
48+
return prevMessages.slice(0, -1).concat({
49+
author: 'ai',
50+
messageChunks: latestMessage.messageChunks.concat(response),
51+
isLoading,
52+
});
53+
});
54+
};
55+
56+
const handleFollowUpQuestions = (questions: any) => {
57+
setChatHistory(prevMessages => {
58+
const [latestMessage] = prevMessages.slice(-1);
59+
return prevMessages
60+
.slice(0, -1)
61+
.concat({ ...latestMessage, suggestions: questions });
62+
});
63+
};
64+
65+
const handleError = () => {
66+
setErrorOccured(true);
67+
setChatHistory(prevItems => prevItems.slice(0, -2));
68+
};
69+
70+
const sendPrompt = (prompt: string) => {
71+
setErrorOccured(false);
72+
addMessage({
73+
author: 'user',
74+
messageChunks: [{ step: 'output', result: prompt }],
75+
isLoading: false,
76+
});
77+
// TODO: uncomment when API changes are added
78+
/*getChatResponse({ prompt, handleChatResponse, handleError, sessionID, clusterUrl: cluster.currentContext.cluster.cluster.server, token: authData.token, certificateAuthorityData: cluster.currentContext.cluster.cluster['certificate-authority-data'], });*/
79+
addMessage({ author: 'ai', messageChunks: [], isLoading: true });
80+
};
81+
82+
const onSubmitInput = () => {
83+
if (inputValue.length === 0) return;
84+
const prompt = inputValue;
85+
setInputValue('');
86+
sendPrompt(prompt);
87+
};
88+
89+
const scrollToBottom = () => {
90+
if (containerRef?.current?.lastChild)
91+
(containerRef.current.lastChild as HTMLElement).scrollIntoView({
92+
behavior: 'smooth',
93+
block: 'start',
94+
});
95+
};
96+
97+
useEffect(() => {
98+
if (chatHistory.length === 0) sendPrompt(initialPrompt);
99+
// eslint-disable-next-line
100+
}, []);
101+
102+
useEffect(() => {
103+
const delay = errorOccured ? 500 : 0;
104+
setTimeout(() => {
105+
scrollToBottom();
106+
}, delay);
107+
}, [chatHistory, errorOccured]);
108+
109+
return (
110+
<FlexBox
111+
direction="Column"
112+
justifyContent="SpaceBetween"
113+
className="chat-container"
114+
>
115+
<div className="chat-list sap-margin-tiny" ref={containerRef}>
116+
{chatHistory.map((message, index) => {
117+
return message.author === 'ai' ? (
118+
<React.Fragment key={index}>
119+
<Message
120+
className="left-aligned"
121+
messageChunks={message.messageChunks}
122+
isLoading={message.isLoading}
123+
/>
124+
{index === chatHistory.length - 1 && !message.isLoading && (
125+
<Bubbles
126+
onClick={sendPrompt}
127+
suggestions={message.suggestions}
128+
/>
129+
)}
130+
</React.Fragment>
131+
) : (
132+
<Message
133+
key={index}
134+
className="right-aligned"
135+
messageChunks={message.messageChunks}
136+
isLoading={message.isLoading}
137+
/>
138+
);
139+
})}
140+
{errorOccured && (
141+
<ErrorMessage
142+
errorOnInitialMessage={chatHistory.length === 0}
143+
resendInitialPrompt={() => sendPrompt(initialPrompt)}
144+
/>
145+
)}
146+
</div>
147+
<div className="sap-margin-x-tiny">
148+
<Input
149+
className="full-width"
150+
disabled={chatHistory[chatHistory.length - 1]?.isLoading}
151+
placeholder={t('kyma-companion.placeholder')}
152+
value={inputValue}
153+
icon={<Icon name="paper-plane" onClick={onSubmitInput} />}
154+
onKeyDown={e => e.key === 'Enter' && onSubmitInput()}
155+
onInput={e => setInputValue(e.target.value)}
156+
/>
157+
</div>
158+
</FlexBox>
159+
);
160+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.bubbles-container {
2+
max-width: 90%;
3+
gap: 8px;
4+
5+
.bubble-button {
6+
align-self: flex-start;
7+
color: var(--sapChart_OrderedColor_5);
8+
}
9+
10+
.bubble-button:hover {
11+
background-color: var(--sapBackgroundColor1);
12+
border-color: var(--sapChart_OrderedColor_5);
13+
}
14+
}

0 commit comments

Comments
 (0)