From 9182c07260862ad15c019864539a1de261c8bd61 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Fri, 3 Jul 2026 14:05:27 +0530 Subject: [PATCH 1/5] feat(lightspeed): add Sources chip + popover for notebook context Replace the inline SourcesCard with a compact chip button that opens a PatternFly Popover showing source documents with FileTypeIcon badges. Scoped to Notebook view only; general chat retains the original display. Includes i18n translations for all 5 languages, unit tests, and consistent source document handling during streaming. Signed-off-by: its-mitesh-kumar Co-authored-by: Cursor --- .../lightspeed/.changeset/swift-pens-gleam.md | 5 + .../plugins/lightspeed/report-alpha.api.md | 4 + .../src/components/LightspeedChatBox.tsx | 35 ++- .../src/components/SourcesChipModal.tsx | 200 ++++++++++++++++++ .../__tests__/SourcesChipModal.test.tsx | 171 +++++++++++++++ .../src/components/notebooks/NotebookView.tsx | 1 + .../src/hooks/useConversationMessages.ts | 20 +- .../plugins/lightspeed/src/translations/de.ts | 5 + .../plugins/lightspeed/src/translations/es.ts | 5 + .../plugins/lightspeed/src/translations/fr.ts | 5 + .../plugins/lightspeed/src/translations/it.ts | 5 + .../plugins/lightspeed/src/translations/ja.ts | 5 + .../lightspeed/src/translations/ref.ts | 7 + 13 files changed, 446 insertions(+), 22 deletions(-) create mode 100644 workspaces/lightspeed/.changeset/swift-pens-gleam.md create mode 100644 workspaces/lightspeed/plugins/lightspeed/src/components/SourcesChipModal.tsx create mode 100644 workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/SourcesChipModal.test.tsx diff --git a/workspaces/lightspeed/.changeset/swift-pens-gleam.md b/workspaces/lightspeed/.changeset/swift-pens-gleam.md new file mode 100644 index 0000000000..77da9bcc38 --- /dev/null +++ b/workspaces/lightspeed/.changeset/swift-pens-gleam.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-lightspeed': minor +--- + +Replace inline SourcesCard with a compact chip + popover for notebook sources display, using PatternFly's native Popover component with custom FileTypeIcon badges and i18n support for all languages. diff --git a/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md b/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md index e8b7b123b0..bd39c05592 100644 --- a/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md +++ b/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md @@ -272,6 +272,10 @@ export const lightspeedTranslationRef: TranslationRef< readonly 'sort.oldest': string; readonly 'sort.alphabeticalAsc': string; readonly 'sort.alphabeticalDesc': string; + readonly 'sources.chip.label': string; + readonly 'sources.modal.title': string; + readonly 'sources.modal.description': string; + readonly 'sources.popover.closeAriaLabel': string; readonly 'reasoning.thinking': string; } >; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBox.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBox.tsx index c162197f66..b45e558465 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBox.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBox.tsx @@ -44,6 +44,7 @@ import { useTranslation } from '../hooks/useTranslation'; import { ToolCall } from '../types'; import { parseReasoning } from '../utils/reasoningParser'; import { mapToPatternFlyToolCall } from '../utils/toolCallMapper'; +import { SourcesChipModal } from './SourcesChipModal'; const useStyles = makeStyles(theme => ({ prompt: { @@ -114,6 +115,8 @@ type LightspeedChatBoxProps = { conversationId: string; isStreaming: boolean; displayMode?: ChatbotDisplayMode; + /** When true, sources are shown as a compact chip + modal instead of the inline SourcesCard. */ + useSourcesChipModal?: boolean; }; export interface ScrollContainerHandle { @@ -132,6 +135,7 @@ export const LightspeedChatBox = forwardRef( isStreaming, topicRestrictionEnabled, displayMode, + useSourcesChipModal: showSourcesChipModal = false, }: LightspeedChatBoxProps, ref: ForwardedRef, ) => { @@ -262,6 +266,7 @@ export const LightspeedChatBox = forwardRef( const extraContentParts: { beforeMainContent?: React.ReactNode; afterMainContent?: React.ReactNode; + endContent?: React.ReactNode; } = {}; let deepThinking: DeepThinkingProps | undefined = undefined; @@ -331,18 +336,34 @@ export const LightspeedChatBox = forwardRef( ); } - const extraContent = - extraContentParts.beforeMainContent || - extraContentParts.afterMainContent - ? extraContentParts - : undefined; - - const finalMessage = + const messageToPrepare = parsedReasoning.hasReasoning || parsedReasoning.isReasoningInProgress ? { ...message, content: parsedReasoning.mainContent } : message; + let finalMessage = messageToPrepare; + + if (showSourcesChipModal) { + const { sources: messageSources, ...messageWithoutSources } = + messageToPrepare; + + if (messageSources?.sources?.length) { + extraContentParts.endContent = ( + + ); + } + + finalMessage = messageWithoutSources; + } + + const extraContent = + extraContentParts.beforeMainContent || + extraContentParts.afterMainContent || + extraContentParts.endContent + ? extraContentParts + : undefined; + return ( ({ + sourcesPopover: { + '& .pf-v6-c-popover__title': { + alignItems: 'flex-start', + }, + '& .pf-v6-c-popover__title-text': { + margin: 0, + }, + '& .pf-v6-c-popover__header': { + paddingBlockEnd: theme.spacing(2), + }, + '& .pf-v6-c-popover__body': { + paddingBlockStart: 0, + }, + }, + chipButton: { + padding: '4px 12px', + fontSize: '0.8125rem', + fontWeight: 500, + borderRadius: 16, + backgroundColor: 'transparent', + color: 'var(--pf-t--global--text--color--regular, #1b1d21)', + '&:hover': { + backgroundColor: + 'var(--pf-t--global--background--color--secondary--default, #e0e0e0)', + }, + '& .pf-v6-c-button__icon': { + color: 'inherit', + }, + }, + infoRow: { + display: 'flex', + alignItems: 'flex-start', + gap: theme.spacing(1), + marginBottom: theme.spacing(1.5), + fontSize: '0.8125rem', + color: 'var(--pf-t--global--text--color--subtle, #c7c7c7)', + }, + infoIcon: { + flexShrink: 0, + marginTop: 2, + fontSize: '0.875rem', + color: 'var(--pf-t--global--icon--color--status--info--default, #2b9af3)', + }, + sourcesList: { + listStyle: 'none', + margin: 0, + padding: 0, + maxHeight: 320, + overflowY: 'auto', + }, + sourceItem: { + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1.5), + padding: `${theme.spacing(1.5)}px 0`, + borderBottom: '1px solid var(--pf-t--global--border--color--default, #444)', + '&:last-child': { + borderBottom: 'none', + }, + }, + sourceContent: { + flex: 1, + minWidth: 0, + }, + sourceTitleButton: { + background: 'none', + border: 'none', + cursor: 'pointer', + textAlign: 'left', + padding: 0, + font: 'inherit', + fontSize: '0.875rem', + fontWeight: 500, + color: 'var(--pf-t--global--text--color--link--default, #2b9af3)', + '&:hover': { + textDecoration: 'underline', + }, + }, + sourceTitlePlain: { + fontSize: '0.875rem', + fontWeight: 500, + }, + sourceBody: { + fontSize: '0.8125rem', + color: 'var(--pf-t--global--text--color--subtle, #c7c7c7)', + marginTop: 2, + overflow: 'hidden', + textOverflow: 'ellipsis', + display: '-webkit-box', + WebkitLineClamp: 2, + WebkitBoxOrient: 'vertical', + }, +})); + +type SourcesChipModalProps = { + sources: SourcesCardProps; +}; + +export const SourcesChipModal = ({ sources }: SourcesChipModalProps) => { + const classes = useStyles(); + const { t } = useTranslation(); + + const count = sources.sources?.length ?? 0; + if (count === 0) return null; + + const handleSourceClick = (link: string, isExternal?: boolean) => { + if (link) { + window.open( + link, + isExternal ? '_blank' : '_self', + isExternal ? 'noreferrer' : undefined, + ); + } + }; + + return ( + } + closeBtnAriaLabel={t('sources.popover.closeAriaLabel')} + minWidth={POPOVER_WIDTH} + maxWidth={POPOVER_WIDTH} + appendTo={() => document.body} + bodyContent={ + <> +
+ + {t('sources.modal.description')} +
+
    + {sources.sources.map((source, index) => { + const title = source.title ?? `Source ${index + 1}`; + return ( +
  • + +
    + {source.link ? ( + + ) : ( +
    {title}
    + )} + {source.body && ( +
    {source.body}
    + )} +
    +
  • + ); + })} +
+ + } + > + +
+ ); +}; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/SourcesChipModal.test.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/SourcesChipModal.test.tsx new file mode 100644 index 0000000000..7bb8e8b1e3 --- /dev/null +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/SourcesChipModal.test.tsx @@ -0,0 +1,171 @@ +/* + * 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 { SourcesCardProps } from '@patternfly/chatbot'; +import { fireEvent, render, screen } from '@testing-library/react'; + +import { mockUseTranslation } from '../../test-utils/mockTranslations'; +import { SourcesChipModal } from '../SourcesChipModal'; + +jest.mock('../../hooks/useTranslation', () => ({ + useTranslation: jest.fn(() => mockUseTranslation()), +})); + +const mockSources: SourcesCardProps = { + sources: [ + { + title: 'aws-bedrock-walkthrough.md', + body: 'Documentation covering architecture, usage guidelines, and best practices.', + link: 'https://example.com/aws-bedrock', + isExternal: true, + }, + { + title: 'codecov.yml', + body: 'Application configuration including backend, database, authentication, and catalog settings.', + link: 'https://example.com/codecov', + isExternal: true, + }, + ], +}; + +describe('SourcesChipModal', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('should render the chip with correct source count', () => { + render(); + + expect(screen.getByText('2 Sources')).toBeInTheDocument(); + }); + + test('should not render when sources array is empty', () => { + const emptySources: SourcesCardProps = { sources: [] }; + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); + + test('should open popover when chip is clicked', () => { + render(); + + fireEvent.click(screen.getByText('2 Sources')); + + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(screen.getByText('Sources')).toBeInTheDocument(); + expect( + screen.getByText( + 'The following sources were used to generate this AI response and provide supporting information:', + ), + ).toBeInTheDocument(); + }); + + test('should display all source items in the popover', () => { + render(); + + fireEvent.click(screen.getByText('2 Sources')); + + expect(screen.getByText('aws-bedrock-walkthrough.md')).toBeInTheDocument(); + expect(screen.getByText('codecov.yml')).toBeInTheDocument(); + }); + + test('should display source descriptions in the popover', () => { + render(); + + fireEvent.click(screen.getByText('2 Sources')); + + expect( + screen.getByText( + 'Documentation covering architecture, usage guidelines, and best practices.', + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + 'Application configuration including backend, database, authentication, and catalog settings.', + ), + ).toBeInTheDocument(); + }); + + test('should render FileTypeIcon badges for source filenames', () => { + render(); + + fireEvent.click(screen.getByText('2 Sources')); + + expect(screen.getByText('md')).toBeInTheDocument(); + expect(screen.getByText('yml')).toBeInTheDocument(); + }); + + test('should render source title as plain text when no link', () => { + const sourcesWithoutLink: SourcesCardProps = { + sources: [ + { + title: 'local-doc.yaml', + body: 'A local document', + link: '', + }, + ], + }; + + render(); + + fireEvent.click(screen.getByText('1 Sources')); + + expect(screen.getByText('local-doc.yaml')).toBeInTheDocument(); + }); + + test('should close popover when close button is clicked', () => { + render(); + + fireEvent.click(screen.getByText('2 Sources')); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + + const closeButton = screen.getByRole('button', { + name: 'Close sources', + }); + fireEvent.click(closeButton); + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + + test('should toggle popover when chip is clicked again', () => { + render(); + + const chip = screen.getByText('2 Sources'); + + fireEvent.click(chip); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + + fireEvent.click(chip); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + + test('should show fallback title when source title is missing', () => { + const sourcesWithoutTitle: SourcesCardProps = { + sources: [ + { + link: 'https://example.com/doc', + isExternal: true, + }, + ], + }; + + render(); + + fireEvent.click(screen.getByText('1 Sources')); + + expect(screen.getByText('Source 1')).toBeInTheDocument(); + }); +}); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx index 49bcb553e5..3f1da18c95 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx @@ -617,6 +617,7 @@ export const NotebookView = ({ conversationId={conversationId} isStreaming={isSendButtonDisabled} topicRestrictionEnabled={topicRestrictionEnabled} + useSourcesChipModal /> ); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/hooks/useConversationMessages.ts b/workspaces/lightspeed/plugins/lightspeed/src/hooks/useConversationMessages.ts index 0a9da2f4e9..77dbf4e95e 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/hooks/useConversationMessages.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/hooks/useConversationMessages.ts @@ -28,13 +28,7 @@ import { TEMP_CONVERSATION_ID } from '../const'; import botAvatarDark from '../images/bot-avatar-dark.svg'; import botAvatarLight from '../images/bot-avatar.svg'; import userAvatar from '../images/user-avatar.svg'; -import { - Attachment, - BaseMessage, - LCSConversation, - ReferencedDocument, - ToolCall, -} from '../types'; +import { Attachment, BaseMessage, LCSConversation, ToolCall } from '../types'; import { createBotMessage, createUserMessage, @@ -734,14 +728,10 @@ export const useConversationMessages = ( isLoading: false, }; - if (documents.length) { - lastMessage.sources = { - sources: documents.map((doc: ReferencedDocument) => ({ - title: doc.doc_title, - link: doc.doc_url, - body: doc.doc_description, - })), - }; + const sourcesFromDocs = + transformDocumentsToSources(documents); + if (sourcesFromDocs) { + lastMessage.sources = sourcesFromDocs; } const updatedConversation = [ diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/de.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/de.ts index 811d2c4888..9ae1e00af0 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/de.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/de.ts @@ -317,6 +317,11 @@ const lightspeedTranslationDe = createTranslationMessages({ 'sort.label': 'Unterhaltungen sortieren', 'sort.newest': 'Datum (neuestes zuerst)', 'sort.oldest': 'Datum (ältestes zuerst)', + 'sources.chip.label': '{{count}} Quellen', + 'sources.modal.description': + 'Die folgenden Quellen wurden verwendet, um diese KI-Antwort zu generieren und ergänzende Informationen bereitzustellen:', + 'sources.modal.title': 'Quellen', + 'sources.popover.closeAriaLabel': 'Quellen schließen', 'tabs.ariaLabel': 'Ansichten des intelligenten Assistenten', 'tabs.chat': 'Chat', 'tabs.notebooks': 'Notizbücher', diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts index 193ca68455..6deb7a0734 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts @@ -312,6 +312,11 @@ const lightspeedTranslationEs = createTranslationMessages({ 'sort.label': 'Ordenar conversaciones', 'sort.newest': 'Fecha (la más reciente primero)', 'sort.oldest': 'Fecha (la más antigua primero)', + 'sources.chip.label': '{{count}} Fuentes', + 'sources.modal.description': + 'Las siguientes fuentes se utilizaron para generar esta respuesta de IA y proporcionar información complementaria:', + 'sources.modal.title': 'Fuentes', + 'sources.popover.closeAriaLabel': 'Cerrar fuentes', 'tabs.ariaLabel': 'Vistas del asistente inteligente', 'tabs.chat': 'Chat', 'tabs.notebooks': 'Cuadernos', diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/fr.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/fr.ts index 7f0b522cad..f8f446b878 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/fr.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/fr.ts @@ -317,6 +317,11 @@ const lightspeedTranslationFr = createTranslationMessages({ 'sort.label': 'Trier les conversations', 'sort.newest': 'Date (plus récent en premier)', 'sort.oldest': 'Date (plus ancien en premier)', + 'sources.chip.label': '{{count}} Sources', + 'sources.modal.description': + 'Les sources suivantes ont été utilisées pour générer cette réponse IA et fournir des informations complémentaires\u00a0:', + 'sources.modal.title': 'Sources', + 'sources.popover.closeAriaLabel': 'Fermer les sources', 'tabs.ariaLabel': 'Vues de l\u2019assistant intelligent', 'tabs.chat': 'Chat', 'tabs.notebooks': 'Carnets', diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/it.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/it.ts index 8e7f854c25..85798caf7f 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/it.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/it.ts @@ -315,6 +315,11 @@ const lightspeedTranslationIt = createTranslationMessages({ 'sort.label': 'Ordina conversazioni', 'sort.newest': 'Data (più recente prima)', 'sort.oldest': 'Data (meno recente prima)', + 'sources.chip.label': '{{count}} Fonti', + 'sources.modal.description': + 'Le seguenti fonti sono state utilizzate per generare questa risposta IA e fornire informazioni di supporto:', + 'sources.modal.title': 'Fonti', + 'sources.popover.closeAriaLabel': 'Chiudi fonti', 'tabs.ariaLabel': "Viste dell'assistente intelligente", 'tabs.chat': 'Chat', 'tabs.notebooks': 'Quaderni', diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/ja.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/ja.ts index 8126edce1e..dc5d99a1b1 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/ja.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/ja.ts @@ -307,6 +307,11 @@ const lightspeedTranslationJa = createTranslationMessages({ 'sort.label': '会話を並べ替え', 'sort.newest': '日付(新しい順)', 'sort.oldest': '日付(古い順)', + 'sources.chip.label': '{{count}} ソース', + 'sources.modal.description': + '以下のソースは、この AI 応答の生成と補足情報の提供に使用されました:', + 'sources.modal.title': 'ソース', + 'sources.popover.closeAriaLabel': 'ソースを閉じる', 'tabs.ariaLabel': 'インテリジェントアシスタントのビュー', 'tabs.chat': 'チャット', 'tabs.notebooks': 'ノートブック', diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts index 8b17db3d92..5a1db090b2 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts @@ -393,6 +393,13 @@ export const lightspeedMessages = { 'sort.oldest': 'Date (oldest first)', 'sort.alphabeticalAsc': 'Name (A-Z)', 'sort.alphabeticalDesc': 'Name (Z-A)', + // Sources + 'sources.chip.label': '{{count}} Sources', + 'sources.modal.title': 'Sources', + 'sources.modal.description': + 'The following sources were used to generate this AI response and provide supporting information:', + 'sources.popover.closeAriaLabel': 'Close sources', + // Deep thinking 'reasoning.thinking': 'Show thinking', }; From 92548178ef7d6a056df7e9c4062aa841a99e2563 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Fri, 3 Jul 2026 14:33:21 +0530 Subject: [PATCH 2/5] handling pluralization Signed-off-by: its-mitesh-kumar --- .../lightspeed/src/components/SourcesChipModal.tsx | 2 +- .../src/components/__tests__/SourcesChipModal.test.tsx | 4 ++-- .../lightspeed/src/test-utils/mockTranslations.ts | 9 ++++++++- .../lightspeed/plugins/lightspeed/src/translations/de.ts | 3 ++- .../lightspeed/plugins/lightspeed/src/translations/es.ts | 3 ++- .../lightspeed/plugins/lightspeed/src/translations/fr.ts | 3 ++- .../lightspeed/plugins/lightspeed/src/translations/it.ts | 3 ++- .../lightspeed/plugins/lightspeed/src/translations/ja.ts | 3 ++- .../plugins/lightspeed/src/translations/ref.ts | 3 ++- 9 files changed, 23 insertions(+), 10 deletions(-) diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/SourcesChipModal.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/SourcesChipModal.tsx index 429a2b93a4..94ba968ad6 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/SourcesChipModal.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/SourcesChipModal.tsx @@ -193,7 +193,7 @@ export const SourcesChipModal = ({ sources }: SourcesChipModalProps) => { } > ); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/SourcesChipModal.test.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/SourcesChipModal.test.tsx index 7bb8e8b1e3..0efcdbbc4b 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/SourcesChipModal.test.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/SourcesChipModal.test.tsx @@ -121,7 +121,7 @@ describe('SourcesChipModal', () => { render(); - fireEvent.click(screen.getByText('1 Sources')); + fireEvent.click(screen.getByText('1 Source')); expect(screen.getByText('local-doc.yaml')).toBeInTheDocument(); }); @@ -164,7 +164,7 @@ describe('SourcesChipModal', () => { render(); - fireEvent.click(screen.getByText('1 Sources')); + fireEvent.click(screen.getByText('1 Source')); expect(screen.getByText('Source 1')).toBeInTheDocument(); }); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/test-utils/mockTranslations.ts b/workspaces/lightspeed/plugins/lightspeed/src/test-utils/mockTranslations.ts index e9fc051b7a..cd5af50ac0 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/test-utils/mockTranslations.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/test-utils/mockTranslations.ts @@ -35,7 +35,14 @@ function flattenMessages(obj: any, prefix = ''): Record { const flattenedMessages = flattenMessages(lightspeedMessages); export const mockT = (key: string, params?: any) => { - let message = flattenedMessages[key] || key; + let resolvedKey = key; + if (params && typeof params.count === 'number') { + const pluralSuffix = params.count === 1 ? '_one' : '_other'; + if (flattenedMessages[`${key}${pluralSuffix}`]) { + resolvedKey = `${key}${pluralSuffix}`; + } + } + let message = flattenedMessages[resolvedKey] || key; if (params) { for (const [paramKey, paramValue] of Object.entries(params)) { message = message.replace( diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/de.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/de.ts index 9ae1e00af0..09ac7f4be0 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/de.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/de.ts @@ -317,7 +317,8 @@ const lightspeedTranslationDe = createTranslationMessages({ 'sort.label': 'Unterhaltungen sortieren', 'sort.newest': 'Datum (neuestes zuerst)', 'sort.oldest': 'Datum (ältestes zuerst)', - 'sources.chip.label': '{{count}} Quellen', + 'sources.chip.label_one': '{{count}} Quelle', + 'sources.chip.label_other': '{{count}} Quellen', 'sources.modal.description': 'Die folgenden Quellen wurden verwendet, um diese KI-Antwort zu generieren und ergänzende Informationen bereitzustellen:', 'sources.modal.title': 'Quellen', diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts index 6deb7a0734..36bf3d0afc 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts @@ -312,7 +312,8 @@ const lightspeedTranslationEs = createTranslationMessages({ 'sort.label': 'Ordenar conversaciones', 'sort.newest': 'Fecha (la más reciente primero)', 'sort.oldest': 'Fecha (la más antigua primero)', - 'sources.chip.label': '{{count}} Fuentes', + 'sources.chip.label_one': '{{count}} Fuente', + 'sources.chip.label_other': '{{count}} Fuentes', 'sources.modal.description': 'Las siguientes fuentes se utilizaron para generar esta respuesta de IA y proporcionar información complementaria:', 'sources.modal.title': 'Fuentes', diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/fr.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/fr.ts index f8f446b878..1c8157dcf6 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/fr.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/fr.ts @@ -317,7 +317,8 @@ const lightspeedTranslationFr = createTranslationMessages({ 'sort.label': 'Trier les conversations', 'sort.newest': 'Date (plus récent en premier)', 'sort.oldest': 'Date (plus ancien en premier)', - 'sources.chip.label': '{{count}} Sources', + 'sources.chip.label_one': '{{count}} Source', + 'sources.chip.label_other': '{{count}} Sources', 'sources.modal.description': 'Les sources suivantes ont été utilisées pour générer cette réponse IA et fournir des informations complémentaires\u00a0:', 'sources.modal.title': 'Sources', diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/it.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/it.ts index 85798caf7f..332198067d 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/it.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/it.ts @@ -315,7 +315,8 @@ const lightspeedTranslationIt = createTranslationMessages({ 'sort.label': 'Ordina conversazioni', 'sort.newest': 'Data (più recente prima)', 'sort.oldest': 'Data (meno recente prima)', - 'sources.chip.label': '{{count}} Fonti', + 'sources.chip.label_one': '{{count}} Fonte', + 'sources.chip.label_other': '{{count}} Fonti', 'sources.modal.description': 'Le seguenti fonti sono state utilizzate per generare questa risposta IA e fornire informazioni di supporto:', 'sources.modal.title': 'Fonti', diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/ja.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/ja.ts index dc5d99a1b1..edef7e1373 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/ja.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/ja.ts @@ -307,7 +307,8 @@ const lightspeedTranslationJa = createTranslationMessages({ 'sort.label': '会話を並べ替え', 'sort.newest': '日付(新しい順)', 'sort.oldest': '日付(古い順)', - 'sources.chip.label': '{{count}} ソース', + 'sources.chip.label_one': '{{count}} ソース', + 'sources.chip.label_other': '{{count}} ソース', 'sources.modal.description': '以下のソースは、この AI 応答の生成と補足情報の提供に使用されました:', 'sources.modal.title': 'ソース', diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts index 5a1db090b2..30ad22906a 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts @@ -394,7 +394,8 @@ export const lightspeedMessages = { 'sort.alphabeticalAsc': 'Name (A-Z)', 'sort.alphabeticalDesc': 'Name (Z-A)', // Sources - 'sources.chip.label': '{{count}} Sources', + 'sources.chip.label_one': '{{count}} Source', + 'sources.chip.label_other': '{{count}} Sources', 'sources.modal.title': 'Sources', 'sources.modal.description': 'The following sources were used to generate this AI response and provide supporting information:', From c087ef763ce48a63aaf55d8e2f7902940cae797f Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Fri, 3 Jul 2026 14:34:18 +0530 Subject: [PATCH 3/5] updating translations Signed-off-by: its-mitesh-kumar --- workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md b/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md index bd39c05592..ff9ac65739 100644 --- a/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md +++ b/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md @@ -272,7 +272,8 @@ export const lightspeedTranslationRef: TranslationRef< readonly 'sort.oldest': string; readonly 'sort.alphabeticalAsc': string; readonly 'sort.alphabeticalDesc': string; - readonly 'sources.chip.label': string; + readonly 'sources.chip.label_one': string; + readonly 'sources.chip.label_other': string; readonly 'sources.modal.title': string; readonly 'sources.modal.description': string; readonly 'sources.popover.closeAriaLabel': string; From 102d13eab08a337de7fcab2b2a428b635609d251 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Mon, 6 Jul 2026 20:07:36 +0530 Subject: [PATCH 4/5] removing lines between docs and changing icon color Signed-off-by: its-mitesh-kumar --- .../plugins/lightspeed/src/components/SourcesChipModal.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/SourcesChipModal.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/SourcesChipModal.tsx index 94ba968ad6..d2aaf0eb5e 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/SourcesChipModal.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/SourcesChipModal.tsx @@ -66,7 +66,7 @@ const useStyles = makeStyles(theme => ({ flexShrink: 0, marginTop: 2, fontSize: '0.875rem', - color: 'var(--pf-t--global--icon--color--status--info--default, #2b9af3)', + color: 'var(--pf-t--global--text--color--subtle, #c7c7c7)', }, sourcesList: { listStyle: 'none', @@ -80,10 +80,6 @@ const useStyles = makeStyles(theme => ({ alignItems: 'center', gap: theme.spacing(1.5), padding: `${theme.spacing(1.5)}px 0`, - borderBottom: '1px solid var(--pf-t--global--border--color--default, #444)', - '&:last-child': { - borderBottom: 'none', - }, }, sourceContent: { flex: 1, From 78b6164d224d7bd47c0c80ea157d7aba38cd7120 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Wed, 8 Jul 2026 02:10:01 +0530 Subject: [PATCH 5/5] removing the button to open links Signed-off-by: its-mitesh-kumar --- .../src/components/LightspeedChatBox.tsx | 6 +- .../src/components/SourcesChipModal.tsx | 40 +------------ .../__tests__/LightspeedChatBox.test.tsx | 57 +++++++++++++++++++ .../__tests__/SourcesChipModal.test.tsx | 28 ++------- .../src/components/notebooks/NotebookView.tsx | 2 +- 5 files changed, 67 insertions(+), 66 deletions(-) diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBox.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBox.tsx index b45e558465..975b8c3850 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBox.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/LightspeedChatBox.tsx @@ -115,8 +115,8 @@ type LightspeedChatBoxProps = { conversationId: string; isStreaming: boolean; displayMode?: ChatbotDisplayMode; - /** When true, sources are shown as a compact chip + modal instead of the inline SourcesCard. */ - useSourcesChipModal?: boolean; + /** When true, sources are shown as a compact chip + popover instead of the inline SourcesCard. */ + showSourcesChipPopover?: boolean; }; export interface ScrollContainerHandle { @@ -135,7 +135,7 @@ export const LightspeedChatBox = forwardRef( isStreaming, topicRestrictionEnabled, displayMode, - useSourcesChipModal: showSourcesChipModal = false, + showSourcesChipPopover: showSourcesChipModal = false, }: LightspeedChatBoxProps, ref: ForwardedRef, ) => { diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/SourcesChipModal.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/SourcesChipModal.tsx index d2aaf0eb5e..ad41900737 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/SourcesChipModal.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/SourcesChipModal.tsx @@ -85,21 +85,7 @@ const useStyles = makeStyles(theme => ({ flex: 1, minWidth: 0, }, - sourceTitleButton: { - background: 'none', - border: 'none', - cursor: 'pointer', - textAlign: 'left', - padding: 0, - font: 'inherit', - fontSize: '0.875rem', - fontWeight: 500, - color: 'var(--pf-t--global--text--color--link--default, #2b9af3)', - '&:hover': { - textDecoration: 'underline', - }, - }, - sourceTitlePlain: { + sourceTitle: { fontSize: '0.875rem', fontWeight: 500, }, @@ -126,16 +112,6 @@ export const SourcesChipModal = ({ sources }: SourcesChipModalProps) => { const count = sources.sources?.length ?? 0; if (count === 0) return null; - const handleSourceClick = (link: string, isExternal?: boolean) => { - if (link) { - window.open( - link, - isExternal ? '_blank' : '_self', - isExternal ? 'noreferrer' : undefined, - ); - } - }; - return ( { >
- {source.link ? ( - - ) : ( -
{title}
- )} +
{title}
{source.body && (
{source.body}
)} diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/LightspeedChatBox.test.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/LightspeedChatBox.test.tsx index 3ff4981d3f..862e977144 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/LightspeedChatBox.test.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/LightspeedChatBox.test.tsx @@ -45,6 +45,14 @@ jest.mock('../../hooks/useFeedbackActions', () => ({ }), })); +jest.mock('../SourcesChipModal', () => ({ + SourcesChipModal: (props: any) => ( +
+ {props.sources?.sources?.length ?? 0} sources +
+ ), +})); + jest.mock('@patternfly/chatbot', () => { const actual = jest.requireActual('@patternfly/chatbot'); return { @@ -68,6 +76,7 @@ jest.mock('@patternfly/chatbot', () => { {props.extraContent?.beforeMainContent} {props.content} {props.extraContent?.afterMainContent} + {props.extraContent?.endContent}
), }; @@ -183,4 +192,52 @@ describe('LightspeedChatBox', () => { expect(deepThinking.textContent).toContain('Line 2'); expect(deepThinking.textContent).toContain('Line 3'); }); + + it('should render SourcesChipModal as endContent when showSourcesChipPopover is true', () => { + const messagesWithSources = [ + { + role: 'bot' as const, + content: 'Here is the answer.', + timestamp: '2024-01-01T00:00:00Z', + sources: { + sources: [ + { title: 'doc.md', body: 'Some documentation', link: '' }, + { title: 'config.yaml', body: 'Config file', link: '' }, + ], + }, + }, + ]; + + render( + , + ); + + expect(screen.getByTestId('sources-chip-modal')).toBeInTheDocument(); + expect(screen.getByTestId('sources-chip-modal')).toHaveTextContent( + '2 sources', + ); + }); + + it('should not render SourcesChipModal when showSourcesChipPopover is false', () => { + const messagesWithSources = [ + { + role: 'bot' as const, + content: 'Here is the answer.', + timestamp: '2024-01-01T00:00:00Z', + sources: { + sources: [{ title: 'doc.md', body: 'Some documentation', link: '' }], + }, + }, + ]; + + render( + , + ); + + expect(screen.queryByTestId('sources-chip-modal')).not.toBeInTheDocument(); + }); }); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/SourcesChipModal.test.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/SourcesChipModal.test.tsx index 0efcdbbc4b..a0f6a5d6df 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/SourcesChipModal.test.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/SourcesChipModal.test.tsx @@ -29,14 +29,12 @@ const mockSources: SourcesCardProps = { { title: 'aws-bedrock-walkthrough.md', body: 'Documentation covering architecture, usage guidelines, and best practices.', - link: 'https://example.com/aws-bedrock', - isExternal: true, + link: '', }, { title: 'codecov.yml', body: 'Application configuration including backend, database, authentication, and catalog settings.', - link: 'https://example.com/codecov', - isExternal: true, + link: '', }, ], }; @@ -108,24 +106,6 @@ describe('SourcesChipModal', () => { expect(screen.getByText('yml')).toBeInTheDocument(); }); - test('should render source title as plain text when no link', () => { - const sourcesWithoutLink: SourcesCardProps = { - sources: [ - { - title: 'local-doc.yaml', - body: 'A local document', - link: '', - }, - ], - }; - - render(); - - fireEvent.click(screen.getByText('1 Source')); - - expect(screen.getByText('local-doc.yaml')).toBeInTheDocument(); - }); - test('should close popover when close button is clicked', () => { render(); @@ -156,8 +136,8 @@ describe('SourcesChipModal', () => { const sourcesWithoutTitle: SourcesCardProps = { sources: [ { - link: 'https://example.com/doc', - isExternal: true, + body: 'A document without a title', + link: '', }, ], }; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx index 3f1da18c95..b07d0bd468 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx @@ -617,7 +617,7 @@ export const NotebookView = ({ conversationId={conversationId} isStreaming={isSendButtonDisabled} topicRestrictionEnabled={topicRestrictionEnabled} - useSourcesChipModal + showSourcesChipPopover /> );