Skip to content

Commit

Permalink
feat: reuse basic framework of the POC
Browse files Browse the repository at this point in the history
  • Loading branch information
chriskari committed Dec 2, 2024
1 parent ad76fef commit e630aa7
Show file tree
Hide file tree
Showing 25 changed files with 1,183 additions and 30 deletions.
17 changes: 17 additions & 0 deletions public/i18n/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ common:
remove-all: Remove all
reset: Reset
restart: Restart
retry: Retry
save: Save
submit: Submit
update: Update
Expand Down Expand Up @@ -748,6 +749,22 @@ jobs:
kubeconfig-id:
error: "Couldn't load kubeconfig ID; configuration not changed (Error: ${{error}})"
must-be-an-object: Kubeconfig must be a JSON or YAML object.
kyma-companion:
name: Joule
opener:
use-ai: AI Companion
suggestions: Suggestions
input-placeholder: Ask about this resource
error-message: Couldn't fetch suggestions. Please try again.
error:
title: Service is interrupted
subtitle: A temporary interruption occured. Please try again.
introduction1: Hello there,
introduction2: How can I help you?
placeholder: Type something
tabs:
chat: Chat
page-insights: Page Insights
kyma-modules:
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.
unmanaged-modules-save-warning: Before proceeding, be aware that disabling module management may impact the stability and data integrity of your cluster. Once the management is disabled, reverting back may not be possible. Are you sure you want to continue?
Expand Down
8 changes: 8 additions & 0 deletions src/components/App/App.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
#splitter-layout {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}

#html-wrap {
position: absolute;
top: 0;
Expand Down
90 changes: 60 additions & 30 deletions src/components/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ import useSidebarCondensed from 'sidebar/useSidebarCondensed';
import { useGetValidationEnabledSchemas } from 'state/validationEnabledSchemasAtom';
import { useGetKymaResources } from 'state/kymaResourcesAtom';

import { SplitterElement, SplitterLayout } from '@ui5/webcomponents-react';
import { showKymaCompanionState } from 'components/KymaCompanion/state/showKymaCompanionAtom';
import KymaCompanion from 'components/KymaCompanion/components/KymaCompanion';

export default function App() {
const language = useRecoilValue(languageAtom);
const cluster = useRecoilValue(clusterState);
Expand Down Expand Up @@ -72,37 +76,63 @@ export default function App() {
useAfterInitHook(kubeconfigIdState);
useGetKymaResources();

const showCompanion = useRecoilValue(showKymaCompanionState);

return (
<div id="html-wrap">
<Header />
<div id="page-wrap">
<Sidebar key={cluster?.name} />
<ContentWrapper>
<Routes key={cluster?.name}>
<Route
path="*"
element={
<IncorrectPath
to="clusters"
message={t('components.incorrect-path.message.clusters')}
<SplitterLayout id="splitter-layout">
<SplitterElement
resizable={showCompanion.show}
size={
showCompanion.show
? showCompanion.fullScreen
? '0%'
: '70%'
: '100%'
}
>
<div id="html-wrap">
<Header />
<div id="page-wrap">
<Sidebar key={cluster?.name} />
<ContentWrapper>
<Routes key={cluster?.name}>
<Route
path="*"
element={
<IncorrectPath
to="clusters"
message={t('components.incorrect-path.message.clusters')}
/>
}
/>
<Route path="/" />
<Route path="clusters" element={<ClusterList />} />
<Route
path="cluster/:currentClusterName"
element={<Navigate to="overview" />}
/>
<Route
path="cluster/:currentClusterName/*"
element={<ClusterRoutes />}
/>
}
/>
<Route path="/" />
<Route path="clusters" element={<ClusterList />} />
<Route
path="cluster/:currentClusterName"
element={<Navigate to="overview" />}
/>
<Route
path="cluster/:currentClusterName/*"
element={<ClusterRoutes />}
/>
{makeGardenerLoginRoute()}
</Routes>
<Preferences />
</ContentWrapper>
</div>
</div>
{makeGardenerLoginRoute()}
</Routes>
<Preferences />
</ContentWrapper>
</div>
</div>
</SplitterElement>
{showCompanion.show ? (
<SplitterElement
resizable={!showCompanion.fullScreen}
size={showCompanion.fullScreen ? '100%' : '30%'}
minSize={350}
>
<KymaCompanion />
</SplitterElement>
) : (
<></>
)}
</SplitterLayout>
);
}
87 changes: 87 additions & 0 deletions src/components/KymaCompanion/api/getChatResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { getClusterConfig } from 'state/utils/getBackendInfo';
import { parseWithNestedBrackets } from '../utils/parseNestedBrackets';

type GetChatResponseArgs = {
prompt: string;
handleChatResponse: (chunk: any) => void;
handleError: () => void;
sessionID: string;
clusterUrl: string;
token: string;
certificateAuthorityData: string;
};

export default async function getChatResponse({
prompt,
handleChatResponse,
handleError,
sessionID,
clusterUrl,
token,
certificateAuthorityData,
}: GetChatResponseArgs): Promise<void> {
const { backendAddress } = getClusterConfig();
const url = `${backendAddress}/api/v1/namespaces/ai-core/services/http:ai-backend-clusterip:5000/proxy/api/v1/chat`;
const payload = { question: prompt, session_id: sessionID };
const k8sAuthorization = `Bearer ${token}`;

fetch(url, {
headers: {
accept: 'application/json',
'content-type': 'application/json',
'X-Cluster-Certificate-Authority-Data': certificateAuthorityData,
'X-Cluster-Url': clusterUrl,
'X-K8s-Authorization': k8sAuthorization,
'X-User': sessionID,
},
body: JSON.stringify(payload),
method: 'POST',
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error('Failed to get reader from response body');
}
const decoder = new TextDecoder();
readChunk(reader, decoder, handleChatResponse, handleError, sessionID);
})
.catch(error => {
handleError();
console.error('Error fetching data:', error);
});
}

function readChunk(
reader: ReadableStreamDefaultReader<Uint8Array>,
decoder: TextDecoder,
handleChatResponse: (chunk: any) => void,
handleError: () => void,
sessionID: string,
) {
reader
.read()
.then(({ done, value }) => {
if (done) {
return;
}
// Also handles the rare case of two chunks being sent at once
const receivedString = decoder.decode(value, { stream: true });
const chunks = parseWithNestedBrackets(receivedString).map(chunk => {
return JSON.parse(chunk);
});
chunks.forEach(chunk => {
if ('error' in chunk) {
throw new Error(chunk.error);
}
handleChatResponse(chunk);
});
readChunk(reader, decoder, handleChatResponse, handleError, sessionID);
})
.catch(error => {
handleError();
console.error('Error reading stream:', error);
});
}
40 changes: 40 additions & 0 deletions src/components/KymaCompanion/api/getFollowUpQuestions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { getClusterConfig } from 'state/utils/getBackendInfo';

interface GetFollowUpQuestionsParams {
sessionID?: string;
handleFollowUpQuestions: (results: any) => void;
clusterUrl: string;
token: string;
certificateAuthorityData: string;
}

export default async function getFollowUpQuestions({
sessionID = '',
handleFollowUpQuestions,
clusterUrl,
token,
certificateAuthorityData,
}: GetFollowUpQuestionsParams): Promise<void> {
try {
const { backendAddress } = getClusterConfig();
const url = `${backendAddress}/api/v1/namespaces/ai-core/services/http:ai-backend-clusterip:5000/proxy/api/v1/llm/followup`;
const payload = JSON.parse(`{"session_id":"${sessionID}"}`);
const k8sAuthorization = `Bearer ${token}`;

let { results } = await fetch(url, {
headers: {
accept: 'application/json',
'content-type': 'application/json',
'X-Cluster-Certificate-Authority-Data': certificateAuthorityData,
'X-Cluster-Url': clusterUrl,
'X-K8s-Authorization': k8sAuthorization,
'X-User': sessionID,
},
body: JSON.stringify(payload),
method: 'POST',
}).then(result => result.json());
handleFollowUpQuestions(results);
} catch (error) {
console.error('Error fetching data:', error);
}
}
55 changes: 55 additions & 0 deletions src/components/KymaCompanion/api/getPromptSuggestions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { getClusterConfig } from 'state/utils/getBackendInfo';
import { extractApiGroup } from 'resources/Roles/helpers';

interface GetPromptSuggestionsParams {
namespace?: string;
resourceType?: string;
groupVersion?: string;
resourceName?: string;
sessionID?: string;
clusterUrl: string;
token: string;
certificateAuthorityData: string;
}

// TODO add return type

export default async function getPromptSuggestions({
namespace = '',
resourceType = '',
groupVersion = '',
resourceName = '',
sessionID = '',
clusterUrl,
token,
certificateAuthorityData,
}: GetPromptSuggestionsParams): Promise<any[] | false> {
try {
const { backendAddress } = getClusterConfig();
const url = `${backendAddress}/api/v1/namespaces/ai-core/services/http:ai-backend-clusterip:5000/proxy/api/v1/llm/init`;
const apiGroup = extractApiGroup(groupVersion);
const payload = JSON.parse(
`{"resource_type":"${resourceType.toLowerCase()}${
apiGroup.length ? `.${apiGroup}` : ''
}","resource_name":"${resourceName}","namespace":"${namespace}","session_id":"${sessionID}"}`,
);
const k8sAuthorization = `Bearer ${token}`;

let { results } = await fetch(url, {
headers: {
accept: 'application/json',
'content-type': 'application/json',
'X-Cluster-Certificate-Authority-Data': certificateAuthorityData,
'X-Cluster-Url': clusterUrl,
'X-K8s-Authorization': k8sAuthorization,
'X-User': sessionID,
},
body: JSON.stringify(payload),
method: 'POST',
}).then(result => result.json());
return results;
} catch (error) {
console.error('Error fetching data:', error);
return false;
}
}
31 changes: 31 additions & 0 deletions src/components/KymaCompanion/components/Chat/Chat.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.chat-container {
height: 100%;
overflow: hidden;

.chat-list {
display: flex;
flex-direction: column;
overflow: auto;
gap: 8px;

&::-webkit-scrollbar {
display: none;
}

.left-aligned {
align-self: flex-start;
background-color: var(--sapBackgroundColor);
border-radius: 8px 8px 8px 0;
}

.right-aligned {
align-self: flex-end;
background-color: var(--sapContent_Illustrative_Color1);
border-radius: 8px 8px 0 8px;

.text {
color: white;
}
}
}
}
Loading

0 comments on commit e630aa7

Please sign in to comment.