From aba43fa20e62ac143887f3e5fb4939f836138998 Mon Sep 17 00:00:00 2001 From: Christian Karidas <105549337+chriskari@users.noreply.github.com> Date: Tue, 19 Mar 2024 16:55:19 +0100 Subject: [PATCH 01/38] feat: ai assistant UI panel with tabs * feat: add basic UI panel for ai assistant * feat: added introbox and tabs --- public/i18n/en.yaml | 7 ++ .../AIassistant/components/AIassistant.js | 62 ++++++++++++++ .../AIassistant/components/AIassistant.scss | 21 +++++ src/components/AIassistant/components/Chat.js | 5 ++ .../AIassistant/components/Chat.scss | 0 .../AIassistant/components/IntroBox.js | 22 +++++ .../AIassistant/components/IntroBox.scss | 40 +++++++++ .../AIassistant/components/PageInsights.js | 5 ++ .../AIassistant/components/PageInsights.scss | 0 .../AIassistant/state/showAIassistantAtom.ts | 12 +++ src/components/App/App.scss | 5 ++ src/components/App/App.tsx | 83 ++++++++++++------- src/header/Header.tsx | 15 +++- 13 files changed, 245 insertions(+), 32 deletions(-) create mode 100644 src/components/AIassistant/components/AIassistant.js create mode 100644 src/components/AIassistant/components/AIassistant.scss create mode 100644 src/components/AIassistant/components/Chat.js create mode 100644 src/components/AIassistant/components/Chat.scss create mode 100644 src/components/AIassistant/components/IntroBox.js create mode 100644 src/components/AIassistant/components/IntroBox.scss create mode 100644 src/components/AIassistant/components/PageInsights.js create mode 100644 src/components/AIassistant/components/PageInsights.scss create mode 100644 src/components/AIassistant/state/showAIassistantAtom.ts diff --git a/public/i18n/en.yaml b/public/i18n/en.yaml index ee2e07f0ed..5e130fb37c 100644 --- a/public/i18n/en.yaml +++ b/public/i18n/en.yaml @@ -3,6 +3,13 @@ horizontal-pod-autoscalers: Horizontal Pod Autoscalers subscriptions: Subscriptions apps: title: Apps +ai-assistant: + name: Joule + introduction1: Hello there, + introduction2: How can I help you? + tabs: + chat: Chat + page-insights: Page Insights cluster-overview: headers: cpu: CPU diff --git a/src/components/AIassistant/components/AIassistant.js b/src/components/AIassistant/components/AIassistant.js new file mode 100644 index 0000000000..7ae17633cd --- /dev/null +++ b/src/components/AIassistant/components/AIassistant.js @@ -0,0 +1,62 @@ +import { useTranslation } from 'react-i18next'; +import { + Button, + Card, + Tab, + TabContainer, + Title, + Toolbar, + ToolbarSpacer, +} from '@ui5/webcomponents-react'; +import { spacing } from '@ui5/webcomponents-react-base'; +import { useSetRecoilState } from 'recoil'; +import { showAIassistantState } from 'components/AIassistant/state/showAIassistantAtom'; +import IntroBox from './IntroBox'; +import Chat from './Chat'; +import PageInsights from './PageInsights'; +import './AIassistant.scss'; + +export default function AIassistant() { + const { t } = useTranslation(); + const setOpenAssistant = useSetRecoilState(showAIassistantState); + + return ( + + + {t('ai-assistant.name')} + + +
+
+ + } + > + + + + + + + + + +
+ ); +} diff --git a/src/components/AIassistant/components/AIassistant.scss b/src/components/AIassistant/components/AIassistant.scss new file mode 100644 index 0000000000..ebe82658bb --- /dev/null +++ b/src/components/AIassistant/components/AIassistant.scss @@ -0,0 +1,21 @@ +.ai_assistant { + height: 100%; + width: 100%; + + &__header { + background-color: rgb(85, 68, 245); + min-height: 60px; + padding-left: 8px; + padding-right: 8px; + + .title { + color: white; + text-shadow: none; + } + + .action { + color: white; + background: transparent; + } + } +} diff --git a/src/components/AIassistant/components/Chat.js b/src/components/AIassistant/components/Chat.js new file mode 100644 index 0000000000..46770ea2af --- /dev/null +++ b/src/components/AIassistant/components/Chat.js @@ -0,0 +1,5 @@ +import './Chat.scss'; + +export default function Chat() { + return <>; +} diff --git a/src/components/AIassistant/components/Chat.scss b/src/components/AIassistant/components/Chat.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/components/AIassistant/components/IntroBox.js b/src/components/AIassistant/components/IntroBox.js new file mode 100644 index 0000000000..110a8b0a0d --- /dev/null +++ b/src/components/AIassistant/components/IntroBox.js @@ -0,0 +1,22 @@ +import { useTranslation } from 'react-i18next'; +import { FlexBox, Icon, Text } from '@ui5/webcomponents-react'; +import './IntroBox.scss'; + +export default function IntroBox() { + const { t } = useTranslation(); + return ( + +
+ +
+
+ + {t('ai-assistant.introduction1')} + + + {t('ai-assistant.introduction2')} + +
+
+ ); +} diff --git a/src/components/AIassistant/components/IntroBox.scss b/src/components/AIassistant/components/IntroBox.scss new file mode 100644 index 0000000000..5ac08491bf --- /dev/null +++ b/src/components/AIassistant/components/IntroBox.scss @@ -0,0 +1,40 @@ +.intro-box { + height: 240px; + width: 100%; + background: linear-gradient(to bottom, rgb(85, 68, 245), rgb(119, 71, 243)); + + .illustration { + height: 175px; + display: flex; + align-items: center; + justify-content: center; + + .joule-icon { + color: white; + width: 75px; + height: 75px; + } + } + + .introduction { + height: 65px; + padding: 1rem; + display: flex; + flex-direction: column; + justify-content: end; + gap: 0.5rem; + + .text { + color: white; + font-weight: lighter; + } + + #text1 { + font-size: 16px; + } + + #text2 { + font-size: 28px; + } + } +} diff --git a/src/components/AIassistant/components/PageInsights.js b/src/components/AIassistant/components/PageInsights.js new file mode 100644 index 0000000000..eb74caec06 --- /dev/null +++ b/src/components/AIassistant/components/PageInsights.js @@ -0,0 +1,5 @@ +import './PageInsights.scss'; + +export default function PageInsights() { + return <>; +} diff --git a/src/components/AIassistant/components/PageInsights.scss b/src/components/AIassistant/components/PageInsights.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/components/AIassistant/state/showAIassistantAtom.ts b/src/components/AIassistant/state/showAIassistantAtom.ts new file mode 100644 index 0000000000..ac035680cf --- /dev/null +++ b/src/components/AIassistant/state/showAIassistantAtom.ts @@ -0,0 +1,12 @@ +import { atom, RecoilState } from 'recoil'; + +type ShowAIassistant = boolean; + +const DEFAULT_SHOW_AI_ASSISTANT = false; + +export const showAIassistantState: RecoilState = atom< + ShowAIassistant +>({ + key: 'showAIassistantState', + default: DEFAULT_SHOW_AI_ASSISTANT, +}); diff --git a/src/components/App/App.scss b/src/components/App/App.scss index 23c3596ddd..f4bda11d4e 100644 --- a/src/components/App/App.scss +++ b/src/components/App/App.scss @@ -17,3 +17,8 @@ min-height: 0; position: relative; } + +#assistant_wrapper { + padding: 0 1rem 1rem 0; + width: 100%; +} diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 4ded226789..27ba4b5058 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -34,6 +34,9 @@ import './App.scss'; import { useAfterInitHook } from 'state/useAfterInitHook'; import useSidebarCondensed from 'sidebar/useSidebarCondensed'; import { useGetValidationEnabledSchemas } from 'state/validationEnabledSchemasAtom'; +import { SplitterElement, SplitterLayout } from '@ui5/webcomponents-react'; +import { showAIassistantState } from 'components/AIassistant/state/showAIassistantAtom'; +import AIassistant from 'components/AIassistant/components/AIassistant'; export default function App() { const { t, i18n } = useTranslation(); @@ -68,37 +71,57 @@ export default function App() { useAppTracking(); useAfterInitHook(kubeconfigIdState); + const assistantOpen = useRecoilValue(showAIassistantState); + return ( -
-
-
- - - - + +
+
+
+ + + + + } + /> + + } /> + } + /> + } /> - } - /> - - } /> - } - /> - } - /> - {makeGardenerLoginRoute()} - - - -
-
+ {makeGardenerLoginRoute()} +
+ +
+
+
+ + {assistantOpen ? ( + +
+ +
+
+ ) : ( + <> + )} + ); } diff --git a/src/header/Header.tsx b/src/header/Header.tsx index b05973b282..ab086499a6 100644 --- a/src/header/Header.tsx +++ b/src/header/Header.tsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useState } from 'react'; +import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { Avatar, Menu, @@ -16,6 +16,7 @@ import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { useFeature } from 'hooks/useFeature'; import { showYamlUploadDialogState } from 'state/showYamlUploadDialogAtom'; +import { showAIassistantState } from 'components/AIassistant/state/showAIassistantAtom'; import { clustersState } from 'state/clustersAtom'; import { clusterState } from 'state/clusterAtom'; @@ -47,6 +48,9 @@ export function Header() { const setPreferencesOpen = useSetRecoilState(isPreferencesOpenState); const cluster = useRecoilValue(clusterState); const clusters = useRecoilValue(clustersState); + const [assistantOpen, setOpenAssistant] = useRecoilState( + showAIassistantState, + ); const setShowAdd = useSetRecoilState(showYamlUploadDialogState); const inactiveClusterNames = Object.keys(clusters || {}).filter( @@ -131,6 +135,13 @@ export function Header() { } onProfileClick={() => setIsMenuOpen(true)} > + { + setOpenAssistant(!assistantOpen); + }} + icon="da" + text={t('ai-assistant.name')} + /> {window.location.pathname !== '/clusters' && !window.location.pathname.endsWith('/no-permissions') && ( Date: Wed, 20 Mar 2024 15:50:48 +0100 Subject: [PATCH 02/38] feat: added PageInsights Tab components feat: added Bubbles and MessageWithList components feat: added ai-button to ResourceDetails fix: missing translation feat: add groq api and color adjustments feat: add error handling to chat UI feat: added automatic scrolling to ai chat feat: added custom input & code adjustments feat: connected with our API and added prompt suggestions on ClusterOverview --- public/i18n/en.yaml | 5 + .../AIassistant/components/AIassistant.js | 12 +- .../AIassistant/components/AIassistant.scss | 6 +- src/components/AIassistant/components/Chat.js | 5 - .../AIassistant/components/Chat.scss | 0 .../AIassistant/components/Chat/Chat.js | 117 ++++++++++++++++++ .../AIassistant/components/Chat/Chat.scss | 31 +++++ .../components/Chat/messages/Bubbles.js | 28 +++++ .../components/Chat/messages/Bubbles.scss | 14 +++ .../components/Chat/messages/ErrorMessage.js | 17 +++ .../Chat/messages/MessageWithList.js | 39 ++++++ .../Chat/messages/MessageWithList.scss | 15 +++ .../components/Chat/messages/PlainMessage.js | 10 ++ .../Chat/messages/PlainMessage.scss | 4 + .../AIassistant/components/IntroBox.scss | 12 +- .../AIassistant/components/PageInsights.js | 5 - .../AIassistant/components/PageInsights.scss | 0 .../components/PageInsights/InsightPanel.js | 51 ++++++++ .../components/PageInsights/InsightPanel.scss | 8 ++ .../components/PageInsights/PageInsights.js | 25 ++++ .../components/PageInsights/PageInsights.scss | 6 + .../AIassistant/utils/getChatResponse.js | 48 +++++++ .../AIassistant/utils/getPromptSuggestions.js | 30 +++++ .../views/ClusterOverview/ClusterOverview.js | 93 ++++++++++++-- .../ClusterOverview/ClusterOverview.scss | 11 ++ .../HelmReleases/HelmReleasesDetails.js | 14 ++- src/header/Header.tsx | 13 +- .../DynamicPageComponent.js | 4 +- .../ResourceDetails/ResourceDetails.js | 30 +++-- .../ResourceDetails/ResourceDetailsCard.js | 6 +- src/styles/index.scss | 4 + 31 files changed, 603 insertions(+), 60 deletions(-) delete mode 100644 src/components/AIassistant/components/Chat.js delete mode 100644 src/components/AIassistant/components/Chat.scss create mode 100644 src/components/AIassistant/components/Chat/Chat.js create mode 100644 src/components/AIassistant/components/Chat/Chat.scss create mode 100644 src/components/AIassistant/components/Chat/messages/Bubbles.js create mode 100644 src/components/AIassistant/components/Chat/messages/Bubbles.scss create mode 100644 src/components/AIassistant/components/Chat/messages/ErrorMessage.js create mode 100644 src/components/AIassistant/components/Chat/messages/MessageWithList.js create mode 100644 src/components/AIassistant/components/Chat/messages/MessageWithList.scss create mode 100644 src/components/AIassistant/components/Chat/messages/PlainMessage.js create mode 100644 src/components/AIassistant/components/Chat/messages/PlainMessage.scss delete mode 100644 src/components/AIassistant/components/PageInsights.js delete mode 100644 src/components/AIassistant/components/PageInsights.scss create mode 100644 src/components/AIassistant/components/PageInsights/InsightPanel.js create mode 100644 src/components/AIassistant/components/PageInsights/InsightPanel.scss create mode 100644 src/components/AIassistant/components/PageInsights/PageInsights.js create mode 100644 src/components/AIassistant/components/PageInsights/PageInsights.scss create mode 100644 src/components/AIassistant/utils/getChatResponse.js create mode 100644 src/components/AIassistant/utils/getPromptSuggestions.js diff --git a/public/i18n/en.yaml b/public/i18n/en.yaml index 5e130fb37c..84c6ce5bb4 100644 --- a/public/i18n/en.yaml +++ b/public/i18n/en.yaml @@ -5,11 +5,16 @@ apps: title: Apps ai-assistant: name: Joule + 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 + use-ai: Use AI cluster-overview: headers: cpu: CPU diff --git a/src/components/AIassistant/components/AIassistant.js b/src/components/AIassistant/components/AIassistant.js index 7ae17633cd..5471d7a13f 100644 --- a/src/components/AIassistant/components/AIassistant.js +++ b/src/components/AIassistant/components/AIassistant.js @@ -11,9 +11,8 @@ import { import { spacing } from '@ui5/webcomponents-react-base'; import { useSetRecoilState } from 'recoil'; import { showAIassistantState } from 'components/AIassistant/state/showAIassistantAtom'; -import IntroBox from './IntroBox'; -import Chat from './Chat'; -import PageInsights from './PageInsights'; +import Chat from './Chat/Chat'; +import PageInsights from './PageInsights/PageInsights'; import './AIassistant.scss'; export default function AIassistant() { @@ -48,8 +47,11 @@ export default function AIassistant() { } > - - + diff --git a/src/components/AIassistant/components/AIassistant.scss b/src/components/AIassistant/components/AIassistant.scss index ebe82658bb..8f9859048f 100644 --- a/src/components/AIassistant/components/AIassistant.scss +++ b/src/components/AIassistant/components/AIassistant.scss @@ -3,7 +3,7 @@ width: 100%; &__header { - background-color: rgb(85, 68, 245); + background-color: var(--sapContent_Illustrative_Color1); min-height: 60px; padding-left: 8px; padding-right: 8px; @@ -18,4 +18,8 @@ background: transparent; } } + + .tab-container { + height: calc(100vh - 60px - 1.4rem); + } } diff --git a/src/components/AIassistant/components/Chat.js b/src/components/AIassistant/components/Chat.js deleted file mode 100644 index 46770ea2af..0000000000 --- a/src/components/AIassistant/components/Chat.js +++ /dev/null @@ -1,5 +0,0 @@ -import './Chat.scss'; - -export default function Chat() { - return <>; -} diff --git a/src/components/AIassistant/components/Chat.scss b/src/components/AIassistant/components/Chat.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/components/AIassistant/components/Chat/Chat.js b/src/components/AIassistant/components/Chat/Chat.js new file mode 100644 index 0000000000..ad3bb40496 --- /dev/null +++ b/src/components/AIassistant/components/Chat/Chat.js @@ -0,0 +1,117 @@ +import { useEffect, useRef, useState } from 'react'; +import { FlexBox, Icon, Input } from '@ui5/webcomponents-react'; +import { spacing } from '@ui5/webcomponents-react-base'; +import PlainMessage from './messages/PlainMessage'; +import Bubbles from './messages/Bubbles'; +import ErrorMessage from './messages/ErrorMessage'; +import { useTranslation } from 'react-i18next'; +import getChatResponse from 'components/AIassistant/utils/getChatResponse'; +import './Chat.scss'; + +export default function Chat() { + const { t } = useTranslation(); + const containerRef = useRef(null); + const [inputValue, setInputValue] = useState(''); + const [chatHistory, setChatHistory] = useState([]); + const [errorOccured, setErrorOccured] = useState(false); + const addMessage = (author, message) => { + setChatHistory(prevItems => [...prevItems, { author, message }]); + }; + const handleSuccess = response => { + addMessage('ai', response); + }; + const handleError = () => { + setErrorOccured(true); + setChatHistory(prevItems => prevItems.slice(0, -1)); + }; + const onClickBubble = prompt => { + setErrorOccured(false); + addMessage('user', prompt); + return getChatResponse(prompt, handleSuccess, handleError); + }; + const onSubmitInput = () => { + const prompt = inputValue; + setInputValue(''); + setErrorOccured(false); + addMessage('user', prompt); + return getChatResponse(prompt, handleSuccess, handleError); + }; + + const scrollToBottom = () => { + if (containerRef?.current?.lastChild) + containerRef.current.lastChild.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + }; + + useEffect(() => { + const delay = errorOccured ? 500 : 0; + setTimeout(() => { + scrollToBottom(); + }, delay); + }, [chatHistory, errorOccured]); + + return ( + +
+ + {chatHistory.map((message, index) => { + return message.author === 'ai' ? ( + <> + + + + ) : ( + + ); + })} + {errorOccured && } +
+
+ } + value={inputValue} + onKeyDown={e => { + if (e.key === 'Enter' && inputValue.length > 0) onSubmitInput(); + }} + onInput={e => setInputValue(e.target.value)} + placeholder={t('ai-assistant.placeholder')} + /> +
+
+ ); +} diff --git a/src/components/AIassistant/components/Chat/Chat.scss b/src/components/AIassistant/components/Chat/Chat.scss new file mode 100644 index 0000000000..a0e7395d36 --- /dev/null +++ b/src/components/AIassistant/components/Chat/Chat.scss @@ -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; + } + } + } +} diff --git a/src/components/AIassistant/components/Chat/messages/Bubbles.js b/src/components/AIassistant/components/Chat/messages/Bubbles.js new file mode 100644 index 0000000000..cf5da864e8 --- /dev/null +++ b/src/components/AIassistant/components/Chat/messages/Bubbles.js @@ -0,0 +1,28 @@ +import { Button, FlexBox } from '@ui5/webcomponents-react'; +import { useState } from 'react'; +import './Bubbles.scss'; + +export default function Bubbles({ suggestions, onClick }) { + const [clicked, setClicked] = useState(false); + + return clicked ? ( + <> + ) : ( + + {suggestions.map((suggestion, index) => ( + + ))} + + ); +} diff --git a/src/components/AIassistant/components/Chat/messages/Bubbles.scss b/src/components/AIassistant/components/Chat/messages/Bubbles.scss new file mode 100644 index 0000000000..1a2ad9540e --- /dev/null +++ b/src/components/AIassistant/components/Chat/messages/Bubbles.scss @@ -0,0 +1,14 @@ +.bubbles-container { + max-width: 275px; + gap: 8px; + + .bubble-button { + align-self: flex-start; + color: var(--sapContent_Illustrative_Color1); + } + + .bubble-button:hover { + background-color: var(--sapBackgroundColor1); + border-color: var(--sapContent_Illustrative_Color1); + } +} diff --git a/src/components/AIassistant/components/Chat/messages/ErrorMessage.js b/src/components/AIassistant/components/Chat/messages/ErrorMessage.js new file mode 100644 index 0000000000..a2b3e4e7aa --- /dev/null +++ b/src/components/AIassistant/components/Chat/messages/ErrorMessage.js @@ -0,0 +1,17 @@ +import { IllustratedMessage } from '@ui5/webcomponents-react'; +import { useTranslation } from 'react-i18next'; +import { spacing } from '@ui5/webcomponents-react-base'; + +export default function ErrorMessage() { + const { t } = useTranslation(); + + return ( + + ); +} diff --git a/src/components/AIassistant/components/Chat/messages/MessageWithList.js b/src/components/AIassistant/components/Chat/messages/MessageWithList.js new file mode 100644 index 0000000000..8d3e045546 --- /dev/null +++ b/src/components/AIassistant/components/Chat/messages/MessageWithList.js @@ -0,0 +1,39 @@ +import { + CustomListItem, + FlexBox, + Icon, + List, + Text, +} from '@ui5/webcomponents-react'; +import { spacing } from '@ui5/webcomponents-react-base'; +import './MessageWithList.scss'; + +export default function MessageWithList({ + className, + message, + items, + appendix, +}) { + return ( +
+ {message} +
+ + {items.map((item, index) => ( + + + {item} + + + + ))} + +
+ {appendix} +
+ ); +} diff --git a/src/components/AIassistant/components/Chat/messages/MessageWithList.scss b/src/components/AIassistant/components/Chat/messages/MessageWithList.scss new file mode 100644 index 0000000000..b2bdbd3c41 --- /dev/null +++ b/src/components/AIassistant/components/Chat/messages/MessageWithList.scss @@ -0,0 +1,15 @@ +.message-with-list { + max-width: 225px; + padding: 12px; + + .list-item-content { + width: 100%; + + .text { + max-width: 85%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } +} diff --git a/src/components/AIassistant/components/Chat/messages/PlainMessage.js b/src/components/AIassistant/components/Chat/messages/PlainMessage.js new file mode 100644 index 0000000000..0351306f52 --- /dev/null +++ b/src/components/AIassistant/components/Chat/messages/PlainMessage.js @@ -0,0 +1,10 @@ +import { Text } from '@ui5/webcomponents-react'; +import './PlainMessage.scss'; + +export default function PlainMessage({ className, message }) { + return ( +
+ {message} +
+ ); +} diff --git a/src/components/AIassistant/components/Chat/messages/PlainMessage.scss b/src/components/AIassistant/components/Chat/messages/PlainMessage.scss new file mode 100644 index 0000000000..df378de169 --- /dev/null +++ b/src/components/AIassistant/components/Chat/messages/PlainMessage.scss @@ -0,0 +1,4 @@ +.plain-message { + max-width: 225px; + padding: 12px; +} diff --git a/src/components/AIassistant/components/IntroBox.scss b/src/components/AIassistant/components/IntroBox.scss index 5ac08491bf..bc2325fecd 100644 --- a/src/components/AIassistant/components/IntroBox.scss +++ b/src/components/AIassistant/components/IntroBox.scss @@ -1,10 +1,14 @@ .intro-box { - height: 240px; + height: 300px; width: 100%; - background: linear-gradient(to bottom, rgb(85, 68, 245), rgb(119, 71, 243)); + background: linear-gradient( + to bottom, + var(--sapContent_Illustrative_Color1), + #a100c2 + ); .illustration { - height: 175px; + height: 235px; display: flex; align-items: center; justify-content: center; @@ -21,7 +25,7 @@ padding: 1rem; display: flex; flex-direction: column; - justify-content: end; + justify-content: flex-end; gap: 0.5rem; .text { diff --git a/src/components/AIassistant/components/PageInsights.js b/src/components/AIassistant/components/PageInsights.js deleted file mode 100644 index eb74caec06..0000000000 --- a/src/components/AIassistant/components/PageInsights.js +++ /dev/null @@ -1,5 +0,0 @@ -import './PageInsights.scss'; - -export default function PageInsights() { - return <>; -} diff --git a/src/components/AIassistant/components/PageInsights.scss b/src/components/AIassistant/components/PageInsights.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/components/AIassistant/components/PageInsights/InsightPanel.js b/src/components/AIassistant/components/PageInsights/InsightPanel.js new file mode 100644 index 0000000000..36be621e17 --- /dev/null +++ b/src/components/AIassistant/components/PageInsights/InsightPanel.js @@ -0,0 +1,51 @@ +import { + ObjectStatus, + Panel, + Text, + Title, + Toolbar, + ToolbarSpacer, +} from '@ui5/webcomponents-react'; +import { useState } from 'react'; +import './InsightPanel.scss'; + +export default function InsightPanel({ resourceType, resourceName, status }) { + const [open, setOpen] = useState(true); + const toggle = () => { + setOpen(!open); + }; + + return ( + + + {resourceType + ' ' + resourceName} + + {status && ( + <> + + + + )} + + } + > + + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy + eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam + voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet + clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit + amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam + nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, + sed diam voluptua. At vero eos et accusam et justo duo dolores et ea + rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem + ipsum dolor sit amet. + + + ); +} diff --git a/src/components/AIassistant/components/PageInsights/InsightPanel.scss b/src/components/AIassistant/components/PageInsights/InsightPanel.scss new file mode 100644 index 0000000000..fa9e24059f --- /dev/null +++ b/src/components/AIassistant/components/PageInsights/InsightPanel.scss @@ -0,0 +1,8 @@ +.insight-panel { + .toolbar-title { + max-width: 200px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} diff --git a/src/components/AIassistant/components/PageInsights/PageInsights.js b/src/components/AIassistant/components/PageInsights/PageInsights.js new file mode 100644 index 0000000000..eb4f8bba99 --- /dev/null +++ b/src/components/AIassistant/components/PageInsights/PageInsights.js @@ -0,0 +1,25 @@ +import { spacing } from '@ui5/webcomponents-react-base'; +import InsightPanel from './InsightPanel'; +import './PageInsights.scss'; + +export default function PageInsights() { + return ( +
+ + + +
+ ); +} diff --git a/src/components/AIassistant/components/PageInsights/PageInsights.scss b/src/components/AIassistant/components/PageInsights/PageInsights.scss new file mode 100644 index 0000000000..685c99598b --- /dev/null +++ b/src/components/AIassistant/components/PageInsights/PageInsights.scss @@ -0,0 +1,6 @@ +.page-insights-list { + display: flex; + flex-direction: column; + gap: 8px; + padding: 0.5rem 0 0.5rem 0; +} diff --git a/src/components/AIassistant/utils/getChatResponse.js b/src/components/AIassistant/utils/getChatResponse.js new file mode 100644 index 0000000000..e441e85faa --- /dev/null +++ b/src/components/AIassistant/utils/getChatResponse.js @@ -0,0 +1,48 @@ +function delay() { + return new Promise(resolve => setTimeout(resolve, 3000)); +} + +export default async function getChatResponse( + prompt, + handleSuccess, + handleError, +) { + try { + if (prompt === 'Throw an error') { + await delay(); + throw new Error('This is a custom error message.'); + } + + const { response } = await fetch( + 'https://api-backend.c-5cb6076.stage.kyma.ondemand.com/api/v1/llm', + { + headers: { + accept: 'application/json, text/plain, */*', + 'accept-language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7', + 'cache-control': 'no-cache', + 'content-type': 'application/json', + pragma: 'no-cache', + 'sec-ch-ua': + '"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"macOS"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'cross-site', + }, + referrer: 'https://ai.kyma.dev.sap/', + referrerPolicy: 'strict-origin-when-cross-origin', + body: `{"question":"${prompt}"}`, + method: 'POST', + mode: 'cors', + credentials: 'omit', + }, + ).then(result => result.json()); + handleSuccess(response); + return true; + } catch (error) { + handleError(); + console.error('Error fetching data:', error); + return false; + } +} diff --git a/src/components/AIassistant/utils/getPromptSuggestions.js b/src/components/AIassistant/utils/getPromptSuggestions.js new file mode 100644 index 0000000000..968987e168 --- /dev/null +++ b/src/components/AIassistant/utils/getPromptSuggestions.js @@ -0,0 +1,30 @@ +export default async function getPromptSuggestions( + pageType = 'statefulsets.apps', + namespace = 'kyma-system', + nodeName = '', +) { + let { results } = await fetch( + 'https://api-backend.c-5cb6076.stage.kyma.ondemand.com/api/v1/llm/init', + { + headers: { + accept: 'application/json, text/plain, */*', + 'accept-language': 'en-US,en;q=0.9,de;q=0.8', + 'content-type': 'application/json', + 'sec-ch-ua': + '"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"macOS"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'cross-site', + }, + referrer: 'https://ai.kyma.dev.sap/', + referrerPolicy: 'strict-origin-when-cross-origin', + body: `{"page_type":"${pageType}","namespace":"${namespace}","node_name":"${nodeName}"}`, + method: 'POST', + mode: 'cors', + credentials: 'omit', + }, + ).then(result => result.json()); + return results; +} diff --git a/src/components/Clusters/views/ClusterOverview/ClusterOverview.js b/src/components/Clusters/views/ClusterOverview/ClusterOverview.js index 56cccfa92f..5f83766e2f 100644 --- a/src/components/Clusters/views/ClusterOverview/ClusterOverview.js +++ b/src/components/Clusters/views/ClusterOverview/ClusterOverview.js @@ -1,5 +1,15 @@ -import React from 'react'; -import { Button, Title } from '@ui5/webcomponents-react'; +import React, { useState } from 'react'; +import { + Button, + CustomListItem, + FlexBox, + Icon, + Input, + List, + Popover, + Text, + Title, +} from '@ui5/webcomponents-react'; import { ClusterNodes } from './ClusterNodes'; import { ClusterValidation } from './ClusterValidation/ClusterValidation'; import { useFeature } from 'hooks/useFeature'; @@ -17,6 +27,9 @@ import { useNavigate } from 'react-router-dom'; import { deleteCluster } from 'components/Clusters/shared'; import { spacing } from '@ui5/webcomponents-react-base'; import './ClusterOverview.scss'; +import { useSetRecoilState } from 'recoil'; +import { showAIassistantState } from 'components/AIassistant/state/showAIassistantAtom'; +import getPromptSuggestions from 'components/AIassistant/utils/getPromptSuggestions'; const Injections = React.lazy(() => import('../../../Extensibility/ExtensibilityInjections'), @@ -33,6 +46,9 @@ export function ClusterOverview() { const [DeleteMessageBox, handleResourceDelete] = useDeleteResource({ resourceType: t('clusters.labels.name'), }); + const setOpenAssistant = useSetRecoilState(showAIassistantState); + const [popoverOpen, setPopoverOpen] = useState(false); + const [suggestions, setSuggestions] = useState([]); const actions = ( + {}} />} + value={''} + onKeyDown={e => {}} + onInput={e => {}} + placeholder="Ask about this cluster" + /> + } + onAfterClose={() => setPopoverOpen(false)} + opener="openPopoverBtn" + placementType="Bottom" + horizontalAlign="Right" + > + {'Suggestions'} + + + {suggestions.map((suggestion, index) => ( + { + setPopoverOpen(false); + setOpenAssistant(true); + }} + > + + {suggestion} + + + + ))} + + + + + {data && } diff --git a/src/components/Clusters/views/ClusterOverview/ClusterOverview.scss b/src/components/Clusters/views/ClusterOverview/ClusterOverview.scss index d8b334a441..23050fd7db 100644 --- a/src/components/Clusters/views/ClusterOverview/ClusterOverview.scss +++ b/src/components/Clusters/views/ClusterOverview/ClusterOverview.scss @@ -1,3 +1,14 @@ .gardener-provider { text-transform: uppercase; } + +.list-item-content { + width: 100%; + + .text { + max-width: 85%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} diff --git a/src/components/HelmReleases/HelmReleasesDetails.js b/src/components/HelmReleases/HelmReleasesDetails.js index dd34cb23c4..5ededf7acd 100644 --- a/src/components/HelmReleases/HelmReleasesDetails.js +++ b/src/components/HelmReleases/HelmReleasesDetails.js @@ -8,17 +8,20 @@ import { HelmReleaseData } from './HelmReleaseData'; import { HelmReleaseStatus } from './HelmReleaseStatus'; import { OtherReleaseVersions } from './OtherReleaseVersions'; import { findRecentRelease } from './findRecentRelease'; -import { useRecoilValue } from 'recoil'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { activeNamespaceIdState } from 'state/activeNamespaceIdAtom'; import { useUrl } from 'hooks/useUrl'; import YamlUploadDialog from 'resources/Namespaces/YamlUpload/YamlUploadDialog'; import { ResourceDescription } from 'components/HelmReleases'; import { Link } from 'shared/components/Link/Link'; +import { Button } from '@ui5/webcomponents-react'; +import { showAIassistantState } from 'components/AIassistant/state/showAIassistantAtom'; function HelmReleasesDetails({ releaseName }) { const { t } = useTranslation(); const { namespaceUrl } = useUrl(); + const setOpenAssistant = useSetRecoilState(showAIassistantState); const namespace = useRecoilValue(activeNamespaceIdState); const breadcrumbItems = [ { name: t('helm-releases.title'), url: namespaceUrl('helm-releases') }, @@ -79,6 +82,15 @@ function HelmReleasesDetails({ releaseName }) { status={releaseSecret.metadata.labels.status} /> + + + )} diff --git a/src/header/Header.tsx b/src/header/Header.tsx index ab086499a6..42fe0bdedd 100644 --- a/src/header/Header.tsx +++ b/src/header/Header.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { Avatar, Menu, @@ -16,7 +16,6 @@ import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { useFeature } from 'hooks/useFeature'; import { showYamlUploadDialogState } from 'state/showYamlUploadDialogAtom'; -import { showAIassistantState } from 'components/AIassistant/state/showAIassistantAtom'; import { clustersState } from 'state/clustersAtom'; import { clusterState } from 'state/clusterAtom'; @@ -48,9 +47,6 @@ export function Header() { const setPreferencesOpen = useSetRecoilState(isPreferencesOpenState); const cluster = useRecoilValue(clusterState); const clusters = useRecoilValue(clustersState); - const [assistantOpen, setOpenAssistant] = useRecoilState( - showAIassistantState, - ); const setShowAdd = useSetRecoilState(showYamlUploadDialogState); const inactiveClusterNames = Object.keys(clusters || {}).filter( @@ -135,13 +131,6 @@ export function Header() { } onProfileClick={() => setIsMenuOpen(true)} > - { - setOpenAssistant(!assistantOpen); - }} - icon="da" - text={t('ai-assistant.name')} - /> {window.location.pathname !== '/clusters' && !window.location.pathname.endsWith('/no-permissions') && ( {
{image &&
{image}
}
-
{title}:
+ {title && ( +
{title + ':'}
+ )} {children}
diff --git a/src/shared/components/ResourceDetails/ResourceDetails.js b/src/shared/components/ResourceDetails/ResourceDetails.js index 04a7e07355..e381587e12 100644 --- a/src/shared/components/ResourceDetails/ResourceDetails.js +++ b/src/shared/components/ResourceDetails/ResourceDetails.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import jsyaml from 'js-yaml'; import pluralize from 'pluralize'; import { useTranslation } from 'react-i18next'; -import { Button, Title } from '@ui5/webcomponents-react'; +import { Button, FlexBox, Title } from '@ui5/webcomponents-react'; import { spacing } from '@ui5/webcomponents-react-base'; import { createPatch } from 'rfc6902'; @@ -40,9 +40,10 @@ import { ResourceStatusCard } from '../ResourceStatusCard/ResourceStatusCard'; import { EMPTY_TEXT_PLACEHOLDER } from '../../constants'; import { ReadableElapsedTimeFromNow } from '../ReadableElapsedTimeFromNow/ReadableElapsedTimeFromNow'; import { HintButton } from '../DescriptionHint/DescriptionHint'; -import { useRecoilValue } from 'recoil'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useFeature } from 'hooks/useFeature'; import { columnLayoutState } from 'state/columnLayoutAtom'; +import { showAIassistantState } from 'components/AIassistant/state/showAIassistantAtom'; // This component is loaded after the page mounts. // Don't try to load it on scroll. It was tested. @@ -198,6 +199,7 @@ function Resource({ ); const [toggleFormFn, getToggleFormFn] = useState(() => {}); const [showTitleDescription, setShowTitleDescription] = useState(false); + const setOpenAssistant = useSetRecoilState(showAIassistantState); const pluralizedResourceKind = pluralize(prettifiedResourceKind); useWindowTitle(windowTitle || pluralizedResourceKind); @@ -415,7 +417,6 @@ function Resource({ const resourceDetailsCard = ( , document.body, )} - - {title ?? t('common.headers.resource-details')} - + + {title ?? t('common.headers.resource-details')} + + +
Date: Tue, 2 Apr 2024 12:29:49 +0200 Subject: [PATCH 03/38] feat: added loading indicator & connected initial suggestions with chat api --- .../AIassistant/components/AIOpener.js | 88 +++++++++++++++++++ .../AIassistant/components/AIOpener.scss | 14 +++ .../AIassistant/components/Chat/Chat.js | 71 ++++++++------- .../components/Chat/messages/Bubbles.js | 13 +-- .../components/Chat/messages/PlainMessage.js | 7 +- .../AIassistant/state/initalPromptAtom.ts | 12 +++ .../AIassistant/utils/getChatResponse.js | 2 - .../views/ClusterOverview/ClusterOverview.js | 79 +---------------- .../ClusterOverview/ClusterOverview.scss | 11 --- .../HelmReleases/HelmReleasesDetails.js | 14 +-- .../ResourceDetails/ResourceDetails.js | 13 +-- src/styles/index.scss | 4 - 12 files changed, 170 insertions(+), 158 deletions(-) create mode 100644 src/components/AIassistant/components/AIOpener.js create mode 100644 src/components/AIassistant/components/AIOpener.scss create mode 100644 src/components/AIassistant/state/initalPromptAtom.ts diff --git a/src/components/AIassistant/components/AIOpener.js b/src/components/AIassistant/components/AIOpener.js new file mode 100644 index 0000000000..b400b405b5 --- /dev/null +++ b/src/components/AIassistant/components/AIOpener.js @@ -0,0 +1,88 @@ +import { + Button, + CustomListItem, + FlexBox, + Icon, + Input, + List, + Popover, + Text, + Title, +} from '@ui5/webcomponents-react'; +import { spacing } from '@ui5/webcomponents-react-base'; +import { useTranslation } from 'react-i18next'; +import { useState } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { showAIassistantState } from 'components/AIassistant/state/showAIassistantAtom'; +import { initialPromptState } from '../state/initalPromptAtom'; +import getPromptSuggestions from 'components/AIassistant/utils/getPromptSuggestions'; +import './AIOpener.scss'; + +export default function AIOpener() { + const { t } = useTranslation(); + const setOpenAssistant = useSetRecoilState(showAIassistantState); + const setInitialPrompt = useSetRecoilState(initialPromptState); + const [popoverOpen, setPopoverOpen] = useState(false); + const [suggestions, setSuggestions] = useState([]); + + return ( + <> + + {}} />} + value={''} + onKeyDown={e => {}} + onInput={e => {}} + placeholder="Ask about this cluster" + /> + } + onAfterClose={() => setPopoverOpen(false)} + opener="openPopoverBtn" + placementType="Bottom" + horizontalAlign="Right" + > + {'Suggestions'} + + + {suggestions.map((suggestion, index) => ( + { + setInitialPrompt(suggestion); + setPopoverOpen(false); + setOpenAssistant(true); + }} + > + + {suggestion} + + + + ))} + + + + + ); +} diff --git a/src/components/AIassistant/components/AIOpener.scss b/src/components/AIassistant/components/AIOpener.scss new file mode 100644 index 0000000000..c85d271016 --- /dev/null +++ b/src/components/AIassistant/components/AIOpener.scss @@ -0,0 +1,14 @@ +.ai-button { + color: var(--sapContent_Illustrative_Color1); +} + +.list-item-content { + width: 100%; + + .text { + max-width: 85%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} diff --git a/src/components/AIassistant/components/Chat/Chat.js b/src/components/AIassistant/components/Chat/Chat.js index ad3bb40496..5dc520a518 100644 --- a/src/components/AIassistant/components/Chat/Chat.js +++ b/src/components/AIassistant/components/Chat/Chat.js @@ -1,10 +1,12 @@ +import { useTranslation } from 'react-i18next'; import { useEffect, useRef, useState } from 'react'; +import { useRecoilValue } from 'recoil'; import { FlexBox, Icon, Input } from '@ui5/webcomponents-react'; import { spacing } from '@ui5/webcomponents-react-base'; +import { initialPromptState } from 'components/AIassistant/state/initalPromptAtom'; import PlainMessage from './messages/PlainMessage'; import Bubbles from './messages/Bubbles'; import ErrorMessage from './messages/ErrorMessage'; -import { useTranslation } from 'react-i18next'; import getChatResponse from 'components/AIassistant/utils/getChatResponse'; import './Chat.scss'; @@ -14,27 +16,35 @@ export default function Chat() { const [inputValue, setInputValue] = useState(''); const [chatHistory, setChatHistory] = useState([]); const [errorOccured, setErrorOccured] = useState(false); - const addMessage = (author, message) => { - setChatHistory(prevItems => [...prevItems, { author, message }]); + const initialPrompt = useRecoilValue(initialPromptState); + const addMessage = (author, message, isLoading) => { + setChatHistory(prevItems => [...prevItems, { author, message, isLoading }]); }; const handleSuccess = response => { - addMessage('ai', response); + setChatHistory(prevItems => { + const newArray = [...prevItems]; + newArray[newArray.length - 1] = { + author: 'ai', + message: response, + isLoading: false, + }; + return newArray; + }); }; const handleError = () => { setErrorOccured(true); - setChatHistory(prevItems => prevItems.slice(0, -1)); + setChatHistory(prevItems => prevItems.slice(0, -2)); }; - const onClickBubble = prompt => { + const onSendPrompt = prompt => { setErrorOccured(false); - addMessage('user', prompt); - return getChatResponse(prompt, handleSuccess, handleError); + addMessage('user', prompt, false); + getChatResponse(prompt, handleSuccess, handleError); + addMessage('ai', null, true); }; const onSubmitInput = () => { const prompt = inputValue; setInputValue(''); - setErrorOccured(false); - addMessage('user', prompt); - return getChatResponse(prompt, handleSuccess, handleError); + onSendPrompt(prompt); }; const scrollToBottom = () => { @@ -45,6 +55,11 @@ export default function Chat() { }); }; + useEffect(() => { + if (chatHistory.length === 0) onSendPrompt(initialPrompt); + // eslint-disable-next-line + }, []); + useEffect(() => { const delay = errorOccured ? 500 : 0; setTimeout(() => { @@ -52,6 +67,12 @@ export default function Chat() { }, delay); }, [chatHistory, errorOccured]); + const test_suggestions = [ + 'test123123123123123xyzxyzuwquxzytsabcde123456', + 'Throw an error', + 'What is your favorite football team?', + ]; + return ( - {chatHistory.map((message, index) => { return message.author === 'ai' ? ( <> - + {index === chatHistory.length - 1 && !message.isLoading && ( + + )} ) : ( - ) : ( + return ( {suggestions.map((suggestion, index) => ( diff --git a/src/components/AIassistant/components/Chat/messages/PlainMessage.js b/src/components/AIassistant/components/Chat/messages/PlainMessage.js index 0351306f52..30b4855f65 100644 --- a/src/components/AIassistant/components/Chat/messages/PlainMessage.js +++ b/src/components/AIassistant/components/Chat/messages/PlainMessage.js @@ -1,10 +1,11 @@ -import { Text } from '@ui5/webcomponents-react'; +import { BusyIndicator, Text } from '@ui5/webcomponents-react'; import './PlainMessage.scss'; -export default function PlainMessage({ className, message }) { +export default function PlainMessage({ className, message, isLoading }) { return (
- {message} + {isLoading && } + {message && {message}}
); } diff --git a/src/components/AIassistant/state/initalPromptAtom.ts b/src/components/AIassistant/state/initalPromptAtom.ts new file mode 100644 index 0000000000..863e3e63c6 --- /dev/null +++ b/src/components/AIassistant/state/initalPromptAtom.ts @@ -0,0 +1,12 @@ +import { atom, RecoilState } from 'recoil'; + +type InitalPrompt = string; + +const DEFAULT_INITIAL_PROMPT = ''; + +export const initialPromptState: RecoilState = atom( + { + key: 'initialPromptState', + default: DEFAULT_INITIAL_PROMPT, + }, +); diff --git a/src/components/AIassistant/utils/getChatResponse.js b/src/components/AIassistant/utils/getChatResponse.js index e441e85faa..76f39f6a58 100644 --- a/src/components/AIassistant/utils/getChatResponse.js +++ b/src/components/AIassistant/utils/getChatResponse.js @@ -39,10 +39,8 @@ export default async function getChatResponse( }, ).then(result => result.json()); handleSuccess(response); - return true; } catch (error) { handleError(); console.error('Error fetching data:', error); - return false; } } diff --git a/src/components/Clusters/views/ClusterOverview/ClusterOverview.js b/src/components/Clusters/views/ClusterOverview/ClusterOverview.js index cdf1c05491..9bccfa789e 100644 --- a/src/components/Clusters/views/ClusterOverview/ClusterOverview.js +++ b/src/components/Clusters/views/ClusterOverview/ClusterOverview.js @@ -1,15 +1,5 @@ -import React, { useState } from 'react'; -import { - Button, - CustomListItem, - FlexBox, - Icon, - Input, - List, - Popover, - Text, - Title, -} from '@ui5/webcomponents-react'; +import React from 'react'; +import { Button, FlexBox, Title } from '@ui5/webcomponents-react'; import { ClusterNodes } from './ClusterNodes'; import { ClusterValidation } from './ClusterValidation/ClusterValidation'; import { useFeature } from 'hooks/useFeature'; @@ -27,9 +17,7 @@ import { useNavigate } from 'react-router-dom'; import { deleteCluster } from 'components/Clusters/shared'; import { spacing } from '@ui5/webcomponents-react-base'; import './ClusterOverview.scss'; -import { useSetRecoilState } from 'recoil'; -import { showAIassistantState } from 'components/AIassistant/state/showAIassistantAtom'; -import getPromptSuggestions from 'components/AIassistant/utils/getPromptSuggestions'; +import AIOpener from 'components/AIassistant/components/AIOpener'; const Injections = React.lazy(() => import('../../../Extensibility/ExtensibilityInjections'), @@ -46,9 +34,6 @@ export function ClusterOverview() { const [DeleteMessageBox, handleResourceDelete] = useDeleteResource({ resourceType: t('clusters.labels.name'), }); - const setOpenAssistant = useSetRecoilState(showAIassistantState); - const [popoverOpen, setPopoverOpen] = useState(false); - const [suggestions, setSuggestions] = useState([]); const actions = ( - {}} />} - value={''} - onKeyDown={e => {}} - onInput={e => {}} - placeholder="Ask about this cluster" - /> - } - onAfterClose={() => setPopoverOpen(false)} - opener="openPopoverBtn" - placementType="Bottom" - horizontalAlign="Right" - > - {'Suggestions'} - - - {suggestions.map((suggestion, index) => ( - { - setPopoverOpen(false); - setOpenAssistant(true); - }} - > - - {suggestion} - - - - ))} - - - - +
{data && } diff --git a/src/components/Clusters/views/ClusterOverview/ClusterOverview.scss b/src/components/Clusters/views/ClusterOverview/ClusterOverview.scss index 23050fd7db..d8b334a441 100644 --- a/src/components/Clusters/views/ClusterOverview/ClusterOverview.scss +++ b/src/components/Clusters/views/ClusterOverview/ClusterOverview.scss @@ -1,14 +1,3 @@ .gardener-provider { text-transform: uppercase; } - -.list-item-content { - width: 100%; - - .text { - max-width: 85%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } -} diff --git a/src/components/HelmReleases/HelmReleasesDetails.js b/src/components/HelmReleases/HelmReleasesDetails.js index 872004a063..5eb2db2c73 100644 --- a/src/components/HelmReleases/HelmReleasesDetails.js +++ b/src/components/HelmReleases/HelmReleasesDetails.js @@ -8,25 +8,21 @@ import { HelmReleaseData } from './HelmReleaseData'; import { HelmReleaseStatus } from './HelmReleaseStatus'; import { OtherReleaseVersions } from './OtherReleaseVersions'; import { findRecentRelease } from './findRecentRelease'; -import { useSetRecoilState } from 'recoil'; import { ResourceCreate } from 'shared/components/ResourceCreate/ResourceCreate'; import { useUrl } from 'hooks/useUrl'; import YamlUploadDialog from 'resources/Namespaces/YamlUpload/YamlUploadDialog'; +import AIOpener from 'components/AIassistant/components/AIOpener'; import { ResourceDescription } from 'components/HelmReleases'; import HelmReleasesYaml from './HelmReleasesYaml'; import { ErrorBoundary } from 'shared/components/ErrorBoundary/ErrorBoundary'; import { showYamlTab } from './index'; import { Link } from 'shared/components/Link/Link'; -import { Button } from '@ui5/webcomponents-react'; -import { showAIassistantState } from 'components/AIassistant/state/showAIassistantAtom'; import { createPortal } from 'react-dom'; function HelmReleasesDetails({ releaseName, namespace }) { const { t } = useTranslation(); const { namespaceUrl } = useUrl(); - const setOpenAssistant = useSetRecoilState(showAIassistantState); - const { data, loading } = useGetList(s => s.type === 'helm.sh/release.v1')( namespace === '-all-' ? `/api/v1/secrets?labelSelector=name==${releaseName}` @@ -100,13 +96,7 @@ function HelmReleasesDetails({ releaseName, namespace }) { /> - + )} diff --git a/src/shared/components/ResourceDetails/ResourceDetails.js b/src/shared/components/ResourceDetails/ResourceDetails.js index a3bafe6ae0..11f1359131 100644 --- a/src/shared/components/ResourceDetails/ResourceDetails.js +++ b/src/shared/components/ResourceDetails/ResourceDetails.js @@ -23,16 +23,16 @@ import { useVersionWarning } from 'hooks/useVersionWarning'; import { Tooltip } from '../Tooltip/Tooltip'; import YamlUploadDialog from 'resources/Namespaces/YamlUpload/YamlUploadDialog'; +import AIOpener from 'components/AIassistant/components/AIOpener'; import { createPortal } from 'react-dom'; import ResourceDetailsCard from './ResourceDetailsCard'; import { ResourceStatusCard } from '../ResourceStatusCard/ResourceStatusCard'; import { EMPTY_TEXT_PLACEHOLDER } from '../../constants'; import { ReadableElapsedTimeFromNow } from '../ReadableElapsedTimeFromNow/ReadableElapsedTimeFromNow'; import { HintButton } from '../DescriptionHint/DescriptionHint'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { useFeature } from 'hooks/useFeature'; import { columnLayoutState } from 'state/columnLayoutAtom'; -import { showAIassistantState } from 'components/AIassistant/state/showAIassistantAtom'; // This component is loaded after the page mounts. // Don't try to load it on scroll. It was tested. @@ -173,7 +173,6 @@ function Resource({ resource.kind, ); const [showTitleDescription, setShowTitleDescription] = useState(false); - const setOpenAssistant = useSetRecoilState(showAIassistantState); const pluralizedResourceKind = pluralize(prettifiedResourceKind); useWindowTitle(windowTitle || pluralizedResourceKind); @@ -368,13 +367,7 @@ function Resource({ {title ?? t('common.headers.resource-details')} - +
Date: Tue, 2 Apr 2024 13:35:30 +0200 Subject: [PATCH 04/38] fix: disable input while waiting for response & hide open-Button when assistant is already open --- .../AIassistant/components/AIOpener.js | 11 +++++--- .../AIassistant/components/Chat/Chat.js | 27 ++++++++++++------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/components/AIassistant/components/AIOpener.js b/src/components/AIassistant/components/AIOpener.js index b400b405b5..2e6f464c15 100644 --- a/src/components/AIassistant/components/AIOpener.js +++ b/src/components/AIassistant/components/AIOpener.js @@ -12,7 +12,7 @@ import { import { spacing } from '@ui5/webcomponents-react-base'; import { useTranslation } from 'react-i18next'; import { useState } from 'react'; -import { useSetRecoilState } from 'recoil'; +import { useRecoilState, useSetRecoilState } from 'recoil'; import { showAIassistantState } from 'components/AIassistant/state/showAIassistantAtom'; import { initialPromptState } from '../state/initalPromptAtom'; import getPromptSuggestions from 'components/AIassistant/utils/getPromptSuggestions'; @@ -20,12 +20,14 @@ import './AIOpener.scss'; export default function AIOpener() { const { t } = useTranslation(); - const setOpenAssistant = useSetRecoilState(showAIassistantState); + const [assistantOpen, setOpenAssistant] = useRecoilState( + showAIassistantState, + ); const setInitialPrompt = useSetRecoilState(initialPromptState); const [popoverOpen, setPopoverOpen] = useState(false); const [suggestions, setSuggestions] = useState([]); - return ( + return !assistantOpen ? ( <> {}} />} - value={''} - onKeyDown={e => {}} - onInput={e => {}} - placeholder="Ask about this cluster" - /> - } onAfterClose={() => setPopoverOpen(false)} opener="openPopoverBtn" placementType="Bottom" horizontalAlign="Right" > - {'Suggestions'} - - + } + value={inputValue} + onKeyDown={e => { + if (e.key === 'Enter') onSubmitInput(); + }} + onInput={e => setInputValue(e.target.value)} + placeholder={t('ai-assistant.opener.input-placeholder')} + className="popover-input" + /> + + {t('ai-assistant.opener.suggestions')} + + {suggestions.length === 0 ? ( +
+ +
+ ) : ( + {suggestions.map((suggestion, index) => ( { setInitialPrompt(suggestion); + setPopoverOpen(false); setOpenAssistant(true); }} + className="custom-list-item" > ))} -
+ )}
- ) : ( - <> ); } diff --git a/src/components/AIassistant/components/AIOpener.scss b/src/components/AIassistant/components/AIOpener.scss index c85d271016..7e3a1be58e 100644 --- a/src/components/AIassistant/components/AIOpener.scss +++ b/src/components/AIassistant/components/AIOpener.scss @@ -1,14 +1,24 @@ .ai-button { color: var(--sapContent_Illustrative_Color1); } +.suggestions-popover::part(content) { + padding: 0.5rem; +} + +.suggestions-popover { + .popover-input { + min-width: 250px; + } + + .custom-list-item::part(native-li) { + padding: 0.5rem; + } -.list-item-content { - width: 100%; + .list-item-content { + width: 100%; - .text { - max-width: 85%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + .text { + width: 90%; + } } } From e4311d267be6424ce4fbee703e920be8fec8cc5a Mon Sep 17 00:00:00 2001 From: chriskari Date: Wed, 3 Apr 2024 20:45:14 +0200 Subject: [PATCH 06/38] feat: added error handling to inital-suggestions-popover --- public/i18n/en.yaml | 2 + .../{utils => api}/getChatResponse.js | 9 --- .../AIassistant/api/getPromptSuggestions.js | 35 ++++++++++ .../AIassistant/components/AIOpener.js | 69 +++++++++++-------- .../AIassistant/components/AIOpener.scss | 9 ++- .../AIassistant/components/Chat/Chat.js | 14 ++-- .../AIassistant/utils/getPromptSuggestions.js | 30 -------- 7 files changed, 91 insertions(+), 77 deletions(-) rename src/components/AIassistant/{utils => api}/getChatResponse.js (85%) create mode 100644 src/components/AIassistant/api/getPromptSuggestions.js delete mode 100644 src/components/AIassistant/utils/getPromptSuggestions.js diff --git a/public/i18n/en.yaml b/public/i18n/en.yaml index f61fb1dd10..308e02d13a 100644 --- a/public/i18n/en.yaml +++ b/public/i18n/en.yaml @@ -9,6 +9,7 @@ ai-assistant: use-ai: Use AI 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. @@ -276,6 +277,7 @@ common: remove-all: Remove all reset: Reset restart: Restart + retry: Retry save: Save submit: Submit update: Update diff --git a/src/components/AIassistant/utils/getChatResponse.js b/src/components/AIassistant/api/getChatResponse.js similarity index 85% rename from src/components/AIassistant/utils/getChatResponse.js rename to src/components/AIassistant/api/getChatResponse.js index 76f39f6a58..6bddab4674 100644 --- a/src/components/AIassistant/utils/getChatResponse.js +++ b/src/components/AIassistant/api/getChatResponse.js @@ -1,18 +1,9 @@ -function delay() { - return new Promise(resolve => setTimeout(resolve, 3000)); -} - export default async function getChatResponse( prompt, handleSuccess, handleError, ) { try { - if (prompt === 'Throw an error') { - await delay(); - throw new Error('This is a custom error message.'); - } - const { response } = await fetch( 'https://api-backend.c-5cb6076.stage.kyma.ondemand.com/api/v1/llm', { diff --git a/src/components/AIassistant/api/getPromptSuggestions.js b/src/components/AIassistant/api/getPromptSuggestions.js new file mode 100644 index 0000000000..b3cbfe0002 --- /dev/null +++ b/src/components/AIassistant/api/getPromptSuggestions.js @@ -0,0 +1,35 @@ +export default async function getPromptSuggestions( + pageType = 'statefulsets.apps', + namespace = 'kyma-system', + nodeName = '', +) { + try { + let { results } = await fetch( + 'https://api-backend.c-5cb6076.stage.kyma.ondemand.com/api/v1/llm/init', + { + headers: { + accept: 'application/json, text/plain, */*', + 'accept-language': 'en-US,en;q=0.9,de;q=0.8', + 'content-type': 'application/json', + 'sec-ch-ua': + '"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"macOS"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'cross-site', + }, + referrer: 'https://ai.kyma.dev.sap/', + referrerPolicy: 'strict-origin-when-cross-origin', + body: `{"page_type":"${pageType}","namespace":"${namespace}","node_name":"${nodeName}"}`, + method: 'POST', + mode: 'cors', + credentials: 'omit', + }, + ).then(result => result.json()); + return results; + } catch (error) { + console.error('Error fetching data:', error); + return false; + } +} diff --git a/src/components/AIassistant/components/AIOpener.js b/src/components/AIassistant/components/AIOpener.js index 01fb1dba3d..d0f50dd24e 100644 --- a/src/components/AIassistant/components/AIOpener.js +++ b/src/components/AIassistant/components/AIOpener.js @@ -16,7 +16,7 @@ import { useState } from 'react'; import { useRecoilState, useSetRecoilState } from 'recoil'; import { showAIassistantState } from 'components/AIassistant/state/showAIassistantAtom'; import { initialPromptState } from '../state/initalPromptAtom'; -import getPromptSuggestions from 'components/AIassistant/utils/getPromptSuggestions'; +import getPromptSuggestions from 'components/AIassistant/api/getPromptSuggestions'; import './AIOpener.scss'; export default function AIOpener() { @@ -28,14 +28,32 @@ export default function AIOpener() { const [popoverOpen, setPopoverOpen] = useState(false); const [suggestions, setSuggestions] = useState([]); const [inputValue, setInputValue] = useState(''); + const [errorOccured, setErrorOccured] = useState(false); + + const fetchSuggestions = async () => { + setErrorOccured(false); + setPopoverOpen(true); + if (suggestions.length === 0) { + const promptSuggestions = await getPromptSuggestions(); + if (!promptSuggestions) { + setErrorOccured(true); + } else { + setSuggestions(promptSuggestions); + } + } + }; + + const sendInitialPrompt = prompt => { + setInitialPrompt(prompt); + setPopoverOpen(false); + setOpenAssistant(true); + }; const onSubmitInput = () => { if (inputValue.length === 0) return; const prompt = inputValue; setInputValue(''); - setInitialPrompt(prompt); - setPopoverOpen(false); - setOpenAssistant(true); + sendInitialPrompt(prompt); }; return ( @@ -45,13 +63,7 @@ export default function AIOpener() { className="ai-button" id="openPopoverBtn" disabled={assistantOpen} - onClick={async () => { - setPopoverOpen(true); - if (suggestions.length === 0) { - const suggestions = await getPromptSuggestions(); - setSuggestions(suggestions); - } - }} + onClick={fetchSuggestions} > {t('ai-assistant.opener.use-ai')} @@ -66,9 +78,7 @@ export default function AIOpener() { } value={inputValue} - onKeyDown={e => { - if (e.key === 'Enter') onSubmitInput(); - }} + onKeyDown={e => e.key === 'Enter' && onSubmitInput()} onInput={e => setInputValue(e.target.value)} placeholder={t('ai-assistant.opener.input-placeholder')} className="popover-input" @@ -76,7 +86,20 @@ export default function AIOpener() { {t('ai-assistant.opener.suggestions')} - {suggestions.length === 0 ? ( + {errorOccured ? ( + + + {t('ai-assistant.opener.error-message')} + + + + ) : suggestions.length === 0 ? (
( { - setInitialPrompt(suggestion); - setPopoverOpen(false); - setOpenAssistant(true); - }} + onClick={() => sendInitialPrompt(suggestion)} className="custom-list-item" > - - {suggestion} - - + {suggestion} + ))} diff --git a/src/components/AIassistant/components/AIOpener.scss b/src/components/AIassistant/components/AIOpener.scss index 7e3a1be58e..89c53bdc39 100644 --- a/src/components/AIassistant/components/AIOpener.scss +++ b/src/components/AIassistant/components/AIOpener.scss @@ -1,21 +1,26 @@ .ai-button { color: var(--sapContent_Illustrative_Color1); } + .suggestions-popover::part(content) { padding: 0.5rem; } .suggestions-popover { .popover-input { - min-width: 250px; + min-width: 225px; + width: 100%; } .custom-list-item::part(native-li) { padding: 0.5rem; } - .list-item-content { + .custom-list-item { width: 100%; + display: flex; + align-items: center; + justify-content: space-between; .text { width: 90%; diff --git a/src/components/AIassistant/components/Chat/Chat.js b/src/components/AIassistant/components/Chat/Chat.js index 38e85ceca4..aaff81607e 100644 --- a/src/components/AIassistant/components/Chat/Chat.js +++ b/src/components/AIassistant/components/Chat/Chat.js @@ -7,7 +7,7 @@ import { initialPromptState } from 'components/AIassistant/state/initalPromptAto import PlainMessage from './messages/PlainMessage'; import Bubbles from './messages/Bubbles'; import ErrorMessage from './messages/ErrorMessage'; -import getChatResponse from 'components/AIassistant/utils/getChatResponse'; +import getChatResponse from 'components/AIassistant/api/getChatResponse'; import './Chat.scss'; export default function Chat() { @@ -39,7 +39,7 @@ export default function Chat() { setChatHistory(prevItems => prevItems.slice(0, -2)); }; - const onSendPrompt = prompt => { + const sendPrompt = prompt => { setErrorOccured(false); addMessage('user', prompt, false); getChatResponse(prompt, handleSuccess, handleError); @@ -50,7 +50,7 @@ export default function Chat() { if (inputValue.length === 0) return; const prompt = inputValue; setInputValue(''); - onSendPrompt(prompt); + sendPrompt(prompt); }; const scrollToBottom = () => { @@ -62,7 +62,7 @@ export default function Chat() { }; useEffect(() => { - if (chatHistory.length === 0) onSendPrompt(initialPrompt); + if (chatHistory.length === 0) sendPrompt(initialPrompt); // eslint-disable-next-line }, []); @@ -96,7 +96,7 @@ export default function Chat() { {index === chatHistory.length - 1 && !message.isLoading && ( } - onKeyDown={e => { - if (e.key === 'Enter') onSubmitInput(); - }} + onKeyDown={e => e.key === 'Enter' && onSubmitInput()} onInput={e => setInputValue(e.target.value)} />
diff --git a/src/components/AIassistant/utils/getPromptSuggestions.js b/src/components/AIassistant/utils/getPromptSuggestions.js deleted file mode 100644 index 968987e168..0000000000 --- a/src/components/AIassistant/utils/getPromptSuggestions.js +++ /dev/null @@ -1,30 +0,0 @@ -export default async function getPromptSuggestions( - pageType = 'statefulsets.apps', - namespace = 'kyma-system', - nodeName = '', -) { - let { results } = await fetch( - 'https://api-backend.c-5cb6076.stage.kyma.ondemand.com/api/v1/llm/init', - { - headers: { - accept: 'application/json, text/plain, */*', - 'accept-language': 'en-US,en;q=0.9,de;q=0.8', - 'content-type': 'application/json', - 'sec-ch-ua': - '"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"', - 'sec-ch-ua-mobile': '?0', - 'sec-ch-ua-platform': '"macOS"', - 'sec-fetch-dest': 'empty', - 'sec-fetch-mode': 'cors', - 'sec-fetch-site': 'cross-site', - }, - referrer: 'https://ai.kyma.dev.sap/', - referrerPolicy: 'strict-origin-when-cross-origin', - body: `{"page_type":"${pageType}","namespace":"${namespace}","node_name":"${nodeName}"}`, - method: 'POST', - mode: 'cors', - credentials: 'omit', - }, - ).then(result => result.json()); - return results; -} From 07ae000d43e871da9050ae90ed9ee6ddce1bc68e Mon Sep 17 00:00:00 2001 From: chriskari Date: Thu, 4 Apr 2024 09:21:45 +0200 Subject: [PATCH 07/38] fix: error bug in suggestions popover & disabled buttons --- src/components/AIassistant/api/getChatResponse.js | 4 ++-- src/components/AIassistant/api/getPromptSuggestions.js | 6 +++--- src/components/AIassistant/components/AIOpener.js | 6 ++++-- src/components/AIassistant/components/AIassistant.js | 1 + src/components/AIassistant/components/Chat/Chat.js | 2 +- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/AIassistant/api/getChatResponse.js b/src/components/AIassistant/api/getChatResponse.js index 6bddab4674..e6ab22ee79 100644 --- a/src/components/AIassistant/api/getChatResponse.js +++ b/src/components/AIassistant/api/getChatResponse.js @@ -1,8 +1,8 @@ -export default async function getChatResponse( +export default async function getChatResponse({ prompt, handleSuccess, handleError, -) { +}) { try { const { response } = await fetch( 'https://api-backend.c-5cb6076.stage.kyma.ondemand.com/api/v1/llm', diff --git a/src/components/AIassistant/api/getPromptSuggestions.js b/src/components/AIassistant/api/getPromptSuggestions.js index b3cbfe0002..7b8b3359f3 100644 --- a/src/components/AIassistant/api/getPromptSuggestions.js +++ b/src/components/AIassistant/api/getPromptSuggestions.js @@ -1,8 +1,8 @@ -export default async function getPromptSuggestions( +export default async function getPromptSuggestions({ pageType = 'statefulsets.apps', - namespace = 'kyma-system', + namespace, nodeName = '', -) { +}) { try { let { results } = await fetch( 'https://api-backend.c-5cb6076.stage.kyma.ondemand.com/api/v1/llm/init', diff --git a/src/components/AIassistant/components/AIOpener.js b/src/components/AIassistant/components/AIOpener.js index d0f50dd24e..ced808d9a2 100644 --- a/src/components/AIassistant/components/AIOpener.js +++ b/src/components/AIassistant/components/AIOpener.js @@ -10,6 +10,7 @@ import { Text, Title, } from '@ui5/webcomponents-react'; +import { useUrl } from 'hooks/useUrl'; import { spacing } from '@ui5/webcomponents-react-base'; import { useTranslation } from 'react-i18next'; import { useState } from 'react'; @@ -24,6 +25,7 @@ export default function AIOpener() { const [assistantOpen, setOpenAssistant] = useRecoilState( showAIassistantState, ); + const { namespace } = useUrl(); const setInitialPrompt = useSetRecoilState(initialPromptState); const [popoverOpen, setPopoverOpen] = useState(false); const [suggestions, setSuggestions] = useState([]); @@ -34,7 +36,7 @@ export default function AIOpener() { setErrorOccured(false); setPopoverOpen(true); if (suggestions.length === 0) { - const promptSuggestions = await getPromptSuggestions(); + const promptSuggestions = await getPromptSuggestions({ namespace }); if (!promptSuggestions) { setErrorOccured(true); } else { @@ -95,7 +97,7 @@ export default function AIOpener() { {t('ai-assistant.opener.error-message')} - diff --git a/src/components/AIassistant/components/AIassistant.js b/src/components/AIassistant/components/AIassistant.js index 5471d7a13f..3299943752 100644 --- a/src/components/AIassistant/components/AIassistant.js +++ b/src/components/AIassistant/components/AIassistant.js @@ -34,6 +34,7 @@ export default function AIassistant() { design="Transparent" icon="full-screen" className="action" + disabled />
+ ) : ( + + + {code} + + + ); +} + +function parseCodeText(text) { + const lines = text.split('\n'); + const language = lines.shift(); + const code = lines.join('\n'); + return { language, code }; +} diff --git a/src/components/AIassistant/components/Chat/messages/CodePanel.scss b/src/components/AIassistant/components/Chat/messages/CodePanel.scss new file mode 100644 index 0000000000..c4b3a7b5f4 --- /dev/null +++ b/src/components/AIassistant/components/Chat/messages/CodePanel.scss @@ -0,0 +1,28 @@ +.code-response { + background-color: #484848; + color: white; + padding: 0.75rem; + border-radius: 4px; + + .text { + color: white; + } +} + +.code-panel::part(header) { + background-color: #484848; + color: white; + border-radius: 4px 4px 0 0; + font-size: 0.9rem; +} + +.code-panel::part(content) { + background-color: #484848; + border-radius: 0 0 4px 4px; +} + +.code-panel { + .text { + color: white; + } +} diff --git a/src/components/AIassistant/components/Chat/messages/Message.js b/src/components/AIassistant/components/Chat/messages/Message.js new file mode 100644 index 0000000000..2f21755afc --- /dev/null +++ b/src/components/AIassistant/components/Chat/messages/Message.js @@ -0,0 +1,59 @@ +import { BusyIndicator, Text } from '@ui5/webcomponents-react'; +import CodePanel from './CodePanel'; +import './Message.scss'; + +export default function Message({ className, message, isLoading }) { + const segmentedText = formatText(message); + return ( +
+ {isLoading && } + {segmentedText && ( + + {segmentedText.map((segment, index) => + segment.type === 'bold' ? ( + + {segment.content} + + ) : segment.type === 'code' ? ( + + ) : segment.type === 'highlighted' ? ( + + {segment.content} + + ) : ( + segment.content + ), + )} + + )} +
+ ); +} + +function formatText(text) { + if (!text) return []; + const regex = /(\*\*(.*?)\*\*)|(```([\s\S]*?)```)|(`(.*?)`)|[^*`]+/g; + return text.match(regex).map(segment => { + if (segment.startsWith('**')) { + return { + type: 'bold', + content: segment.replace(/\*\*/g, ''), + }; + } else if (segment.startsWith('```')) { + return { + type: 'code', + content: segment.replace(/```/g, ''), + }; + } else if (segment.startsWith('`')) { + return { + type: 'highlighted', + content: segment.replace(/`/g, ''), + }; + } else { + return { + type: 'normal', + content: segment, + }; + } + }); +} diff --git a/src/components/AIassistant/components/Chat/messages/Message.scss b/src/components/AIassistant/components/Chat/messages/Message.scss new file mode 100644 index 0000000000..a1c7fde92a --- /dev/null +++ b/src/components/AIassistant/components/Chat/messages/Message.scss @@ -0,0 +1,17 @@ +.message { + max-width: 240px; + padding: 12px; + + .bold { + font-weight: bold; + font-size: 1rem; + } + + .highlighted { + background-color: var(--sapContent_LabelColor); + color: white; + padding: 0.2rem 0.25rem; + margin: 0.2rem 0; + border-radius: 4px; + } +} diff --git a/src/components/AIassistant/components/Chat/messages/MessageWithList.js b/src/components/AIassistant/components/Chat/messages/MessageWithList.js deleted file mode 100644 index 8d3e045546..0000000000 --- a/src/components/AIassistant/components/Chat/messages/MessageWithList.js +++ /dev/null @@ -1,39 +0,0 @@ -import { - CustomListItem, - FlexBox, - Icon, - List, - Text, -} from '@ui5/webcomponents-react'; -import { spacing } from '@ui5/webcomponents-react-base'; -import './MessageWithList.scss'; - -export default function MessageWithList({ - className, - message, - items, - appendix, -}) { - return ( -
- {message} -
- - {items.map((item, index) => ( - - - {item} - - - - ))} - -
- {appendix} -
- ); -} diff --git a/src/components/AIassistant/components/Chat/messages/MessageWithList.scss b/src/components/AIassistant/components/Chat/messages/MessageWithList.scss deleted file mode 100644 index b2bdbd3c41..0000000000 --- a/src/components/AIassistant/components/Chat/messages/MessageWithList.scss +++ /dev/null @@ -1,15 +0,0 @@ -.message-with-list { - max-width: 225px; - padding: 12px; - - .list-item-content { - width: 100%; - - .text { - max-width: 85%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } -} diff --git a/src/components/AIassistant/components/Chat/messages/PlainMessage.js b/src/components/AIassistant/components/Chat/messages/PlainMessage.js deleted file mode 100644 index 30b4855f65..0000000000 --- a/src/components/AIassistant/components/Chat/messages/PlainMessage.js +++ /dev/null @@ -1,11 +0,0 @@ -import { BusyIndicator, Text } from '@ui5/webcomponents-react'; -import './PlainMessage.scss'; - -export default function PlainMessage({ className, message, isLoading }) { - return ( -
- {isLoading && } - {message && {message}} -
- ); -} diff --git a/src/components/AIassistant/components/Chat/messages/PlainMessage.scss b/src/components/AIassistant/components/Chat/messages/PlainMessage.scss deleted file mode 100644 index df378de169..0000000000 --- a/src/components/AIassistant/components/Chat/messages/PlainMessage.scss +++ /dev/null @@ -1,4 +0,0 @@ -.plain-message { - max-width: 225px; - padding: 12px; -} From 48375aa827bb24afc843984fc16dec67385189ba Mon Sep 17 00:00:00 2001 From: chriskari Date: Mon, 8 Apr 2024 10:08:53 +0200 Subject: [PATCH 09/38] fix: markdown formatting adjustments --- .../AIassistant/components/Chat/messages/CodePanel.js | 3 ++- src/components/AIassistant/components/Chat/messages/Message.js | 2 +- .../AIassistant/components/Chat/messages/Message.scss | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/AIassistant/components/Chat/messages/CodePanel.js b/src/components/AIassistant/components/Chat/messages/CodePanel.js index 990ecc12e9..b57e40069b 100644 --- a/src/components/AIassistant/components/Chat/messages/CodePanel.js +++ b/src/components/AIassistant/components/Chat/messages/CodePanel.js @@ -21,6 +21,7 @@ export default function CodePanel({ text }) { function parseCodeText(text) { const lines = text.split('\n'); const language = lines.shift(); - const code = lines.join('\n'); + const nonEmptyLines = lines.filter(line => line.trim() !== ''); + const code = nonEmptyLines.join('\n'); return { language, code }; } diff --git a/src/components/AIassistant/components/Chat/messages/Message.js b/src/components/AIassistant/components/Chat/messages/Message.js index 2f21755afc..0ec758f677 100644 --- a/src/components/AIassistant/components/Chat/messages/Message.js +++ b/src/components/AIassistant/components/Chat/messages/Message.js @@ -32,7 +32,7 @@ export default function Message({ className, message, isLoading }) { function formatText(text) { if (!text) return []; - const regex = /(\*\*(.*?)\*\*)|(```([\s\S]*?)```)|(`(.*?)`)|[^*`]+/g; + const regex = /(\*\*(.*?)\*\*)|(```([\s\S]*?)```\s)|(`(.*?)`)|[^*`]+/g; return text.match(regex).map(segment => { if (segment.startsWith('**')) { return { diff --git a/src/components/AIassistant/components/Chat/messages/Message.scss b/src/components/AIassistant/components/Chat/messages/Message.scss index a1c7fde92a..f7956eb857 100644 --- a/src/components/AIassistant/components/Chat/messages/Message.scss +++ b/src/components/AIassistant/components/Chat/messages/Message.scss @@ -11,7 +11,7 @@ background-color: var(--sapContent_LabelColor); color: white; padding: 0.2rem 0.25rem; - margin: 0.2rem 0; + margin: 0.1rem 0; border-radius: 4px; } } From 33062da766331cce16a1eaa60d20dab9d16b7e66 Mon Sep 17 00:00:00 2001 From: chriskari Date: Tue, 9 Apr 2024 13:05:45 +0200 Subject: [PATCH 10/38] feat: initial suggestions now depend on currently opened resource --- .../AIassistant/api/getPromptSuggestions.js | 8 +- .../AIassistant/components/AIOpener.js | 126 +++++++++--------- .../AIassistant/components/AIOpener.scss | 2 +- .../components/Chat/messages/Bubbles.scss | 4 +- .../components/Chat/messages/CodePanel.js | 11 +- .../components/Chat/messages/Message.js | 31 +---- .../AIassistant/utils/formatMarkdown.js | 35 +++++ .../views/ClusterOverview/ClusterOverview.js | 5 +- .../HelmReleases/HelmReleasesDetails.js | 6 +- .../ResourceDetails/ResourceDetails.js | 6 +- 10 files changed, 126 insertions(+), 108 deletions(-) create mode 100644 src/components/AIassistant/utils/formatMarkdown.js diff --git a/src/components/AIassistant/api/getPromptSuggestions.js b/src/components/AIassistant/api/getPromptSuggestions.js index 5f78cf91bd..1f8be0f754 100644 --- a/src/components/AIassistant/api/getPromptSuggestions.js +++ b/src/components/AIassistant/api/getPromptSuggestions.js @@ -1,7 +1,7 @@ export default async function getPromptSuggestions({ - pageType = 'statefulsets.apps', - namespace, - nodeName = '', + namespace = '', + resourceType = '', + resourceName = '', }) { try { let { results } = await fetch( @@ -11,7 +11,7 @@ export default async function getPromptSuggestions({ accept: 'application/json, text/plain, */*', 'content-type': 'application/json', }, - body: `{"page_type":"${pageType}","namespace":"${namespace}","node_name":"${nodeName}"}`, + body: `{"resource_type":"${resourceType}","resource_name":"${resourceName}","namespace":"${namespace}"}`, method: 'POST', }, ).then(result => result.json()); diff --git a/src/components/AIassistant/components/AIOpener.js b/src/components/AIassistant/components/AIOpener.js index ced808d9a2..894b7a55a8 100644 --- a/src/components/AIassistant/components/AIOpener.js +++ b/src/components/AIassistant/components/AIOpener.js @@ -10,7 +10,6 @@ import { Text, Title, } from '@ui5/webcomponents-react'; -import { useUrl } from 'hooks/useUrl'; import { spacing } from '@ui5/webcomponents-react-base'; import { useTranslation } from 'react-i18next'; import { useState } from 'react'; @@ -18,14 +17,14 @@ import { useRecoilState, useSetRecoilState } from 'recoil'; import { showAIassistantState } from 'components/AIassistant/state/showAIassistantAtom'; import { initialPromptState } from '../state/initalPromptAtom'; import getPromptSuggestions from 'components/AIassistant/api/getPromptSuggestions'; +import { createPortal } from 'react-dom'; import './AIOpener.scss'; -export default function AIOpener() { +export default function AIOpener({ namespace, resourceType, resourceName }) { const { t } = useTranslation(); const [assistantOpen, setOpenAssistant] = useRecoilState( showAIassistantState, ); - const { namespace } = useUrl(); const setInitialPrompt = useSetRecoilState(initialPromptState); const [popoverOpen, setPopoverOpen] = useState(false); const [suggestions, setSuggestions] = useState([]); @@ -36,7 +35,11 @@ export default function AIOpener() { setErrorOccured(false); setPopoverOpen(true); if (suggestions.length === 0) { - const promptSuggestions = await getPromptSuggestions({ namespace }); + const promptSuggestions = await getPromptSuggestions({ + namespace, + resourceType, + resourceName, + }); if (!promptSuggestions) { setErrorOccured(true); } else { @@ -69,62 +72,65 @@ export default function AIOpener() { > {t('ai-assistant.opener.use-ai')} - setPopoverOpen(false)} - opener="openPopoverBtn" - placementType="Bottom" - horizontalAlign="Right" - > - } - value={inputValue} - onKeyDown={e => e.key === 'Enter' && onSubmitInput()} - onInput={e => setInputValue(e.target.value)} - placeholder={t('ai-assistant.opener.input-placeholder')} - className="popover-input" - /> - - {t('ai-assistant.opener.suggestions')} - - {errorOccured ? ( - - - {t('ai-assistant.opener.error-message')} - - - - ) : suggestions.length === 0 ? ( -
- -
- ) : ( - - {suggestions.map((suggestion, index) => ( - sendInitialPrompt(suggestion)} - className="custom-list-item" - > - {suggestion} - - - ))} - - )} -
+ {createPortal( + setPopoverOpen(false)} + opener="openPopoverBtn" + placementType="Bottom" + horizontalAlign="Right" + > + } + value={inputValue} + onKeyDown={e => e.key === 'Enter' && onSubmitInput()} + onInput={e => setInputValue(e.target.value)} + placeholder={t('ai-assistant.opener.input-placeholder')} + className="popover-input" + /> + + {t('ai-assistant.opener.suggestions')} + + {errorOccured ? ( + + + {t('ai-assistant.opener.error-message')} + + + + ) : suggestions.length === 0 ? ( +
+ +
+ ) : ( + + {suggestions.map((suggestion, index) => ( + sendInitialPrompt(suggestion)} + className="custom-list-item" + > + {suggestion} + + + ))} + + )} +
, + document.body, + )} ); } diff --git a/src/components/AIassistant/components/AIOpener.scss b/src/components/AIassistant/components/AIOpener.scss index 89c53bdc39..4a0a177f93 100644 --- a/src/components/AIassistant/components/AIOpener.scss +++ b/src/components/AIassistant/components/AIOpener.scss @@ -1,5 +1,5 @@ .ai-button { - color: var(--sapContent_Illustrative_Color1); + color: var(--sapChart_OrderedColor_5); } .suggestions-popover::part(content) { diff --git a/src/components/AIassistant/components/Chat/messages/Bubbles.scss b/src/components/AIassistant/components/Chat/messages/Bubbles.scss index 1a2ad9540e..331767d2b6 100644 --- a/src/components/AIassistant/components/Chat/messages/Bubbles.scss +++ b/src/components/AIassistant/components/Chat/messages/Bubbles.scss @@ -4,11 +4,11 @@ .bubble-button { align-self: flex-start; - color: var(--sapContent_Illustrative_Color1); + color: var(--sapChart_OrderedColor_5); } .bubble-button:hover { background-color: var(--sapBackgroundColor1); - border-color: var(--sapContent_Illustrative_Color1); + border-color: var(--sapChart_OrderedColor_5); } } diff --git a/src/components/AIassistant/components/Chat/messages/CodePanel.js b/src/components/AIassistant/components/Chat/messages/CodePanel.js index b57e40069b..78c18285af 100644 --- a/src/components/AIassistant/components/Chat/messages/CodePanel.js +++ b/src/components/AIassistant/components/Chat/messages/CodePanel.js @@ -1,8 +1,9 @@ import { Text, Panel } from '@ui5/webcomponents-react'; +import { formatCodeSegment } from 'components/AIassistant/utils/formatMarkdown'; import './CodePanel.scss'; export default function CodePanel({ text }) { - const { language, code } = parseCodeText(text); + const { language, code } = formatCodeSegment(text); return !language ? (
@@ -17,11 +18,3 @@ export default function CodePanel({ text }) { ); } - -function parseCodeText(text) { - const lines = text.split('\n'); - const language = lines.shift(); - const nonEmptyLines = lines.filter(line => line.trim() !== ''); - const code = nonEmptyLines.join('\n'); - return { language, code }; -} diff --git a/src/components/AIassistant/components/Chat/messages/Message.js b/src/components/AIassistant/components/Chat/messages/Message.js index 0ec758f677..3d45a4bf5a 100644 --- a/src/components/AIassistant/components/Chat/messages/Message.js +++ b/src/components/AIassistant/components/Chat/messages/Message.js @@ -1,9 +1,10 @@ import { BusyIndicator, Text } from '@ui5/webcomponents-react'; +import { segmentMarkdownText } from 'components/AIassistant/utils/formatMarkdown'; import CodePanel from './CodePanel'; import './Message.scss'; export default function Message({ className, message, isLoading }) { - const segmentedText = formatText(message); + const segmentedText = segmentMarkdownText(message); return (
{isLoading && } @@ -29,31 +30,3 @@ export default function Message({ className, message, isLoading }) {
); } - -function formatText(text) { - if (!text) return []; - const regex = /(\*\*(.*?)\*\*)|(```([\s\S]*?)```\s)|(`(.*?)`)|[^*`]+/g; - return text.match(regex).map(segment => { - if (segment.startsWith('**')) { - return { - type: 'bold', - content: segment.replace(/\*\*/g, ''), - }; - } else if (segment.startsWith('```')) { - return { - type: 'code', - content: segment.replace(/```/g, ''), - }; - } else if (segment.startsWith('`')) { - return { - type: 'highlighted', - content: segment.replace(/`/g, ''), - }; - } else { - return { - type: 'normal', - content: segment, - }; - } - }); -} diff --git a/src/components/AIassistant/utils/formatMarkdown.js b/src/components/AIassistant/utils/formatMarkdown.js new file mode 100644 index 0000000000..c725f97162 --- /dev/null +++ b/src/components/AIassistant/utils/formatMarkdown.js @@ -0,0 +1,35 @@ +export function segmentMarkdownText(text) { + if (!text) return []; + const regex = /(\*\*(.*?)\*\*)|(```([\s\S]*?)```\s)|(`(.*?)`)|[^*`]+/g; + return text.match(regex).map(segment => { + if (segment.startsWith('**')) { + return { + type: 'bold', + content: segment.replace(/\*\*/g, ''), + }; + } else if (segment.startsWith('```')) { + return { + type: 'code', + content: segment.replace(/```/g, ''), + }; + } else if (segment.startsWith('`')) { + return { + type: 'highlighted', + content: segment.replace(/`/g, ''), + }; + } else { + return { + type: 'normal', + content: segment, + }; + } + }); +} + +export function formatCodeSegment(text) { + const lines = text.split('\n'); + const language = lines.shift(); + const nonEmptyLines = lines.filter(line => line.trim() !== ''); + const code = nonEmptyLines.join('\n'); + return { language, code }; +} diff --git a/src/components/Clusters/views/ClusterOverview/ClusterOverview.js b/src/components/Clusters/views/ClusterOverview/ClusterOverview.js index 477e738226..3d6b642b49 100644 --- a/src/components/Clusters/views/ClusterOverview/ClusterOverview.js +++ b/src/components/Clusters/views/ClusterOverview/ClusterOverview.js @@ -85,7 +85,10 @@ export function ClusterOverview() { {t('cluster-overview.headers.cluster-details')} - + {data && } diff --git a/src/components/HelmReleases/HelmReleasesDetails.js b/src/components/HelmReleases/HelmReleasesDetails.js index 5eb2db2c73..39682ba4da 100644 --- a/src/components/HelmReleases/HelmReleasesDetails.js +++ b/src/components/HelmReleases/HelmReleasesDetails.js @@ -96,7 +96,11 @@ function HelmReleasesDetails({ releaseName, namespace }) { /> - + )} diff --git a/src/shared/components/ResourceDetails/ResourceDetails.js b/src/shared/components/ResourceDetails/ResourceDetails.js index 42ba161b40..792c686b08 100644 --- a/src/shared/components/ResourceDetails/ResourceDetails.js +++ b/src/shared/components/ResourceDetails/ResourceDetails.js @@ -369,7 +369,11 @@ function Resource({ {title ?? t('common.headers.resource-details')} - +
Date: Tue, 9 Apr 2024 23:14:25 +0200 Subject: [PATCH 11/38] feat: assistant closes when open resource changes --- src/components/AIassistant/components/AIOpener.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/AIassistant/components/AIOpener.js b/src/components/AIassistant/components/AIOpener.js index 894b7a55a8..c75a9dc6ce 100644 --- a/src/components/AIassistant/components/AIOpener.js +++ b/src/components/AIassistant/components/AIOpener.js @@ -12,7 +12,7 @@ import { } from '@ui5/webcomponents-react'; import { spacing } from '@ui5/webcomponents-react-base'; import { useTranslation } from 'react-i18next'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useRecoilState, useSetRecoilState } from 'recoil'; import { showAIassistantState } from 'components/AIassistant/state/showAIassistantAtom'; import { initialPromptState } from '../state/initalPromptAtom'; @@ -31,6 +31,13 @@ export default function AIOpener({ namespace, resourceType, resourceName }) { const [inputValue, setInputValue] = useState(''); const [errorOccured, setErrorOccured] = useState(false); + useEffect(() => { + return () => { + setOpenAssistant(false); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const fetchSuggestions = async () => { setErrorOccured(false); setPopoverOpen(true); From 23901afeec491b5483c8c1ebfd7bc2b1fd137939 Mon Sep 17 00:00:00 2001 From: chriskari Date: Wed, 10 Apr 2024 13:13:00 +0200 Subject: [PATCH 12/38] feat: added more error handling & fullscreen-mode --- .../AIassistant/components/AIOpener.js | 18 ++++++++---- .../AIassistant/components/AIassistant.js | 23 ++++++++++----- .../AIassistant/components/Chat/Chat.js | 7 ++++- .../components/Chat/messages/ErrorMessage.js | 15 ++++++++-- .../AIassistant/state/showAIassistantAtom.ts | 10 +++++-- src/components/App/App.scss | 8 ++++++ src/components/App/App.tsx | 28 ++++++++++++------- 7 files changed, 80 insertions(+), 29 deletions(-) diff --git a/src/components/AIassistant/components/AIOpener.js b/src/components/AIassistant/components/AIOpener.js index c75a9dc6ce..97e2c29056 100644 --- a/src/components/AIassistant/components/AIOpener.js +++ b/src/components/AIassistant/components/AIOpener.js @@ -22,7 +22,7 @@ import './AIOpener.scss'; export default function AIOpener({ namespace, resourceType, resourceName }) { const { t } = useTranslation(); - const [assistantOpen, setOpenAssistant] = useRecoilState( + const [showAssistant, setShowAssistant] = useRecoilState( showAIassistantState, ); const setInitialPrompt = useSetRecoilState(initialPromptState); @@ -30,10 +30,11 @@ export default function AIOpener({ namespace, resourceType, resourceName }) { const [suggestions, setSuggestions] = useState([]); const [inputValue, setInputValue] = useState(''); const [errorOccured, setErrorOccured] = useState(false); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { return () => { - setOpenAssistant(false); + setShowAssistant({ show: false, fullScreen: false }); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -42,11 +43,13 @@ export default function AIOpener({ namespace, resourceType, resourceName }) { setErrorOccured(false); setPopoverOpen(true); if (suggestions.length === 0) { + setIsLoading(true); const promptSuggestions = await getPromptSuggestions({ namespace, resourceType, resourceName, }); + setIsLoading(false); if (!promptSuggestions) { setErrorOccured(true); } else { @@ -58,7 +61,10 @@ export default function AIOpener({ namespace, resourceType, resourceName }) { const sendInitialPrompt = prompt => { setInitialPrompt(prompt); setPopoverOpen(false); - setOpenAssistant(true); + setShowAssistant({ + show: true, + fullScreen: false, + }); }; const onSubmitInput = () => { @@ -74,7 +80,7 @@ export default function AIOpener({ namespace, resourceType, resourceName }) { icon="ai" className="ai-button" id="openPopoverBtn" - disabled={assistantOpen} + disabled={showAssistant.show} onClick={fetchSuggestions} > {t('ai-assistant.opener.use-ai')} @@ -99,7 +105,7 @@ export default function AIOpener({ namespace, resourceType, resourceName }) { {t('ai-assistant.opener.suggestions')} - {errorOccured ? ( + {errorOccured || (!isLoading && suggestions.length === 0) ? ( - ) : suggestions.length === 0 ? ( + ) : isLoading ? (
diff --git a/src/components/AIassistant/components/Chat/Chat.js b/src/components/AIassistant/components/Chat/Chat.js index 76805abfb2..7d1b5d3fa2 100644 --- a/src/components/AIassistant/components/Chat/Chat.js +++ b/src/components/AIassistant/components/Chat/Chat.js @@ -115,7 +115,12 @@ export default function Chat() { /> ); })} - {errorOccured && } + {errorOccured && ( + sendPrompt(initialPrompt)} + /> + )}
+ > + {errorOnInitialMessage && ( + + )} + ); } diff --git a/src/components/AIassistant/state/showAIassistantAtom.ts b/src/components/AIassistant/state/showAIassistantAtom.ts index ac035680cf..92baec53e1 100644 --- a/src/components/AIassistant/state/showAIassistantAtom.ts +++ b/src/components/AIassistant/state/showAIassistantAtom.ts @@ -1,8 +1,14 @@ import { atom, RecoilState } from 'recoil'; -type ShowAIassistant = boolean; +type ShowAIassistant = { + show: boolean; + fullScreen: boolean; +}; -const DEFAULT_SHOW_AI_ASSISTANT = false; +const DEFAULT_SHOW_AI_ASSISTANT: ShowAIassistant = { + show: false, + fullScreen: false, +}; export const showAIassistantState: RecoilState = atom< ShowAIassistant diff --git a/src/components/App/App.scss b/src/components/App/App.scss index f4bda11d4e..a658d7ce23 100644 --- a/src/components/App/App.scss +++ b/src/components/App/App.scss @@ -1,3 +1,11 @@ +#splitter-layout { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + #html-wrap { position: absolute; top: 0; diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 27ba4b5058..fa096b787a 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -71,16 +71,20 @@ export default function App() { useAppTracking(); useAfterInitHook(kubeconfigIdState); - const assistantOpen = useRecoilValue(showAIassistantState); + const showAssistant = useRecoilValue(showAIassistantState); return ( - - + +
@@ -113,8 +117,12 @@ export default function App() {
- {assistantOpen ? ( - + {showAssistant.show ? ( +
From bd71c24c90cb4569092c8aae9f3e95cc248f20ab Mon Sep 17 00:00:00 2001 From: chriskari Date: Thu, 11 Apr 2024 14:26:31 +0200 Subject: [PATCH 13/38] fix: small code clean-up --- .../AIassistant/components/AIassistant.js | 95 ++++++++++--------- .../AIassistant/components/AIassistant.scss | 37 ++++---- src/components/App/App.scss | 5 - src/components/App/App.tsx | 4 +- 4 files changed, 69 insertions(+), 72 deletions(-) diff --git a/src/components/AIassistant/components/AIassistant.js b/src/components/AIassistant/components/AIassistant.js index a2d82c48ea..e78a47906f 100644 --- a/src/components/AIassistant/components/AIassistant.js +++ b/src/components/AIassistant/components/AIassistant.js @@ -22,53 +22,54 @@ export default function AIassistant() { ); return ( - - - {t('ai-assistant.name')} - - -
-
- - } - > - + + + {t('ai-assistant.name')} + + +
+
+ + } > - - - - - - -
-
+ + + + + + + + + +
); } diff --git a/src/components/AIassistant/components/AIassistant.scss b/src/components/AIassistant/components/AIassistant.scss index 8f9859048f..c85a662389 100644 --- a/src/components/AIassistant/components/AIassistant.scss +++ b/src/components/AIassistant/components/AIassistant.scss @@ -1,25 +1,28 @@ -.ai_assistant { - height: 100%; +#assistant_wrapper { width: 100%; - &__header { - background-color: var(--sapContent_Illustrative_Color1); - min-height: 60px; - padding-left: 8px; - padding-right: 8px; + .ai_assistant { + height: 100%; + width: 100%; - .title { - color: white; - text-shadow: none; - } + &__header { + background-color: var(--sapContent_Illustrative_Color1); + min-height: 60px; + padding: 0.5rem; + + .title { + color: white; + text-shadow: none; + } - .action { - color: white; - background: transparent; + .action { + color: white; + background: transparent; + } } - } - .tab-container { - height: calc(100vh - 60px - 1.4rem); + .tab-container { + height: calc(100vh - 60px - 1.4rem); + } } } diff --git a/src/components/App/App.scss b/src/components/App/App.scss index a658d7ce23..f72c0f17d6 100644 --- a/src/components/App/App.scss +++ b/src/components/App/App.scss @@ -25,8 +25,3 @@ min-height: 0; position: relative; } - -#assistant_wrapper { - padding: 0 1rem 1rem 0; - width: 100%; -} diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index fa096b787a..875e745840 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -123,9 +123,7 @@ export default function App() { size={showAssistant.fullScreen ? '100%' : '20%'} minSize={325} > -
- -
+ ) : ( <> From 9d001835ec311deeca29c20348b05cedaf62a26a Mon Sep 17 00:00:00 2001 From: chriskari Date: Fri, 12 Apr 2024 17:03:07 +0200 Subject: [PATCH 14/38] feat: added streaming of responses to UI --- .../AIassistant/api/getChatResponse.js | 59 +++++++++++++------ .../AIassistant/api/getPromptSuggestions.js | 25 ++++---- .../AIassistant/components/AIOpener.js | 2 +- .../AIassistant/components/Chat/Chat.js | 4 +- .../components/Chat/messages/Message.js | 6 ++ 5 files changed, 66 insertions(+), 30 deletions(-) diff --git a/src/components/AIassistant/api/getChatResponse.js b/src/components/AIassistant/api/getChatResponse.js index 7098ad0917..32fc28e1e0 100644 --- a/src/components/AIassistant/api/getChatResponse.js +++ b/src/components/AIassistant/api/getChatResponse.js @@ -3,21 +3,46 @@ export default async function getChatResponse({ handleSuccess, handleError, }) { - try { - const { response } = await fetch( - 'https://api-backend.c-5cb6076.stage.kyma.ondemand.com/api/v1/llm', - { - headers: { - accept: 'application/json, text/plain, */*', - 'content-type': 'application/json', - }, - body: `{"question":"${prompt}"}`, - method: 'POST', - }, - ).then(result => result.json()); - handleSuccess(response); - } catch (error) { - handleError(); - console.error('Error fetching data:', error); - } + const url = + 'https://api-backend.c-5cb6076.stage.kyma.ondemand.com/api/v1/chat'; /*'http://127.0.0.1:5000/api/v1/chat';*/ + const payload = { question: prompt, session_id: 'abcdef12345' }; + + fetch(url, { + headers: { + accept: 'application/json', + 'content-type': 'application/json', + }, + body: JSON.stringify(payload), + method: 'POST', + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + readChunk(reader, decoder, handleSuccess, handleError); + }) + .catch(error => { + handleError(); + console.error('Error fetching data:', error); + }); +} + +function readChunk(reader, decoder, handleSuccess, handleError) { + reader + .read() + .then(({ done, value }) => { + if (done) { + return; + } + const chunk = decoder.decode(value, { stream: true }); + console.log(chunk); + handleSuccess(chunk); + readChunk(reader, decoder, handleSuccess, handleError); + }) + .catch(error => { + handleError(); + console.error('Error reading stream:', error); + }); } diff --git a/src/components/AIassistant/api/getPromptSuggestions.js b/src/components/AIassistant/api/getPromptSuggestions.js index 1f8be0f754..715e6f9779 100644 --- a/src/components/AIassistant/api/getPromptSuggestions.js +++ b/src/components/AIassistant/api/getPromptSuggestions.js @@ -4,17 +4,22 @@ export default async function getPromptSuggestions({ resourceName = '', }) { try { - let { results } = await fetch( - 'https://api-backend.c-5cb6076.stage.kyma.ondemand.com/api/v1/llm/init', - { - headers: { - accept: 'application/json, text/plain, */*', - 'content-type': 'application/json', - }, - body: `{"resource_type":"${resourceType}","resource_name":"${resourceName}","namespace":"${namespace}"}`, - method: 'POST', + const url = + 'https://api-backend.c-5cb6076.stage.kyma.ondemand.com/api/v1/llm/init'; /*'http://127.0.0.1:5000/api/v1/llm/init';*/ + const payload = { + page_type: 'deployments.apps', + session_id: 'abcdef12345', + namespace: namespace, + }; //`{"resource_type":"${resourceType}","resource_name":"${resourceName}","namespace":"${namespace}"}` + + let { results } = await fetch(url, { + headers: { + accept: 'application/json, text/plain, */*', + 'content-type': 'application/json', }, - ).then(result => result.json()); + body: JSON.stringify(payload), + method: 'POST', + }).then(result => result.json()); return results; } catch (error) { console.error('Error fetching data:', error); diff --git a/src/components/AIassistant/components/AIOpener.js b/src/components/AIassistant/components/AIOpener.js index 97e2c29056..da8dbe7fa1 100644 --- a/src/components/AIassistant/components/AIOpener.js +++ b/src/components/AIassistant/components/AIOpener.js @@ -42,7 +42,7 @@ export default function AIOpener({ namespace, resourceType, resourceName }) { const fetchSuggestions = async () => { setErrorOccured(false); setPopoverOpen(true); - if (suggestions.length === 0) { + if (!isLoading && suggestions.length === 0) { setIsLoading(true); const promptSuggestions = await getPromptSuggestions({ namespace, diff --git a/src/components/AIassistant/components/Chat/Chat.js b/src/components/AIassistant/components/Chat/Chat.js index 7d1b5d3fa2..cf31d5d4a9 100644 --- a/src/components/AIassistant/components/Chat/Chat.js +++ b/src/components/AIassistant/components/Chat/Chat.js @@ -41,7 +41,7 @@ export default function Chat() { const sendPrompt = prompt => { setErrorOccured(false); - addMessage('user', prompt, false); + addMessage('user', { step: 'output', result: prompt }, false); getChatResponse({ prompt, handleSuccess, handleError }); addMessage('ai', null, true); }; @@ -100,7 +100,7 @@ export default function Chat() { suggestions={ message.suggestions ?? [ 'test123123123123123xyzxyzuwquxzytsabcde123456', - 'Throw an error', + "Hey, how's it going?", 'What is your favorite football team?', ] } diff --git a/src/components/AIassistant/components/Chat/messages/Message.js b/src/components/AIassistant/components/Chat/messages/Message.js index 3d45a4bf5a..cd6b1040f8 100644 --- a/src/components/AIassistant/components/Chat/messages/Message.js +++ b/src/components/AIassistant/components/Chat/messages/Message.js @@ -4,6 +4,12 @@ import CodePanel from './CodePanel'; import './Message.scss'; export default function Message({ className, message, isLoading }) { + if (typeof message === 'string') { + const jsonMessage = JSON.parse(message); + message = jsonMessage; + } + + message = message?.result; const segmentedText = segmentMarkdownText(message); return (
From e07c5bf0b2397fd0ef5bb08b1b3e5b723122079b Mon Sep 17 00:00:00 2001 From: chriskari Date: Sun, 14 Apr 2024 15:48:09 +0200 Subject: [PATCH 15/38] feat: added ui elements for streaming of message chunks --- .../AIassistant/api/getChatResponse.js | 3 +- .../AIassistant/components/Chat/Chat.js | 27 ++++++------ .../components/Chat/messages/Message.js | 42 +++++++++++++++---- .../components/Chat/messages/Message.scss | 21 ++++++++++ 4 files changed, 70 insertions(+), 23 deletions(-) diff --git a/src/components/AIassistant/api/getChatResponse.js b/src/components/AIassistant/api/getChatResponse.js index 32fc28e1e0..652e7aa479 100644 --- a/src/components/AIassistant/api/getChatResponse.js +++ b/src/components/AIassistant/api/getChatResponse.js @@ -36,8 +36,7 @@ function readChunk(reader, decoder, handleSuccess, handleError) { if (done) { return; } - const chunk = decoder.decode(value, { stream: true }); - console.log(chunk); + const chunk = JSON.parse(decoder.decode(value, { stream: true })); handleSuccess(chunk); readChunk(reader, decoder, handleSuccess, handleError); }) diff --git a/src/components/AIassistant/components/Chat/Chat.js b/src/components/AIassistant/components/Chat/Chat.js index cf31d5d4a9..5727b55611 100644 --- a/src/components/AIassistant/components/Chat/Chat.js +++ b/src/components/AIassistant/components/Chat/Chat.js @@ -18,19 +18,20 @@ export default function Chat() { const [errorOccured, setErrorOccured] = useState(false); const initialPrompt = useRecoilValue(initialPromptState); - const addMessage = (author, message, isLoading) => { - setChatHistory(prevItems => [...prevItems, { author, message, isLoading }]); + const addMessage = (author, messageChunks, isLoading) => { + setChatHistory(prevItems => + prevItems.concat({ author, messageChunks, isLoading }), + ); }; const handleSuccess = response => { - setChatHistory(prevItems => { - const newArray = [...prevItems]; - newArray[newArray.length - 1] = { + setChatHistory(prevMessages => { + const [latestMessage] = prevMessages.slice(-1); + return prevMessages.slice(0, -1).concat({ author: 'ai', - message: response, - isLoading: false, - }; - return newArray; + messageChunks: latestMessage.messageChunks.concat(response), + isLoading: response?.step !== 'output', + }); }); }; @@ -41,9 +42,9 @@ export default function Chat() { const sendPrompt = prompt => { setErrorOccured(false); - addMessage('user', { step: 'output', result: prompt }, false); + addMessage('user', [{ step: 'output', result: prompt }], false); getChatResponse({ prompt, handleSuccess, handleError }); - addMessage('ai', null, true); + addMessage('ai', [], true); }; const onSubmitInput = () => { @@ -90,7 +91,7 @@ export default function Chat() { {index === chatHistory.length - 1 && !message.isLoading && ( @@ -111,7 +112,7 @@ export default function Chat() { ); })} diff --git a/src/components/AIassistant/components/Chat/messages/Message.js b/src/components/AIassistant/components/Chat/messages/Message.js index cd6b1040f8..454e606074 100644 --- a/src/components/AIassistant/components/Chat/messages/Message.js +++ b/src/components/AIassistant/components/Chat/messages/Message.js @@ -1,19 +1,45 @@ -import { BusyIndicator, Text } from '@ui5/webcomponents-react'; +import { + BusyIndicator, + FlexBox, + ObjectStatus, + Text, +} from '@ui5/webcomponents-react'; import { segmentMarkdownText } from 'components/AIassistant/utils/formatMarkdown'; import CodePanel from './CodePanel'; import './Message.scss'; -export default function Message({ className, message, isLoading }) { - if (typeof message === 'string') { - const jsonMessage = JSON.parse(message); - message = jsonMessage; +export default function Message({ className, messageChunks, isLoading }) { + if (isLoading) { + return ( +
+ {messageChunks.length > 0 ? ( + messageChunks.map((chunk, index) => ( + + {chunk?.result} +
+ {index !== messageChunks.length - 1 ? ( + + ) : ( + + )} +
+
+ )) + ) : ( + + )} +
+ ); } - message = message?.result; - const segmentedText = segmentMarkdownText(message); + const segmentedText = segmentMarkdownText(messageChunks.slice(-1)[0]?.result); return (
- {isLoading && } {segmentedText && ( {segmentedText.map((segment, index) => diff --git a/src/components/AIassistant/components/Chat/messages/Message.scss b/src/components/AIassistant/components/Chat/messages/Message.scss index f7956eb857..4f86da9a05 100644 --- a/src/components/AIassistant/components/Chat/messages/Message.scss +++ b/src/components/AIassistant/components/Chat/messages/Message.scss @@ -2,6 +2,27 @@ max-width: 240px; padding: 12px; + &.loading { + display: flex; + flex-direction: column; + gap: 16px; + + .loading-item { + gap: 8px; + + .text { + flex-grow: 1; + } + + .loading-status { + display: flex; + align-items: center; + justify-content: center; + min-width: 35px; + } + } + } + .bold { font-weight: bold; font-size: 1rem; From 124abed1d898f6ddd8d395a9a4046206538eb313 Mon Sep 17 00:00:00 2001 From: chriskari Date: Mon, 15 Apr 2024 11:14:47 +0200 Subject: [PATCH 16/38] fix: adjusted styling in fullscreen-mode --- src/components/AIassistant/components/AIassistant.js | 6 ++++-- .../AIassistant/components/AIassistant.scss | 12 ++++++++++++ src/components/AIassistant/components/Chat/Chat.js | 7 ++++--- .../AIassistant/components/Chat/messages/Bubbles.js | 8 ++++++-- .../components/Chat/messages/Bubbles.scss | 4 ++++ .../components/Chat/messages/Message.scss | 4 ++++ 6 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/components/AIassistant/components/AIassistant.js b/src/components/AIassistant/components/AIassistant.js index e78a47906f..5266bb01f9 100644 --- a/src/components/AIassistant/components/AIassistant.js +++ b/src/components/AIassistant/components/AIassistant.js @@ -60,10 +60,12 @@ export default function AIassistant() { - + diff --git a/src/components/AIassistant/components/AIassistant.scss b/src/components/AIassistant/components/AIassistant.scss index c85a662389..d9fdfe529c 100644 --- a/src/components/AIassistant/components/AIassistant.scss +++ b/src/components/AIassistant/components/AIassistant.scss @@ -24,5 +24,17 @@ .tab-container { height: calc(100vh - 60px - 1.4rem); } + + .tab-container::part(content) { + width: 100%; + margin-left: 0; + margin-right: 0; + } + + .tab-container.fullscreen::part(content) { + width: 60%; + margin-left: 20%; + margin-right: 20%; + } } } diff --git a/src/components/AIassistant/components/Chat/Chat.js b/src/components/AIassistant/components/Chat/Chat.js index 5727b55611..cb67b900cd 100644 --- a/src/components/AIassistant/components/Chat/Chat.js +++ b/src/components/AIassistant/components/Chat/Chat.js @@ -10,7 +10,7 @@ import ErrorMessage from './messages/ErrorMessage'; import getChatResponse from 'components/AIassistant/api/getChatResponse'; import './Chat.scss'; -export default function Chat() { +export default function Chat({ isFullScreen }) { const { t } = useTranslation(); const containerRef = useRef(null); const [inputValue, setInputValue] = useState(''); @@ -90,13 +90,14 @@ export default function Chat() { <> {index === chatHistory.length - 1 && !message.isLoading && ( ); diff --git a/src/components/AIassistant/components/Chat/messages/Bubbles.js b/src/components/AIassistant/components/Chat/messages/Bubbles.js index 3386201850..470fce12d3 100644 --- a/src/components/AIassistant/components/Chat/messages/Bubbles.js +++ b/src/components/AIassistant/components/Chat/messages/Bubbles.js @@ -1,9 +1,13 @@ import { Button, FlexBox } from '@ui5/webcomponents-react'; import './Bubbles.scss'; -export default function Bubbles({ suggestions, onClick }) { +export default function Bubbles({ suggestions, onClick, className }) { return ( - + {suggestions.map((suggestion, index) => (
diff --git a/src/components/AIassistant/components/AIassistant.scss b/src/components/AIassistant/components/AIassistant.scss index d9fdfe529c..99d0f4f2bd 100644 --- a/src/components/AIassistant/components/AIassistant.scss +++ b/src/components/AIassistant/components/AIassistant.scss @@ -24,17 +24,21 @@ .tab-container { height: calc(100vh - 60px - 1.4rem); } + } +} - .tab-container::part(content) { - width: 100%; - margin-left: 0; - margin-right: 0; - } +@container (max-width: 950px) { + .tab-container::part(content) { + width: 100%; + margin-left: 0; + margin-right: 0; + } +} - .tab-container.fullscreen::part(content) { - width: 60%; - margin-left: 20%; - margin-right: 20%; - } +@container (min-width: 950px) { + .tab-container::part(content) { + width: 70%; + margin-left: 15%; + margin-right: 15%; } } diff --git a/src/components/AIassistant/components/Chat/Chat.js b/src/components/AIassistant/components/Chat/Chat.js index 0164629c4b..63718fe632 100644 --- a/src/components/AIassistant/components/Chat/Chat.js +++ b/src/components/AIassistant/components/Chat/Chat.js @@ -12,7 +12,7 @@ import { sessionIDState } from 'components/AIassistant/state/sessionIDAtom'; import getFollowUpQuestions from 'components/AIassistant/api/getFollowUpQuestions'; import './Chat.scss'; -export default function Chat({ isFullScreen }) { +export default function Chat() { const { t } = useTranslation(); const containerRef = useRef(null); const [inputValue, setInputValue] = useState(''); @@ -106,14 +106,13 @@ export default function Chat({ isFullScreen }) { <> {index === chatHistory.length - 1 && !message.isLoading && ( @@ -122,7 +121,7 @@ export default function Chat({ isFullScreen }) { ) : ( ); diff --git a/src/components/AIassistant/components/Chat/messages/Bubbles.js b/src/components/AIassistant/components/Chat/messages/Bubbles.js index 309af694ae..1057e1171d 100644 --- a/src/components/AIassistant/components/Chat/messages/Bubbles.js +++ b/src/components/AIassistant/components/Chat/messages/Bubbles.js @@ -1,13 +1,9 @@ import { Button, FlexBox } from '@ui5/webcomponents-react'; import './Bubbles.scss'; -export default function Bubbles({ suggestions, onClick, className }) { +export default function Bubbles({ suggestions, onClick }) { return suggestions ? ( - + {suggestions.map((suggestion, index) => (