diff --git a/libs/chatbot/lib/components/ChatBot/BotMessage.tsx b/libs/chatbot/lib/components/ChatBot/BotMessage.tsx index ea6ee11afa..2a1d031a8d 100644 --- a/libs/chatbot/lib/components/ChatBot/BotMessage.tsx +++ b/libs/chatbot/lib/components/ChatBot/BotMessage.tsx @@ -46,7 +46,7 @@ const getActions = (text: string, onActionClick: SentimentActionClick) => ({ const userFeedbackForm = ( onSubmit: (quickResponse: string | undefined, additionalFeedback: string | undefined) => void, onClose: VoidFunction, -) => ({ +): UserFeedbackProps => ({ onClose, onSubmit, title: 'Please provide feedback', @@ -54,38 +54,45 @@ const userFeedbackForm = ( textAreaPlaceholder: 'Add details here', hasTextArea: true, closeButtonAriaLabel: 'Close feedback form', - focusOnLoad: false, + focusOnLoad: true, }); export type BotMessageProps = { onFeedbackSubmit: (req: FeedbackRequest) => Promise; messageIndex: number; message: MsgProps; - onScrollToBottom: () => void; isLoading: boolean; + isLastMsg: boolean; + initHeight?: number; }; const BotMessage = ({ onFeedbackSubmit, messageIndex, message, - onScrollToBottom, isLoading, + initHeight, + isLastMsg, }: BotMessageProps) => { + const [height, setHeight] = React.useState(initHeight); const [isNegativeFeedback, setIsNegativeFeedback] = React.useState(false); + const msgRef = React.useRef(null); + const scrollToMsgRef = React.useRef(null); // Scroll to bottom when negative feedback form opens - React.useEffect(() => { + React.useLayoutEffect(() => { if (isNegativeFeedback) { - // Use requestAnimationFrame to ensure the form is rendered and painted - requestAnimationFrame(() => { - // Double RAF to ensure layout is complete - requestAnimationFrame(() => { - onScrollToBottom(); - }); - }); + scrollToMsgRef.current?.scrollIntoView({ behavior: 'smooth' }); } - }, [isNegativeFeedback, onScrollToBottom]); + }, [isNegativeFeedback]); + + // run on every re-render + // eslint-disable-next-line react-hooks/exhaustive-deps + React.useLayoutEffect(() => { + if (height && !isLoading && msgRef.current && msgRef.current.scrollHeight > height) { + setHeight(undefined); + } + }); const actions = React.useMemo(() => { return getActions(message.pfProps.content || '', (positiveFeedback) => { @@ -133,37 +140,45 @@ const BotMessage = ({ }, [isNegativeFeedback, onFeedbackSubmit, messageIndex]); return ( - - ) : message.actions?.length ? ( - - {message.actions.map(({ title, url }, idx) => ( - - - - ))} - - ) : undefined, - }} - /> + <> + +
+ {isLoading && } + {!isLoading && message.actions?.length && ( + + {message.actions.map(({ title, url }, idx) => ( + + + + ))} + + )} + + ), + }} + /> + ); }; diff --git a/libs/chatbot/lib/components/ChatBot/ChatBotWindow.tsx b/libs/chatbot/lib/components/ChatBot/ChatBotWindow.tsx index 59e7dc6922..b10f8aaa18 100644 --- a/libs/chatbot/lib/components/ChatBot/ChatBotWindow.tsx +++ b/libs/chatbot/lib/components/ChatBot/ChatBotWindow.tsx @@ -12,6 +12,7 @@ import { Message, MessageBar, MessageBox, + MessageBoxHandle, } from '@patternfly-6/chatbot'; import { Alert, @@ -69,6 +70,7 @@ const ChatBotWindow = ({ onClose, username, }: ChatBotWindowProps) => { + const [triggerScroll, setTriggerScroll] = React.useState(0); const [msg, setMsg] = React.useState(''); const [error, setError] = React.useState(); const [isStreaming, setIsStreaming] = React.useState(false); @@ -78,7 +80,8 @@ const ChatBotWindow = ({ ); const [isConfirmModalOpen, setIsConfirmModalOpen] = React.useState(false); const scrollToBottomRef = React.useRef(null); - const hasInitiallyScrolled = React.useRef(false); + const lastUserMsgRef = React.useRef(null); + const msgBoxRef = React.useRef(null); React.useEffect(() => { !isConfirmModalOpen && focusNewMessageBox(); @@ -99,16 +102,26 @@ const ChatBotWindow = ({ } }; - const scrollToBottom = React.useCallback((behavior: ScrollBehavior = 'smooth') => { - scrollToBottomRef.current?.scrollIntoView({ behavior }); - }, []); - React.useEffect(() => { - // Determine scroll behavior: auto for initial render with existing messages, smooth for new content - const scrollBehavior = !hasInitiallyScrolled.current && messages.length > 0 ? 'auto' : 'smooth'; - scrollToBottom(scrollBehavior); - hasInitiallyScrolled.current = true; - }, [messages, scrollToBottom]); + if (triggerScroll === 0) { + scrollToBottomRef.current?.scrollIntoView({ behavior: 'auto' }); + } else { + const msgTop = lastUserMsgRef.current?.offsetTop; + if (msgTop !== undefined && msgBoxRef.current) { + msgBoxRef.current.scrollTo({ + top: msgTop, + behavior: 'smooth', + }); + } + } + }, [triggerScroll]); + + const getVisibleHeight = () => { + if (lastUserMsgRef.current && msgBoxRef.current) { + return msgBoxRef.current.clientHeight - lastUserMsgRef.current.clientHeight - 64; + } + return undefined; + }; const handleSend = async (message: string | number) => { setError(undefined); @@ -117,6 +130,7 @@ const ChatBotWindow = ({ let eventEnded = false; const timestamp = new Date().toLocaleString(); try { + setAnnouncement(`Message from User: ${message}. Message from Bot is loading.`); setMessages((msgs) => [ ...msgs, { @@ -128,13 +142,6 @@ const ChatBotWindow = ({ timestamp, }, }, - ]); - setAnnouncement(`Message from User: ${message}. Message from Bot is loading.`); - - let convId = ''; - - setMessages((msgs) => [ - ...msgs, { pfProps: { role: botRole, @@ -145,6 +152,9 @@ const ChatBotWindow = ({ }, }, ]); + setTriggerScroll(triggerScroll + 1); + + let convId = ''; const resp = await onApiCall('/v1/streaming_query', { method: 'POST', @@ -291,6 +301,8 @@ const ChatBotWindow = ({ [onApiCall, conversationId, messages], ); + const lastUserMsg = [...messages].reverse().findIndex((msg) => msg.pfProps.role === userRole); + return ( @@ -325,7 +337,7 @@ const ChatBotWindow = ({ - + {isAlertVisible && ( )} {messages.map((message, index) => { + const isLastMsg = index === messages.length - 1; const messageKey = conversationId ? `${conversationId}-${index}` : index; const isBotMessage = message.pfProps.role === botRole; if (isBotMessage) { @@ -375,13 +388,20 @@ const ChatBotWindow = ({ messageIndex={index} message={message} onFeedbackSubmit={onFeedbackSubmit} - onScrollToBottom={scrollToBottom} isLoading={index === messages.length - 1 && isStreaming} + initHeight={isLastMsg ? getVisibleHeight() : undefined} + isLastMsg={isLastMsg} /> ); } - return ; + return ( + + ); })} {error && (