From d7a7c7501558463fabbdc1f8abdcf52c511678f2 Mon Sep 17 00:00:00 2001 From: rawagner Date: Tue, 9 Sep 2025 12:26:07 +0200 Subject: [PATCH] Add MessageEntry component for unified chatbot --- .../lib/components/ChatBot/MessageEntry.tsx | 172 ++++++++++++++++++ .../chatbot/lib/components/ChatBot/helpers.ts | 2 +- libs/chatbot/lib/index.ts | 2 + libs/chatbot/package.json | 2 + yarn.lock | 27 +++ 5 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 libs/chatbot/lib/components/ChatBot/MessageEntry.tsx diff --git a/libs/chatbot/lib/components/ChatBot/MessageEntry.tsx b/libs/chatbot/lib/components/ChatBot/MessageEntry.tsx new file mode 100644 index 0000000000..b161e9f1ba --- /dev/null +++ b/libs/chatbot/lib/components/ChatBot/MessageEntry.tsx @@ -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; + 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 => { + 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( + (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 ( + <> + + {actions?.length && ( + + {actions.map(({ title, url, clusterId }, idx) => ( + + {url && ( + + )} + {clusterId && ( + + )} + + ))} + + )} + + ), + endContent: openFeedback && ( + { + await onFeedbackSubmit(req); + setOpenFeedback(false); + }} + onClose={() => setOpenFeedback(false)} + /> + ), + }} + /> + + ); +}; + +export default MessageEntry; diff --git a/libs/chatbot/lib/components/ChatBot/helpers.ts b/libs/chatbot/lib/components/ChatBot/helpers.ts index af92bb5e83..52b605c4aa 100644 --- a/libs/chatbot/lib/components/ChatBot/helpers.ts +++ b/libs/chatbot/lib/components/ChatBot/helpers.ts @@ -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, 'role' | 'content' | 'timestamp'>; diff --git a/libs/chatbot/lib/index.ts b/libs/chatbot/lib/index.ts index 488fe178f2..a7a0c281ea 100644 --- a/libs/chatbot/lib/index.ts +++ b/libs/chatbot/lib/index.ts @@ -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'; diff --git a/libs/chatbot/package.json b/libs/chatbot/package.json index d6c37dc62a..fb12d7574a 100644 --- a/libs/chatbot/package.json +++ b/libs/chatbot/package.json @@ -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" diff --git a/yarn.lock b/yarn.lock index 547875bfaf..f595a195c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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 @@ -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"