Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions libs/chatbot/lib/components/ChatBot/MessageEntry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import * as React from 'react';
import { Message as MessageType } from '@redhat-cloud-services/ai-client-state';
import { LightSpeedCoreAdditionalProperties } from '@redhat-cloud-services/lightspeed-client';
import { Message } from '@patternfly/chatbot';
import { saveAs } from 'file-saver';
import { Button, Stack, StackItem } from '@patternfly-6/react-core';
import { DownloadIcon, ExternalLinkAltIcon } from '@patternfly-6/react-icons';

import { isToolArgStreamEvent, isToolResponseStreamEvent, StreamEvent } from './types';
import { getToolAction, MsgAction } from './helpers';
import FeedbackForm from './FeedbackCard';
import { FeedbackRequest } from './BotMessage';

export type MessageEntryProps = {
openClusterDetails: (clusterId: string) => void;
message: MessageType<LightSpeedCoreAdditionalProperties>;
avatar: string;
onApiCall: typeof fetch;
};

const MessageEntry = ({ message, avatar, openClusterDetails, onApiCall }: MessageEntryProps) => {
const [openFeedback, setOpenFeedback] = React.useState(false);
const onFeedbackSubmit = React.useCallback(
async (req: FeedbackRequest): Promise<void> => {
const resp = await onApiCall('/v1/feedback', {
method: 'POST',
body: JSON.stringify({
conversation_id: message.additionalAttributes?.conversationId,
user_question: 'TODO',
user_feedback: req.userFeedback,
llm_response: message.answer,
sentiment: req.sentiment,
category: req.category,
}),
headers: {
'Content-Type': 'application/json',
},
});
if (!resp.ok) {
throw new Error(`${resp.status} ${resp.statusText}`);
}
},
[onApiCall, message],
);

const messageDate = `${message.date?.toLocaleDateString()} ${message.date?.toLocaleTimeString()}`;
const isLoading = message.role === 'bot' && message.answer === '';

const toolArgs: { [key: string]: { [key: string]: string } } = {};
const actions =
message.role === 'user' || isLoading
? []
: (message.additionalAttributes?.toolCalls as StreamEvent[])?.reduce<MsgAction[]>(
(acc, ev) => {
if (isToolArgStreamEvent(ev)) {
toolArgs[ev.data.id] = ev.data.token.arguments;
} else if (isToolResponseStreamEvent(ev)) {
const action = getToolAction({
toolName: ev.data.token.tool_name,
response: ev.data.token.response,
args: toolArgs[ev.data.id],
});
if (action) {
acc.push(action);
}
}
return acc;
},
[],
);

const feedback =
message.role === 'user' || isLoading
? undefined
: {
positive: {
ariaLabel: 'Good response',
tooltipContent: 'Good response',
clickedTooltipContent: 'Feedback sent',
onClick: () => {
void onFeedbackSubmit({
userFeedback: '',
sentiment: 1,
});
},
},
negative: {
ariaLabel: 'Bad response',
tooltipContent: 'Bad response',
clickedTooltipContent: 'Feedback sent',
onClick: () => setOpenFeedback(true),
},
copy: {
isDisabled: !message.answer,
onClick: () => {
void navigator.clipboard.writeText(message.answer || '');
},
},
};

return (
<>
<Message
id={`message-${message.id}`}
// Don't want users to paste MD and display it
isMarkdownDisabled={message.role === 'user'}
isLoading={isLoading}
role={message.role}
avatar={avatar}
content={message.answer}
aria-label={`${message.role === 'user' ? 'Your message' : 'AI response'}: ${
message.answer
}`}
timestamp={messageDate}
actions={feedback}
extraContent={{
afterMainContent: (
<>
{actions?.length && (
<Stack hasGutter>
{actions.map(({ title, url, clusterId }, idx) => (
<StackItem key={idx}>
{url && (
<Button
onClick={(e) => {
e.preventDefault();
try {
saveAs(url);
} catch (error) {
// eslint-disable-next-line
console.error('Download failed: ', error);
}
}}
variant="secondary"
component="a"
href={url}
icon={<DownloadIcon />}
>
{title}
</Button>
)}
{clusterId && (
<Button
onClick={() => openClusterDetails(clusterId)}
variant="secondary"
icon={<ExternalLinkAltIcon />}
>
{title}
</Button>
)}
</StackItem>
))}
</Stack>
)}
</>
),
endContent: openFeedback && (
<FeedbackForm
onFeedbackSubmit={async (req: FeedbackRequest) => {
await onFeedbackSubmit(req);
setOpenFeedback(false);
}}
onClose={() => setOpenFeedback(false)}
/>
),
}}
/>
</>
);
};

export default MessageEntry;
2 changes: 1 addition & 1 deletion libs/chatbot/lib/components/ChatBot/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import isString from 'lodash-es/isString.js';
import { Message } from '@patternfly/chatbot';
import { validate as uuidValidate } from 'uuid';

type MsgAction = { title: string; url?: string; clusterId?: string };
export type MsgAction = { title: string; url?: string; clusterId?: string };

export type MsgProps = {
pfProps: Pick<React.ComponentProps<typeof Message>, 'role' | 'content' | 'timestamp'>;
Expand Down
2 changes: 2 additions & 0 deletions libs/chatbot/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { default as ChatBot } from './components/ChatBot/ChatBot';
export type { ChatBotWindowProps } from './components/ChatBot/ChatBotWindow';
export { default as MessageEntry } from './components/ChatBot/MessageEntry';
export type { MessageEntryProps } from './components/ChatBot/MessageEntry';
2 changes: 2 additions & 0 deletions libs/chatbot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
"@patternfly/react-icons": "6.3.1",
"@patternfly/react-styles": "6.3.1",
"@patternfly/react-tokens": "6.3.1",
"@redhat-cloud-services/ai-client-state": "^0.14.0",
"@redhat-cloud-services/lightspeed-client": "^0.14.0",
"file-saver": "^2.0.2",
"lodash-es": "^4.17.21",
"uuid": "^8.1"
Expand Down
27 changes: 27 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,8 @@ __metadata:
"@patternfly/react-icons": 6.3.1
"@patternfly/react-styles": 6.3.1
"@patternfly/react-tokens": 6.3.1
"@redhat-cloud-services/ai-client-state": ^0.14.0
"@redhat-cloud-services/lightspeed-client": ^0.14.0
"@tsconfig/recommended": ^1.0.2
"@types/node": ^18.14.6
"@types/react": ^18.0.0
Expand Down Expand Up @@ -1369,6 +1371,31 @@ __metadata:
languageName: node
linkType: hard

"@redhat-cloud-services/ai-client-common@npm:0.13.0":
version: 0.13.0
resolution: "@redhat-cloud-services/ai-client-common@npm:0.13.0"
checksum: 07f27af3be76e7f4b06212f72414e5804ae37d506f75ca4c43631f15e70aca142e91471442a32f9b03e4b43d8933eea2bd2a4c7aaba3e6def387973402b6df84
languageName: node
linkType: hard

"@redhat-cloud-services/ai-client-state@npm:^0.14.0":
version: 0.14.0
resolution: "@redhat-cloud-services/ai-client-state@npm:0.14.0"
dependencies:
"@redhat-cloud-services/ai-client-common": 0.13.0
checksum: 0bf91b1289da3fb4a6702f11cc3c5d39d0e31d069cf7a857e3a87e2e920fa2f55cdbe3ab19ee4754652428d2d3fb728ef626c0a1d4c67e0aac7796814fc25068
languageName: node
linkType: hard

"@redhat-cloud-services/lightspeed-client@npm:^0.14.0":
version: 0.14.0
resolution: "@redhat-cloud-services/lightspeed-client@npm:0.14.0"
dependencies:
"@redhat-cloud-services/ai-client-common": 0.13.0
checksum: a57b602e277f4720e4037e689a0f95d2be234849f013da1af30de2ba94d78c13855639df80e9766a3c79cca209d27911377ff4158f0d1a527b55d6af5b54b441
languageName: node
linkType: hard

"@redhat-cloud-services/types@npm:^1.0.1":
version: 1.0.7
resolution: "@redhat-cloud-services/types@npm:1.0.7"
Expand Down
Loading