diff --git a/public/i18n/en.yaml b/public/i18n/en.yaml index 6fd8164d11..b9ccaba157 100644 --- a/public/i18n/en.yaml +++ b/public/i18n/en.yaml @@ -767,6 +767,8 @@ kyma-companion: error: title: Service is interrupted subtitle: A temporary interruption occured. Please try again. + chat-error: An error occurred + suggestions-error: No suggestions available introduction: Hi, I am your Kyma assistant! You can ask me any question, and I will try to help you to the best of my abilities. Meanwhile, you can check the suggested questions below; you may find them helpful! introduction-no-suggestions: Hi, I am your Kyma assistant! You can ask me any question, and I will try to help you to the best of my abilities. While I don't have any initial suggestions for this resource, feel free to ask me anything you'd like! placeholder: Message Joule diff --git a/src/components/KymaCompanion/api/getChatResponse.ts b/src/components/KymaCompanion/api/getChatResponse.ts index 741420f9ce..1b101da87f 100644 --- a/src/components/KymaCompanion/api/getChatResponse.ts +++ b/src/components/KymaCompanion/api/getChatResponse.ts @@ -15,7 +15,7 @@ type GetChatResponseArgs = { resourceName?: string; sessionID: string; handleChatResponse: (chunk: MessageChunk) => void; - handleError: (error?: Error) => void; + handleError: (error?: string) => void; clusterUrl: string; clusterAuth: ClusterAuth; certificateAuthorityData: string; @@ -87,7 +87,7 @@ function readChunk( reader: ReadableStreamDefaultReader, decoder: TextDecoder, handleChatResponse: (chunk: any) => void, - handleError: (error?: Error) => void, + handleError: (error?: string) => void, sessionID: string, ) { reader diff --git a/src/components/KymaCompanion/api/getFollowUpQuestions.ts b/src/components/KymaCompanion/api/getFollowUpQuestions.ts index 709994a6b6..eb96999f67 100644 --- a/src/components/KymaCompanion/api/getFollowUpQuestions.ts +++ b/src/components/KymaCompanion/api/getFollowUpQuestions.ts @@ -3,7 +3,7 @@ import { getClusterConfig } from 'state/utils/getBackendInfo'; interface GetFollowUpQuestionsParams { sessionID?: string; handleFollowUpQuestions: (results: any) => void; - handleError: (error?: Error) => void; + handleFollowUpError: () => void; clusterUrl: string; token: string; certificateAuthorityData: string; @@ -12,7 +12,7 @@ interface GetFollowUpQuestionsParams { export default async function getFollowUpQuestions({ sessionID = '', handleFollowUpQuestions, - handleError, + handleFollowUpError, clusterUrl, token, certificateAuthorityData, @@ -35,10 +35,13 @@ export default async function getFollowUpQuestions({ }); const promptSuggestions = (await response.json()).promptSuggestions; + if (!promptSuggestions) { + throw new Error('No follow-up questions available'); + } handleFollowUpQuestions(promptSuggestions); } catch (error) { - handleError(); + handleFollowUpError(); console.error('Error fetching data:', error); } } diff --git a/src/components/KymaCompanion/components/Chat/Chat.scss b/src/components/KymaCompanion/components/Chat/Chat.scss index 239efae6f1..b736b7c50b 100644 --- a/src/components/KymaCompanion/components/Chat/Chat.scss +++ b/src/components/KymaCompanion/components/Chat/Chat.scss @@ -12,17 +12,14 @@ display: none; } - .left-aligned { + .message-container.left-aligned { position: relative; align-self: flex-start; - border-radius: 8px 8px 8px 0; } - .right-aligned { + .message-container.right-aligned { position: relative; align-self: flex-end; - border-radius: 8px 8px 0 8px; - background-color: var(--sapAssistant_Question_Background); } } diff --git a/src/components/KymaCompanion/components/Chat/Chat.tsx b/src/components/KymaCompanion/components/Chat/Chat.tsx index fd74891475..7ad7be8219 100644 --- a/src/components/KymaCompanion/components/Chat/Chat.tsx +++ b/src/components/KymaCompanion/components/Chat/Chat.tsx @@ -11,6 +11,7 @@ import { authDataState } from 'state/authDataAtom'; import getFollowUpQuestions from 'components/KymaCompanion/api/getFollowUpQuestions'; import getChatResponse from 'components/KymaCompanion/api/getChatResponse'; import { usePromptSuggestions } from 'components/KymaCompanion/hooks/usePromptSuggestions'; +import { AIError } from '../KymaCompanion'; import './Chat.scss'; export interface MessageType { @@ -19,6 +20,7 @@ export interface MessageType { isLoading: boolean; suggestions?: string[]; suggestionsLoading?: boolean; + hasError?: boolean | undefined; } type ChatProps = { @@ -28,8 +30,8 @@ type ChatProps = { setLoading: React.Dispatch>; isReset: boolean; setIsReset: React.Dispatch>; - error: string | null; - setError: React.Dispatch>; + error: AIError; + setError: React.Dispatch>; }; export const Chat = ({ @@ -78,18 +80,34 @@ export const Chat = ({ const handleChatResponse = (response: MessageChunk) => { const isLoading = response?.data?.answer?.next !== '__end__'; + if (!isLoading) { - setFollowUpLoading(); - getFollowUpQuestions({ - sessionID, - handleFollowUpQuestions, - handleError, - clusterUrl: cluster.currentContext.cluster.cluster.server, - token: authData.token, - certificateAuthorityData: - cluster.currentContext.cluster.cluster['certificate-authority-data'], - }); + const finalTask = response.data.answer?.tasks?.at(-1); + const hasError = finalTask?.status === 'error'; + + if (hasError) { + const allTasksError = + response.data.answer?.tasks?.every(task => task.status === 'error') ?? + false; + const displayRetry = response.data.error !== null || allTasksError; + handleError(response.data.answer.content, displayRetry); + return; + } else { + setFollowUpLoading(); + getFollowUpQuestions({ + sessionID, + handleFollowUpQuestions, + handleFollowUpError, + clusterUrl: cluster.currentContext.cluster.cluster.server, + token: authData.token, + certificateAuthorityData: + cluster.currentContext.cluster.cluster[ + 'certificate-authority-data' + ], + }); + } } + setChatHistory(prevMessages => { const [latestMessage] = prevMessages.slice(-1); return prevMessages.slice(0, -1).concat({ @@ -101,7 +119,7 @@ export const Chat = ({ }; const setFollowUpLoading = () => { - setError(null); + setError({ message: null, displayRetry: false }); setLoading(true); updateLatestMessage({ suggestionsLoading: true }); }; @@ -111,14 +129,36 @@ export const Chat = ({ setLoading(false); }; - const handleError = (error?: Error) => { - setError(error?.message ?? t('kyma-companion.error.subtitle')); + const handleFollowUpError = () => { + updateLatestMessage({ + hasError: true, + suggestionsLoading: false, + }); + setLoading(false); + }; + + const handleError = (error?: string, displayRetry?: boolean) => { + const errorMessage = error ?? t('kyma-companion.error.subtitle') ?? ''; + setError({ + message: errorMessage, + displayRetry: displayRetry ?? false, + }); setChatHistory(prevItems => prevItems.slice(0, -1)); + updateLatestMessage({ hasError: true }); setLoading(false); }; + const retryPreviousPrompt = () => { + const previousPrompt = chatHistory.at(-1)?.messageChunks[0].data.answer + .content; + if (previousPrompt) { + setChatHistory(prevItems => prevItems.slice(0, -1)); + sendPrompt(previousPrompt); + } + }; + const sendPrompt = (query: string) => { - setError(null); + setError({ message: null, displayRetry: false }); setLoading(true); addMessage({ author: 'user', @@ -225,12 +265,15 @@ export const Chat = ({ ref={containerRef} > {chatHistory.map((message, index) => { + const isLast = index === chatHistory.length - 1; return message.author === 'ai' ? ( {index === chatHistory.length - 1 && !message.isLoading && ( ) : ( ); })} - {error && ( + {error.message && ( {}} + errorMessage={error.message ?? t('kyma-companion.error.subtitle')} + retryPrompt={() => retryPreviousPrompt()} + displayRetry={error.displayRetry} /> )} diff --git a/src/components/KymaCompanion/components/Chat/messages/ErrorMessage.tsx b/src/components/KymaCompanion/components/Chat/messages/ErrorMessage.tsx index c92725f92e..c1d936903d 100644 --- a/src/components/KymaCompanion/components/Chat/messages/ErrorMessage.tsx +++ b/src/components/KymaCompanion/components/Chat/messages/ErrorMessage.tsx @@ -1,30 +1,38 @@ -import { Button, Card, IllustratedMessage } from '@ui5/webcomponents-react'; +import { + Button, + Card, + IllustratedMessage, + Text, +} from '@ui5/webcomponents-react'; import { useTranslation } from 'react-i18next'; interface ErrorMessageProps { errorMessage: string; - errorOnInitialMessage: boolean; + displayRetry: boolean; retryPrompt: () => void; } export default function ErrorMessage({ errorMessage, - errorOnInitialMessage, + displayRetry, retryPrompt, }: ErrorMessageProps): JSX.Element { const { t } = useTranslation(); return ( -
+
{errorMessage} + } className="sap-margin-top-small no-padding" > - {errorOnInitialMessage && ( + {displayRetry && (