diff --git a/workspaces/lightspeed/.changeset/tasty-pumpkins-design.md b/workspaces/lightspeed/.changeset/tasty-pumpkins-design.md new file mode 100644 index 0000000000..ff9eb3de7b --- /dev/null +++ b/workspaces/lightspeed/.changeset/tasty-pumpkins-design.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-lightspeed': patch +--- + +Render new lines in deepThinking component diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBox.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBox.tsx index 072ccf8b48..7d4195a8a1 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBox.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBox.tsx @@ -17,6 +17,7 @@ import { ForwardedRef, forwardRef, + Fragment, useEffect, useImperativeHandle, useRef, @@ -247,9 +248,18 @@ export const LightspeedChatBox = forwardRef( })(); if (reasoningContent) { + const reasoningBody = reasoningContent + .split('\n') + .map((line, lineIndex, array) => ( + + {line} + {lineIndex < array.length - 1 &&
} +
+ )); + deepThinking = { toggleContent: t('reasoning.thinking'), - body: reasoningContent, + body: reasoningBody, expandableSectionProps: {}, }; extraContentParts.beforeMainContent = ( diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/LightspeedChatBox.test.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/LightspeedChatBox.test.tsx new file mode 100644 index 0000000000..33f3808fe9 --- /dev/null +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/LightspeedChatBox.test.tsx @@ -0,0 +1,176 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ChatbotDisplayMode } from '@patternfly/chatbot'; +import { render, screen } from '@testing-library/react'; + +import { mockUseTranslation } from '../../test-utils/mockTranslations'; +import { LightspeedChatBox } from '../LightspeedChatBox'; + +jest.mock('../../hooks/useTranslation', () => ({ + useTranslation: jest.fn(() => mockUseTranslation()), +})); + +jest.mock('../../hooks/useAutoScroll', () => ({ + useAutoScroll: jest.fn(() => ({ + autoScroll: true, + resumeAutoScroll: jest.fn(), + stopAutoScroll: jest.fn(), + scrollToBottom: jest.fn(), + scrollToTop: jest.fn(), + })), +})); + +jest.mock('../../hooks/useBufferedMessages', () => ({ + useBufferedMessages: jest.fn(messages => { + return messages || []; + }), +})); + +jest.mock('../../hooks/useFeedbackActions', () => ({ + useFeedbackActions: jest.fn(messages => { + return messages || []; + }), +})); + +jest.mock('@patternfly/chatbot', () => { + const actual = jest.requireActual('@patternfly/chatbot'); + return { + ...actual, + DeepThinking: ({ body }: { body: React.ReactNode }) => ( +
{body}
+ ), + MessageBox: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + Message: (props: any) => ( +
+ {props.extraContent?.beforeMainContent} + {props.content} + {props.extraContent?.afterMainContent} +
+ ), + }; +}); + +describe('LightspeedChatBox', () => { + const defaultProps = { + userName: 'user:test', + messages: [], + announcement: undefined, + conversationId: 'test-conversation-id', + profileLoading: false, + welcomePrompts: [], + isStreaming: false, + topicRestrictionEnabled: false, + displayMode: ChatbotDisplayMode.embedded, + }; + + it('should render reasoning content with newlines as line breaks', () => { + const messagesWithReasoning = [ + { + role: 'bot' as const, + content: 'Line 1\nLine 2\nLine 3Main response content', + timestamp: '2026-01-22T00:00:00Z', + }, + ]; + + render( + , + ); + + const deepThinking = screen.getByTestId('deep-thinking'); + expect(deepThinking).toBeInTheDocument(); + + // Check that the reasoning body contains line breaks + const reasoningContent = deepThinking.textContent; + expect(reasoningContent).toContain('Line 1'); + expect(reasoningContent).toContain('Line 2'); + expect(reasoningContent).toContain('Line 3'); + + const brElements = deepThinking.querySelectorAll('br'); + expect(brElements.length).toBe(2); + }); + + it('should handle reasoning content without newlines', () => { + const messagesWithReasoning = [ + { + role: 'bot' as const, + content: 'Single line reasoningMain response', + name: 'assistant', + timestamp: '2024-01-01T00:00:00Z', + }, + ]; + + render( + , + ); + + const deepThinking = screen.getByTestId('deep-thinking'); + expect(deepThinking).toBeInTheDocument(); + expect(deepThinking).toHaveTextContent('Single line reasoning'); + + const brElements = deepThinking.querySelectorAll('br'); + expect(brElements.length).toBe(0); + }); + + it('should handle reasoning in progress with newlines', () => { + const messagesWithReasoningInProgress = [ + { + role: 'bot' as const, + content: 'Line 1\nLine 2\nLine 3', + timestamp: '2024-01-01T00:00:00Z', + name: 'assistant', + }, + ]; + + render( + , + ); + + const deepThinking = screen.getByTestId('deep-thinking'); + expect(deepThinking).toBeInTheDocument(); + + const brElements = deepThinking.querySelectorAll('br'); + expect(brElements.length).toBe(2); + }); + + it('should handle reasoning content with multiple newlines', () => { + const messagesWithReasoningInProgress = [ + { + role: 'bot' as const, + content: 'Line 1\n\n\nLine 2\nLine 3', + timestamp: '2024-01-01T00:00:00Z', + name: 'assistant', + }, + ]; + + render( + , + ); + + const deepThinking = screen.getByTestId('deep-thinking'); + expect(deepThinking).toBeInTheDocument(); + + const brElements = deepThinking.querySelectorAll('br'); + expect(brElements.length).toBe(4); + }); +});