From f45c3cb979fc8de1c087aec369a74a04eba4286d Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Wed, 6 May 2026 20:58:00 +0530 Subject: [PATCH 01/12] feat(lightspeed): implement fullscreen chat UX updates Signed-off-by: its-mitesh-kumar --- .../src/components/AttachPlusMenu.tsx | 220 ++++++++++ .../src/components/CollapsedHistoryStrip.tsx | 129 ++++++ .../src/components/LightSpeedChat.tsx | 385 ++++++++++++++++-- .../components/MessageBarModelSelector.tsx | 130 ++++++ .../plugins/lightspeed/src/translations/es.ts | 7 + .../plugins/lightspeed/src/translations/it.ts | 7 + .../lightspeed/src/translations/ref.ts | 11 + 7 files changed, 847 insertions(+), 42 deletions(-) create mode 100644 workspaces/lightspeed/plugins/lightspeed/src/components/AttachPlusMenu.tsx create mode 100644 workspaces/lightspeed/plugins/lightspeed/src/components/CollapsedHistoryStrip.tsx create mode 100644 workspaces/lightspeed/plugins/lightspeed/src/components/MessageBarModelSelector.tsx diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/AttachPlusMenu.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/AttachPlusMenu.tsx new file mode 100644 index 0000000000..872b96dfdd --- /dev/null +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/AttachPlusMenu.tsx @@ -0,0 +1,220 @@ +/* + * 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 { ChangeEvent, Ref, useRef, useState } from 'react'; +import { DropEvent, FileRejection } from 'react-dropzone'; + +import { makeStyles } from '@material-ui/core'; +import { + Dropdown, + DropdownItem, + DropdownList, + MenuToggle, + MenuToggleElement, +} from '@patternfly/react-core'; +import { PaperclipIcon, PlusIcon } from '@patternfly/react-icons'; + +import { useTranslation } from '../hooks/useTranslation'; + +type AttachPlusMenuProps = { + onAttach: ( + data: File[], + event: DropEvent | ChangeEvent, + ) => void; + allowedFileTypes?: { [key: string]: string[] }; + onAttachRejected?: (rejections: FileRejection[]) => void; +}; + +const useStyles = makeStyles(theme => ({ + plusButton: { + width: 40, + height: 40, + padding: 0, + minWidth: 0, + borderRadius: '50%', + backgroundColor: 'transparent', + color: '#6a7282', + '&:hover': { + backgroundColor: 'rgba(0, 0, 0, 0.05)', + }, + }, + dropdown: { + padding: 0, + '& ul': { + padding: 0, + }, + '& .pf-v6-c-menu__content, & .pf-v5-c-menu__content': { + paddingTop: 4, + paddingBottom: 4, + }, + }, + menuItem: { + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + gap: 4, + padding: theme.spacing(1.5), + cursor: 'pointer', + minWidth: 280, + '--pf-v6-c-menu__item--PaddingTop': `${theme.spacing(1.5)}px`, + '--pf-v6-c-menu__item--PaddingBottom': `${theme.spacing(1.5)}px`, + }, + menuItemContent: { + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + gap: 4, + width: '100%', + }, + menuItemHeader: { + display: 'flex', + alignItems: 'center', + gap: 8, + fontSize: 14, + fontWeight: 500, + color: '#151515', + }, + menuItemDescription: { + fontSize: 14, + color: '#707070', + paddingLeft: 26, + }, + paperclipIcon: { + width: 17.5, + height: 20, + color: '#151515', + }, + hiddenInput: { + display: 'none', + }, +})); + +export const AttachPlusMenu = ({ + onAttach, + allowedFileTypes, + onAttachRejected, +}: AttachPlusMenuProps) => { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const classes = useStyles(); + const { t } = useTranslation(); + + const fileInputRef = useRef(null); + + const handleAttachClick = () => { + setIsMenuOpen(false); + fileInputRef.current?.click(); + }; + + const handleFileChange = (event: ChangeEvent) => { + const files = Array.from(event.target.files || []); + if (files.length === 0) return; + + if (allowedFileTypes && onAttachRejected) { + const allowedExtensions = Object.values(allowedFileTypes).flat(); + const accepted: File[] = []; + const rejected: FileRejection[] = []; + + files.forEach(file => { + const ext = `.${file.name.split('.').pop()?.toLowerCase()}`; + if (allowedExtensions.includes(ext)) { + accepted.push(file); + } else { + rejected.push({ + file, + errors: [ + { + code: 'file-invalid-type', + message: `File type ${ext} is not supported`, + }, + ], + }); + } + }); + + if (rejected.length > 0) { + onAttachRejected(rejected); + } + + if (accepted.length > 0) { + onAttach(accepted, event); + } + } else { + onAttach(files, event); + } + + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + const acceptTypes = allowedFileTypes + ? Object.entries(allowedFileTypes) + .map(([mime, exts]) => [mime, ...exts]) + .flat() + .join(',') + : undefined; + + const toggle = (toggleRef: Ref) => ( + setIsMenuOpen(!isMenuOpen)} + isExpanded={isMenuOpen} + variant="plain" + className={classes.plusButton} + aria-label={t('tooltip.attach')} + > + + + ); + + return ( + <> + setIsMenuOpen(false)} + onOpenChange={isOpen => setIsMenuOpen(isOpen)} + toggle={toggle} + popperProps={{ position: 'left' }} + > + + +
+
+ + {t('attach.menu.title')} +
+
+ {t('attach.menu.description')} +
+
+
+
+
+ + + ); +}; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/CollapsedHistoryStrip.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/CollapsedHistoryStrip.tsx new file mode 100644 index 0000000000..c01b74cf9e --- /dev/null +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/CollapsedHistoryStrip.tsx @@ -0,0 +1,129 @@ +/* + * 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 { makeStyles } from '@material-ui/core'; +import { Button, Tooltip } from '@patternfly/react-core'; + +import { useTranslation } from '../hooks/useTranslation'; + +type IconProps = { + className?: string; +}; + +const ExpandPanelIcon = ({ className }: IconProps) => ( + + + +); + +export const EditSquareIcon = ({ className }: IconProps) => ( + + + +); + +const useStyles = makeStyles(theme => ({ + strip: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + paddingTop: theme.spacing(1.5), + gap: theme.spacing(1.5), + borderRight: '1px solid var(--pf-t--global--border--color--default)', + width: 48, + minWidth: 48, + flexShrink: 0, + backgroundColor: 'var(--pf-t--global--background--color--primary--default)', + height: '100%', + }, + iconButton: { + padding: 0, + minWidth: 0, + lineHeight: 1, + color: 'var(--pf-t--global--icon--color--regular)', + '&:hover': { + color: 'var(--pf-t--global--icon--color--hover)', + }, + }, + newChatIconButton: { + padding: 0, + minWidth: 0, + lineHeight: 1, + color: '#0066CC', + '&:hover': { + color: 'var(--pf-t--global--color--brand--hover)', + }, + }, +})); + +type CollapsedHistoryStripProps = { + onExpand: () => void; + onNewChat: () => void; + newChatDisabled?: boolean; +}; + +export const CollapsedHistoryStrip = ({ + onExpand, + onNewChat, + newChatDisabled = false, +}: CollapsedHistoryStripProps) => { + const classes = useStyles(); + const { t } = useTranslation(); + + return ( +
+ + + + + + +
+ ); +}; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx index 6b199861b1..b37abc3e81 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx @@ -99,6 +99,7 @@ import { useLightspeedDrawerContext } from '../hooks/useLightspeedDrawerContext' import { useLightspeedUpdatePermission } from '../hooks/useLightspeedUpdatePermission'; import { useTranslation } from '../hooks/useTranslation'; import { useWelcomePrompts } from '../hooks/useWelcomePrompts'; +import roundedLogo from '../images/rounded-logo.svg'; import { ConversationSummary, NotebookSession } from '../types'; import { getAttachments } from '../utils/attachment-utils'; import { @@ -108,11 +109,14 @@ import { } from '../utils/lightspeed-chatbox-utils'; import Attachment from './Attachment'; import { useFileAttachmentContext } from './AttachmentContext'; +import { AttachPlusMenu } from './AttachPlusMenu'; +import { CollapsedHistoryStrip, EditSquareIcon } from './CollapsedHistoryStrip'; import { DeleteModal } from './DeleteModal'; import FilePreview from './FilePreview'; import { LightspeedChatBox } from './LightspeedChatBox'; import { LightspeedChatBoxHeader } from './LightspeedChatBoxHeader'; import { McpServersSettings } from './McpServersSettings'; +import { MessageBarModelSelector } from './MessageBarModelSelector'; import { DeleteNotebookModal } from './notebooks/DeleteNotebookModal'; import { NotebooksTab } from './notebooks/NotebooksTab'; import { NotebookView } from './notebooks/NotebookView'; @@ -142,6 +146,8 @@ const useStyles = makeStyles(theme => ({ columnGap: 0, '--pf-v6-c-multiple-file-upload--Gap': '0', '--pf-v5-c-multiple-file-upload--Gap': '0', + flex: 1, + minWidth: 0, }, headerMenu: { // align hamburger icon with title @@ -150,8 +156,20 @@ const useStyles = makeStyles(theme => ({ alignItems: 'center', }, }, + headerLogo: { + width: 48, + height: 48, + marginRight: theme.spacing(1.5), + flexShrink: 0, + }, headerTitle: { justifyContent: 'left !important', + '& h1': { + fontSize: '32px !important', + fontWeight: '700 !important', + lineHeight: '36.4px !important', + fontFamily: '"Red Hat Display", sans-serif !important', + }, }, tabs: { padding: `${theme.spacing(2)}px ${theme.spacing(3)}px 0`, @@ -320,6 +338,67 @@ const useStyles = makeStyles(theme => ({ maxWidth: 'unset !important', }, }, + fullscreenFooter: { + '&>.pf-chatbot__footer-container': { + width: '100% !important', + padding: theme.spacing(1.5), + maxWidth: 'unset !important', + margin: '0 auto', + }, + }, + messageBarContainer: { + backgroundColor: + 'var(--pf-t--global--background--color--secondary--default)', + border: '1px solid var(--pf-t--global--border--color--default)', + borderRadius: 24, + padding: 4, + paddingBottom: 48, + width: '100%', + maxWidth: 'unset', + margin: '0 auto', + position: 'relative', + cursor: 'text', + '& .pf-chatbot__message-bar': { + backgroundColor: 'transparent', + position: 'static', + }, + '& .pf-chatbot__message-bar::after': { + border: 'none !important', + display: 'none !important', + }, + '& .pf-chatbot__message-bar-input': { + backgroundColor: 'transparent', + }, + '& .pf-v6-c-form-control, & .pf-v5-c-form-control': { + backgroundColor: 'transparent', + '--pf-v6-c-form-control--BackgroundColor': 'transparent', + '--pf-v5-c-form-control--BackgroundColor': 'transparent', + }, + '& textarea': { + backgroundColor: 'transparent !important', + border: 'none !important', + boxShadow: 'none !important', + outline: 'none !important', + }, + '& textarea:focus, & textarea:hover, & textarea:focus-visible': { + border: 'none !important', + boxShadow: 'none !important', + outline: 'none !important', + }, + '& .pf-chatbot__message-bar-actions': { + position: 'absolute', + bottom: 4, + right: 8, + }, + }, + messageBarBottomRow: { + position: 'absolute', + bottom: 4, + left: 8, + display: 'flex', + alignItems: 'center', + gap: 8, + }, sortDropdown: { padding: 0, margin: 0, @@ -449,6 +528,65 @@ const useStyles = makeStyles(theme => ({ transition: 'none !important', }, }, + fullscreenChatLayout: { + display: 'flex', + flexDirection: 'row', + flex: 1, + minHeight: 0, + height: '100%', + width: '100%', + '& .pf-v6-c-drawer, & .pf-v5-c-drawer': { + flex: 1, + minWidth: 0, + }, + '& .pf-v6-c-drawer__content, & .pf-v5-c-drawer__content': { + flex: 1, + minWidth: 0, + }, + '& .pf-v6-c-drawer:not(.pf-m-expanded) > .pf-v6-c-drawer__main > .pf-v6-c-drawer__panel, & .pf-v5-c-drawer:not(.pf-m-expanded) > .pf-v5-c-drawer__main > .pf-v5-c-drawer__panel': + { + display: 'none', + }, + '& .pf-v6-c-drawer__close .pf-v6-c-button svg, & .pf-v5-c-drawer__close .pf-v5-c-button svg': + { + display: 'none', + }, + '& .pf-v6-c-drawer__close .pf-v6-c-button, & .pf-v5-c-drawer__close .pf-v5-c-button': + { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + '&::before': { + content: '""', + display: 'block', + width: 24, + height: 24, + mask: `url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16 21V3H14V21H16ZM12 17V7L7 12L12 17Z' fill='black'/%3E%3C/svg%3E") no-repeat center`, + WebkitMask: `url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16 21V3H14V21H16ZM12 17V7L7 12L12 17Z' fill='black'/%3E%3C/svg%3E") no-repeat center`, + backgroundColor: 'currentColor', + }, + }, + '& .pf-chatbot__menu-item': { + cursor: 'pointer', + }, + '& .pf-chatbot__menu-item .pf-v6-c-menu-toggle, & .pf-chatbot__menu-item .pf-v5-c-menu-toggle': + { + opacity: 0, + transition: 'opacity 0.15s ease-in-out', + }, + '& .pf-chatbot__menu-item:hover .pf-v6-c-menu-toggle, & .pf-chatbot__menu-item:hover .pf-v5-c-menu-toggle': + { + opacity: 1, + }, + }, + fullscreenMainContent: { + display: 'flex', + flexDirection: 'column', + flex: 1, + minHeight: 0, + minWidth: 0, + overflow: 'hidden', + }, })); type LightspeedChatProps = { @@ -1556,40 +1694,113 @@ export const LightspeedChat = ({ )} - + - { + const textarea = (e.currentTarget as HTMLElement).querySelector( + 'textarea', + ); + if (textarea) textarea.focus(); + }} + onKeyDown={e => { + if (e.key === 'Enter' || e.key === ' ') { + const textarea = (e.currentTarget as HTMLElement).querySelector( + 'textarea', + ); + if (textarea) textarea.focus(); + } + }} + > + +
+ + { + setIsMcpSettingsOpen(false); + onNewChat(); + handleSelectedModel(item); + }} + disabled={isSendButtonDisabled} + /> +
+ + ) : ( + + microphone: { + tooltipContent: { + active: t('tooltip.microphone.active'), + inactive: t('tooltip.microphone.inactive'), + }, + }, + send: { + tooltipContent: t('tooltip.send'), + }, + }} + allowedFileTypes={supportedFileTypes} + onAttachRejected={onAttachRejected} + placeholder={t('chatbox.message.placeholder')} + /> + )}
@@ -1635,6 +1846,8 @@ export const LightspeedChat = ({ drawerPanelStyle = { zIndex: 1300 }; } else if (isMcpSettingsOpen) { drawerPanelStyle = { width: 320, minWidth: 320, maxWidth: 320 }; + } else { + drawerPanelStyle = { minWidth: 232, maxWidth: 400 }; } return ( @@ -1711,7 +1924,7 @@ export const LightspeedChat = ({ > - {showChatPanel && ( + {showChatPanel && !isFullscreenMode && ( )} {isFullscreenMode && ( - - - {t('chatbox.header.title')} - - + <> + {t('icon.lightspeed.alt')} + + + {t('chatbox.header.title')} + + + )} @@ -1739,7 +1959,7 @@ export const LightspeedChat = ({ models={models} isPinningChatsEnabled={isPinningChatsEnabled} isModelSelectorDisabled={isSendButtonDisabled} - hideModelSelector={showNotebooksPanel} + hideModelSelector={showNotebooksPanel || isFullscreenMode} showChatTabOptions={!showNotebooksPanel} setDisplayMode={setDisplayModeFromHeader} displayMode={displayMode} @@ -1761,11 +1981,92 @@ export const LightspeedChat = ({
)} - {showChatPanel && ( + {showChatPanel && isFullscreenMode && ( +
+ {!isChatHistoryDrawerOpen && ( + setIsChatHistoryDrawerOpen(true)} + onNewChat={onNewChat} + newChatDisabled={newChatCreated} + /> + )} + , + }} + handleTextInputChange={handleFilter} + searchInputPlaceholder={t('chatbox.search.placeholder')} + searchInputAriaLabel={t('aria.search.placeholder')} + searchInputProps={{ + value: filterValue, + onClear: () => { + setFilterValue(''); + }, + }} + searchActionEnd={sortDropdown} + noResultsState={ + filterValue && + Object.keys(filterConversations(filterValue)).length === 0 + ? { + bodyText: t('chatbox.emptyState.noResults.body'), + titleText: t('chatbox.emptyState.noResults.title'), + icon: SearchIcon, + } + : undefined + } + drawerContent={ + handleAttach(data, e)} + displayMode={ChatbotDisplayMode.embedded} + infoText={t('chatbox.fileUpload.infoText')} + allowedFileTypes={supportedFileTypes} + onAttachRejected={onAttachRejected} + > + {showAlert && uploadError.message && ( +
+ setUploadError({ message: null })} + > + {uploadError.message} + +
+ )} + {mainPanelContent} +
+ } + /> +
+ )} + {showChatPanel && !isFullscreenMode && ( void; + disabled?: boolean; +}; + +const useStyles = makeStyles(() => ({ + selectorToggle: { + display: 'flex', + alignItems: 'center', + gap: 4, + color: '#6a7282', + fontSize: 14, + fontWeight: 500, + cursor: 'pointer', + padding: '4px 8px', + borderRadius: 8, + border: 'none', + background: 'transparent', + '&:hover': { + backgroundColor: 'rgba(0, 0, 0, 0.05)', + }, + '&:disabled': { + cursor: 'not-allowed', + opacity: 0.5, + }, + }, + dropdown: { + '& ul, & li': { + padding: 0, + margin: 0, + }, + }, + groupTitle: { + fontWeight: 'bold', + }, +})); + +export const MessageBarModelSelector = ({ + selectedModel, + models, + onSelect, + disabled = false, +}: MessageBarModelSelectorProps) => { + const [isOpen, setIsOpen] = useState(false); + const classes = useStyles(); + const { t } = useTranslation(); + + const selectedModelLabel = + models.find(m => m.value === selectedModel)?.label ?? selectedModel; + + const toggle = (toggleRef: Ref) => ( + setIsOpen(!isOpen)} + isExpanded={isOpen} + isDisabled={disabled} + variant="plain" + className={classes.selectorToggle} + aria-label={t('aria.chatbotSelector')} + > + {selectedModelLabel} + + + ); + + return ( + { + onSelect(value as string); + setIsOpen(false); + }} + onOpenChange={open => setIsOpen(open)} + popperProps={{ position: 'left' }} + shouldFocusToggleOnSelect + shouldFocusFirstItemOnOpen={false} + toggle={toggle} + isScrollable={models.length > 10} + maxMenuHeight={models.length > 10 ? '240px' : undefined} + > + + {models.map(model => ( + + + {model.label} + + + ))} + + + ); +}; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts index 2de64c3c59..054dbb73ba 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts @@ -218,6 +218,9 @@ const lightspeedTranslationEs = createTranslationMessages({ 'tooltip.send': 'Enviar', 'tooltip.microphone.active': 'Dejar de escuchar', 'tooltip.microphone.inactive': 'Usar micrófono', + 'tooltip.expandHistoryPanel': 'Expandir historial de chat', + 'tooltip.collapseHistoryPanel': 'Colapsar historial de chat', + 'tooltip.quickNewChat': 'Nuevo chat', 'button.newChat': 'Nuevo chat', 'tooltip.chatHistoryMenu': 'Menú del historial de chat', 'tooltip.responseRecorded': 'Respuesta grabada', @@ -225,6 +228,10 @@ const lightspeedTranslationEs = createTranslationMessages({ 'tooltip.backToBottom': 'Volver al final', 'tooltip.settings': 'Opciones de chatbot', 'tooltip.close': 'Cerrar', + 'attach.menu.title': 'Adjuntar', + 'attach.menu.description': 'Adjuntar un archivo JSON, YAML, TXT o XML', + 'history.section.pinned': 'Fijados', + 'history.section.recent': 'Recientes', 'modal.title.preview': 'Previsualizar archivo adjunto', 'modal.title.edit': 'Modificar archivo adjunto', 'icon.lightspeed.alt': 'icono de Lightspeed', diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/it.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/it.ts index b6f74ed4ab..e3cb19dae3 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/it.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/it.ts @@ -217,6 +217,9 @@ const lightspeedTranslationIt = createTranslationMessages({ 'tooltip.send': 'Invia', 'tooltip.microphone.active': 'Non ascoltare più', 'tooltip.microphone.inactive': 'Usa il microfono', + 'tooltip.expandHistoryPanel': 'Espandi cronologia chat', + 'tooltip.collapseHistoryPanel': 'Comprimi cronologia chat', + 'tooltip.quickNewChat': 'Nuova chat', 'button.newChat': 'Nuova chat', 'tooltip.chatHistoryMenu': 'Menu cronologia chat', 'tooltip.responseRecorded': 'Risposta registrata', @@ -224,6 +227,10 @@ const lightspeedTranslationIt = createTranslationMessages({ 'tooltip.backToBottom': 'Torna alla fine', 'tooltip.settings': 'Opzioni chatbot', 'tooltip.close': 'Chiudi', + 'attach.menu.title': 'Allega', + 'attach.menu.description': 'Allega un file JSON, YAML, TXT o XML', + 'history.section.pinned': 'Fissati', + 'history.section.recent': 'Recenti', 'modal.title.preview': 'Anteprima allegato', 'modal.title.edit': 'Modifica allegato', 'icon.lightspeed.alt': 'icona di lightspeed', diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts index a69b280c6a..853be05b53 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts @@ -238,6 +238,9 @@ export const lightspeedMessages = { 'tooltip.send': 'Send', 'tooltip.microphone.active': 'Stop listening', 'tooltip.microphone.inactive': 'Use microphone', + 'tooltip.expandHistoryPanel': 'Expand chat history', + 'tooltip.collapseHistoryPanel': 'Collapse chat history', + 'tooltip.quickNewChat': 'New chat', 'button.newChat': 'New chat', 'tooltip.chatHistoryMenu': 'Chat history menu', 'tooltip.responseRecorded': 'Response recorded', @@ -246,6 +249,14 @@ export const lightspeedMessages = { 'tooltip.settings': 'Chatbot options', 'tooltip.close': 'Close', + // Attach menu + 'attach.menu.title': 'Attach', + 'attach.menu.description': 'Attach a JSON, YAML, TXT, or XML file', + + // History panel sections + 'history.section.pinned': 'Pinned', + 'history.section.recent': 'Recent', + // Modal titles 'modal.title.preview': 'Preview attachment', 'modal.title.edit': 'Edit attachment', From 4cc766731c8d1f932ccc8955b2a4bea63d05339c Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Wed, 6 May 2026 21:01:50 +0530 Subject: [PATCH 02/12] adding api report Signed-off-by: its-mitesh-kumar --- workspaces/lightspeed/.changeset/witty-eyes-learn.md | 11 +++++++++++ .../lightspeed/plugins/lightspeed/report-alpha.api.md | 7 +++++++ 2 files changed, 18 insertions(+) create mode 100644 workspaces/lightspeed/.changeset/witty-eyes-learn.md diff --git a/workspaces/lightspeed/.changeset/witty-eyes-learn.md b/workspaces/lightspeed/.changeset/witty-eyes-learn.md new file mode 100644 index 0000000000..92a8b80248 --- /dev/null +++ b/workspaces/lightspeed/.changeset/witty-eyes-learn.md @@ -0,0 +1,11 @@ +--- +'@red-hat-developer-hub/backstage-plugin-lightspeed': minor +--- + +Implemented fullscreen chat UX updates including: + +- Collapsible history panel with new expand/collapse icons +- Redesigned message bar with inline model selector and attachment menu +- New collapsed history strip with quick new chat functionality +- Updated header with Lightspeed logo +- Improved conversation list with hover-only options menu diff --git a/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md b/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md index 2202fec3e4..ac5767978d 100644 --- a/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md +++ b/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md @@ -318,6 +318,9 @@ export const lightspeedTranslationRef: TranslationRef< readonly 'tooltip.send': string; readonly 'tooltip.microphone.active': string; readonly 'tooltip.microphone.inactive': string; + readonly 'tooltip.expandHistoryPanel': string; + readonly 'tooltip.collapseHistoryPanel': string; + readonly 'tooltip.quickNewChat': string; readonly 'button.newChat': string; readonly 'tooltip.chatHistoryMenu': string; readonly 'tooltip.responseRecorded': string; @@ -325,6 +328,10 @@ export const lightspeedTranslationRef: TranslationRef< readonly 'tooltip.backToBottom': string; readonly 'tooltip.settings': string; readonly 'tooltip.close': string; + readonly 'attach.menu.title': string; + readonly 'attach.menu.description': string; + readonly 'history.section.pinned': string; + readonly 'history.section.recent': string; readonly 'modal.title.preview': string; readonly 'modal.title.edit': string; readonly 'icon.lightspeed.alt': string; From b582f39dfa3e460f77d925efa4e901f2c1f18669 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Wed, 6 May 2026 21:32:37 +0530 Subject: [PATCH 03/12] adding unit tests Signed-off-by: its-mitesh-kumar --- .../plugins/lightspeed/report-alpha.api.md | 4 +- .../__tests__/AttachPlusMenu.test.tsx | 196 +++++++++++++++ .../__tests__/CollapsedHistoryStrip.test.tsx | 149 +++++++++++ .../__tests__/LightspeedChat.test.tsx | 105 +++++++- .../MessageBarModelSelector.test.tsx | 238 ++++++++++++++++++ 5 files changed, 689 insertions(+), 3 deletions(-) create mode 100644 workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AttachPlusMenu.test.tsx create mode 100644 workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/CollapsedHistoryStrip.test.tsx create mode 100644 workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/MessageBarModelSelector.test.tsx diff --git a/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md b/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md index 31dae3784a..3ae204de2c 100644 --- a/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md +++ b/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md @@ -328,12 +328,12 @@ export const lightspeedTranslationRef: TranslationRef< readonly 'tooltip.backToBottom': string; readonly 'tooltip.settings': string; readonly 'tooltip.close': string; + readonly 'tooltip.fab.open': string; + readonly 'tooltip.fab.close': string; readonly 'attach.menu.title': string; readonly 'attach.menu.description': string; readonly 'history.section.pinned': string; readonly 'history.section.recent': string; - readonly 'tooltip.fab.open': string; - readonly 'tooltip.fab.close': string; readonly 'modal.title.preview': string; readonly 'modal.title.edit': string; readonly 'icon.lightspeed.alt': string; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AttachPlusMenu.test.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AttachPlusMenu.test.tsx new file mode 100644 index 0000000000..77ded9b28f --- /dev/null +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AttachPlusMenu.test.tsx @@ -0,0 +1,196 @@ +/* + * 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 { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { mockUseTranslation } from '../../test-utils/mockTranslations'; +import { AttachPlusMenu } from '../AttachPlusMenu'; + +jest.mock('../../hooks/useTranslation', () => ({ + useTranslation: jest.fn(() => mockUseTranslation()), +})); + +describe('AttachPlusMenu', () => { + const mockOnAttach = jest.fn(); + const mockOnAttachRejected = jest.fn(); + const allowedFileTypes = { + 'text/plain': ['.txt'], + 'application/json': ['.json'], + 'application/yaml': ['.yaml', '.yml'], + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render the plus button toggle', () => { + render(); + + const toggleButton = screen.getByRole('button', { name: 'Attach' }); + expect(toggleButton).toBeInTheDocument(); + }); + + it('should open dropdown menu when toggle is clicked', async () => { + render(); + + const toggleButton = screen.getByRole('button', { name: 'Attach' }); + await userEvent.click(toggleButton); + + await waitFor(() => { + expect(screen.getByText('Attach')).toBeInTheDocument(); + }); + }); + + it('should show attach menu item with description', async () => { + render(); + + const toggleButton = screen.getByRole('button', { name: 'Attach' }); + await userEvent.click(toggleButton); + + await waitFor(() => { + expect(screen.getByText('Attach')).toBeInTheDocument(); + expect( + screen.getByText('Attach a JSON, YAML, TXT, or XML file'), + ).toBeInTheDocument(); + }); + }); + + it('should have hidden file input', () => { + render(); + + const fileInput = screen.getByTestId('attachment-input'); + expect(fileInput).toBeInTheDocument(); + expect(fileInput).toHaveAttribute('type', 'file'); + }); + + it('should set correct accept attribute when allowedFileTypes provided', () => { + render( + , + ); + + const fileInput = screen.getByTestId( + 'attachment-input', + ) as HTMLInputElement; + expect(fileInput.accept).toContain('text/plain'); + expect(fileInput.accept).toContain('.txt'); + expect(fileInput.accept).toContain('application/json'); + expect(fileInput.accept).toContain('.json'); + }); + + it('should call onAttach when valid files are selected', async () => { + render(); + + const fileInput = screen.getByTestId('attachment-input'); + const file = new File(['test content'], 'test.txt', { type: 'text/plain' }); + + fireEvent.change(fileInput, { target: { files: [file] } }); + + expect(mockOnAttach).toHaveBeenCalledWith([file], expect.any(Object)); + }); + + it('should call onAttachRejected for invalid file types', async () => { + render( + , + ); + + const fileInput = screen.getByTestId('attachment-input'); + const invalidFile = new File(['test content'], 'test.pdf', { + type: 'application/pdf', + }); + + fireEvent.change(fileInput, { target: { files: [invalidFile] } }); + + expect(mockOnAttachRejected).toHaveBeenCalledWith([ + expect.objectContaining({ + file: invalidFile, + errors: expect.arrayContaining([ + expect.objectContaining({ + code: 'file-invalid-type', + }), + ]), + }), + ]); + expect(mockOnAttach).not.toHaveBeenCalled(); + }); + + it('should accept valid files and reject invalid files in mixed selection', async () => { + render( + , + ); + + const fileInput = screen.getByTestId('attachment-input'); + const validFile = new File(['valid'], 'valid.txt', { type: 'text/plain' }); + const invalidFile = new File(['invalid'], 'invalid.exe', { + type: 'application/octet-stream', + }); + + fireEvent.change(fileInput, { + target: { files: [validFile, invalidFile] }, + }); + + expect(mockOnAttach).toHaveBeenCalledWith([validFile], expect.any(Object)); + expect(mockOnAttachRejected).toHaveBeenCalledWith([ + expect.objectContaining({ + file: invalidFile, + }), + ]); + }); + + it('should close dropdown after clicking attach menu item', async () => { + render(); + + const toggleButton = screen.getByRole('button', { name: 'Attach' }); + await userEvent.click(toggleButton); + + await waitFor(() => { + expect( + screen.getByText('Attach a JSON, YAML, TXT, or XML file'), + ).toBeInTheDocument(); + }); + + const attachMenuItem = screen.getByText( + 'Attach a JSON, YAML, TXT, or XML file', + ); + await userEvent.click(attachMenuItem); + + await waitFor(() => { + expect( + screen.queryByText('Attach a JSON, YAML, TXT, or XML file'), + ).not.toBeInTheDocument(); + }); + }); + + it('should not call onAttach when no files are selected', () => { + render(); + + const fileInput = screen.getByTestId('attachment-input'); + fireEvent.change(fileInput, { target: { files: [] } }); + + expect(mockOnAttach).not.toHaveBeenCalled(); + }); +}); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/CollapsedHistoryStrip.test.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/CollapsedHistoryStrip.test.tsx new file mode 100644 index 0000000000..0bc0201525 --- /dev/null +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/CollapsedHistoryStrip.test.tsx @@ -0,0 +1,149 @@ +/* + * 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 { fireEvent, render, screen } from '@testing-library/react'; + +import { mockUseTranslation } from '../../test-utils/mockTranslations'; +import { + CollapsedHistoryStrip, + EditSquareIcon, +} from '../CollapsedHistoryStrip'; + +jest.mock('../../hooks/useTranslation', () => ({ + useTranslation: jest.fn(() => mockUseTranslation()), +})); + +describe('CollapsedHistoryStrip', () => { + const mockOnExpand = jest.fn(); + const mockOnNewChat = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render the collapsed history strip with expand and new chat buttons', () => { + render( + , + ); + + expect( + screen.getByRole('button', { name: 'Expand chat history' }), + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'New chat' }), + ).toBeInTheDocument(); + }); + + it('should call onExpand when expand button is clicked', () => { + render( + , + ); + + const expandButton = screen.getByRole('button', { + name: 'Expand chat history', + }); + fireEvent.click(expandButton); + + expect(mockOnExpand).toHaveBeenCalledTimes(1); + }); + + it('should call onNewChat when new chat button is clicked', () => { + render( + , + ); + + const newChatButton = screen.getByRole('button', { name: 'New chat' }); + fireEvent.click(newChatButton); + + expect(mockOnNewChat).toHaveBeenCalledTimes(1); + }); + + it('should disable new chat button when newChatDisabled is true', () => { + render( + , + ); + + const newChatButton = screen.getByRole('button', { name: 'New chat' }); + expect(newChatButton).toBeDisabled(); + }); + + it('should not disable new chat button when newChatDisabled is false', () => { + render( + , + ); + + const newChatButton = screen.getByRole('button', { name: 'New chat' }); + expect(newChatButton).not.toBeDisabled(); + }); + + it('should not call onNewChat when button is disabled', () => { + render( + , + ); + + const newChatButton = screen.getByRole('button', { name: 'New chat' }); + fireEvent.click(newChatButton); + + expect(mockOnNewChat).not.toHaveBeenCalled(); + }); +}); + +describe('EditSquareIcon', () => { + it('should render the EditSquareIcon SVG', () => { + const { container } = render(); + + const svg = container.querySelector('svg'); + expect(svg).toBeInTheDocument(); + expect(svg).toHaveAttribute('width', '20'); + expect(svg).toHaveAttribute('height', '20'); + expect(svg).toHaveAttribute('viewBox', '0 0 24 24'); + }); + + it('should apply className when provided', () => { + const { container } = render(); + + const svg = container.querySelector('svg'); + expect(svg).toHaveClass('custom-class'); + }); + + it('should have verticalAlign middle style', () => { + const { container } = render(); + + const svg = container.querySelector('svg'); + expect(svg).toHaveStyle({ verticalAlign: 'middle' }); + }); +}); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/LightspeedChat.test.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/LightspeedChat.test.tsx index eb0f063bdc..c298c489f8 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/LightspeedChat.test.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/LightspeedChat.test.tsx @@ -914,7 +914,7 @@ describe('LightspeedChat', () => { const chatTab = screen.getByRole('tab', { name: 'Chat' }); expect(chatTab).toHaveAttribute('aria-selected', 'true'); expect( - screen.getByRole('button', { name: 'Chat history menu' }), + screen.getByRole('button', { name: 'New chat' }), ).toBeInTheDocument(); }); @@ -1019,4 +1019,107 @@ describe('LightspeedChat', () => { ); }); }); + + describe('fullscreen mode specific features', () => { + beforeEach(() => { + mockUseLightspeedDrawerContext.mockReturnValue({ + isChatbotActive: false, + toggleChatbot: jest.fn(), + displayMode: ChatbotDisplayMode.embedded, + setDisplayMode: mockSetDisplayMode, + drawerWidth: 500, + setDrawerWidth: jest.fn(), + currentConversationId: undefined, + setCurrentConversationId: mockSetCurrentConversationId, + draftMessage: '', + setDraftMessage: jest.fn(), + draftFileContents: [], + setDraftFileContents: jest.fn(), + consumePendingOverlayThreadHandoff: jest.fn(() => false), + shellViewTab: 0, + setShellViewTab: jest.fn(), + }); + }); + + it('should render Chat and Notebooks tabs in fullscreen mode', async () => { + render(setupLightspeedChat()); + + await waitFor(() => { + expect(screen.getByRole('tab', { name: 'Chat' })).toBeInTheDocument(); + expect( + screen.getByRole('tab', { name: 'Notebooks' }), + ).toBeInTheDocument(); + }); + }); + + it('should show history panel with conversations in fullscreen', async () => { + mockUseConversations.mockReturnValue({ + data: [ + { + conversation_id: 'test-id', + topic_summary: 'Test Chat', + last_message_timestamp: Date.now() / 1000, + }, + ], + isRefetching: false, + isLoading: false, + } as Partial> as ReturnType< + typeof useConversations + >); + + render(setupLightspeedChat()); + + await waitFor(() => { + expect(screen.getByText('Developer Lightspeed')).toBeInTheDocument(); + }); + + expect( + screen.getByRole('button', { name: 'New chat' }), + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Close drawer panel' }), + ).toBeInTheDocument(); + }); + + it('should show EditSquareIcon in new chat button in fullscreen mode', async () => { + mockUseConversations.mockReturnValue({ + data: [ + { + conversation_id: 'test-id', + topic_summary: 'Test Chat', + last_message_timestamp: Date.now() / 1000, + }, + ], + isRefetching: false, + isLoading: false, + } as Partial> as ReturnType< + typeof useConversations + >); + + render(setupLightspeedChat()); + + await waitFor(() => { + expect(screen.getByText('Developer Lightspeed')).toBeInTheDocument(); + }); + + expect( + screen.getByRole('button', { name: 'New chat' }), + ).toBeInTheDocument(); + }); + + it('should render model selector and attach menu in fullscreen mode', async () => { + render(setupLightspeedChat()); + + await waitFor(() => { + expect(screen.getByText('Developer Lightspeed')).toBeInTheDocument(); + }); + + expect( + screen.getByRole('button', { name: 'Chatbot selector' }), + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Attach' }), + ).toBeInTheDocument(); + }); + }); }); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/MessageBarModelSelector.test.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/MessageBarModelSelector.test.tsx new file mode 100644 index 0000000000..d7dacbeb30 --- /dev/null +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/MessageBarModelSelector.test.tsx @@ -0,0 +1,238 @@ +/* + * 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 { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { mockUseTranslation } from '../../test-utils/mockTranslations'; +import { MessageBarModelSelector } from '../MessageBarModelSelector'; + +jest.mock('../../hooks/useTranslation', () => ({ + useTranslation: jest.fn(() => mockUseTranslation()), +})); + +describe('MessageBarModelSelector', () => { + const mockModels = [ + { label: 'Granite 3.3', value: 'granite-3.3', provider: 'ibm' }, + { label: 'GPT-4', value: 'gpt-4', provider: 'openai' }, + { label: 'Claude 3', value: 'claude-3', provider: 'anthropic' }, + ]; + const mockOnSelect = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render the selector with selected model label', () => { + render( + , + ); + + expect(screen.getByText('Granite 3.3')).toBeInTheDocument(); + }); + + it('should show model value when model is not in the list', () => { + render( + , + ); + + expect(screen.getByText('unknown-model')).toBeInTheDocument(); + }); + + it('should open dropdown when toggle is clicked', async () => { + render( + , + ); + + const toggleButton = screen.getByRole('button', { + name: 'Chatbot selector', + }); + await userEvent.click(toggleButton); + + await waitFor(() => { + expect(screen.getByRole('menu')).toBeInTheDocument(); + }); + }); + + it('should display all models in the dropdown', async () => { + render( + , + ); + + const toggleButton = screen.getByRole('button', { + name: 'Chatbot selector', + }); + await userEvent.click(toggleButton); + + await waitFor(() => { + mockModels.forEach(model => { + expect( + screen.getByRole('menuitem', { name: model.label }), + ).toBeInTheDocument(); + }); + }); + }); + + it('should call onSelect when a model is selected', async () => { + render( + , + ); + + const toggleButton = screen.getByRole('button', { + name: 'Chatbot selector', + }); + await userEvent.click(toggleButton); + + await waitFor(() => { + expect(screen.getByRole('menu')).toBeInTheDocument(); + }); + + const gpt4Option = screen.getByRole('menuitem', { name: 'GPT-4' }); + await userEvent.click(gpt4Option); + + expect(mockOnSelect).toHaveBeenCalledWith('gpt-4'); + }); + + it('should close dropdown after selection', async () => { + render( + , + ); + + const toggleButton = screen.getByRole('button', { + name: 'Chatbot selector', + }); + await userEvent.click(toggleButton); + + await waitFor(() => { + expect(screen.getByRole('menu')).toBeInTheDocument(); + }); + + const gpt4Option = screen.getByRole('menuitem', { name: 'GPT-4' }); + await userEvent.click(gpt4Option); + + await waitFor(() => { + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); + }); + }); + + it('should be disabled when disabled prop is true', () => { + render( + , + ); + + const toggleButton = screen.getByRole('button', { + name: 'Chatbot selector', + }); + expect(toggleButton).toBeDisabled(); + }); + + it('should not open dropdown when disabled', async () => { + render( + , + ); + + const toggleButton = screen.getByRole('button', { + name: 'Chatbot selector', + }); + await userEvent.click(toggleButton); + + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); + }); + + it('should mark selected model in dropdown', async () => { + render( + , + ); + + const toggleButton = screen.getByRole('button', { + name: 'Chatbot selector', + }); + await userEvent.click(toggleButton); + + await waitFor(() => { + const selectedItem = screen.getByRole('menuitem', { + name: 'Granite 3.3', + }); + expect(selectedItem).toHaveClass('pf-m-selected'); + }); + }); + + it('should render with single model', () => { + const singleModel = [ + { label: 'Granite 3.3', value: 'granite-3.3', provider: 'ibm' }, + ]; + + render( + , + ); + + expect(screen.getByText('Granite 3.3')).toBeInTheDocument(); + }); + + it('should render with empty models list', () => { + render( + , + ); + + expect(screen.getByText('granite-3.3')).toBeInTheDocument(); + }); +}); From c0a1f04b671fefcd9e15562c7ff6ee493754804c Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Wed, 6 May 2026 23:46:00 +0530 Subject: [PATCH 04/12] updating e2e Signed-off-by: its-mitesh-kumar --- .../e2e-tests/lightspeed.ui.test.ts | 7 +-- .../e2e-tests/pages/LightspeedPage.ts | 34 ++++++++++---- .../lightspeed/e2e-tests/utils/fileUpload.ts | 47 +++++++++++++------ .../lightspeed/e2e-tests/utils/sidebar.ts | 38 +++++++++++++-- .../src/components/AttachPlusMenu.tsx | 1 + .../plugins/lightspeed/src/translations/de.ts | 6 +++ .../plugins/lightspeed/src/translations/fr.ts | 5 ++ .../plugins/lightspeed/src/translations/ja.ts | 5 ++ 8 files changed, 111 insertions(+), 32 deletions(-) diff --git a/workspaces/lightspeed/e2e-tests/lightspeed.ui.test.ts b/workspaces/lightspeed/e2e-tests/lightspeed.ui.test.ts index 2bbbcc1102..9c49664107 100644 --- a/workspaces/lightspeed/e2e-tests/lightspeed.ui.test.ts +++ b/workspaces/lightspeed/e2e-tests/lightspeed.ui.test.ts @@ -193,7 +193,7 @@ test.describe('Lightspeed UI', () => { function validationTestCase(path: string, name: string) { test(`should validate file: ${name}`, async ({ browser }, testInfo) => { const fileExtension = `.${name.split('.').pop()}`; - await uploadFiles(sharedPage, [path]); + await uploadFiles(sharedPage, [path], translations); if (supportedFileTypes.includes(fileExtension)) { await uploadAndAssertDuplicate( @@ -221,7 +221,7 @@ test.describe('Lightspeed UI', () => { test(`Multiple file upload`, async () => { const file1 = `e2e-tests/fixtures/uploads/${locale}.upload1.json`; const file2 = `e2e-tests/fixtures/uploads/${locale}.upload2.json`; - await uploadFiles(sharedPage, [file1, file2]); + await uploadFiles(sharedPage, [file1, file2], translations); const heading = sharedPage.getByRole('heading', { name: `Danger alert: ${translations['chatbox.fileUpload.failed']}`, @@ -235,7 +235,8 @@ test.describe('Lightspeed UI', () => { await assertVisibilityState('visible', heading, text, closeBtn); - await closeBtn.click(); + // Use evaluate to click via JavaScript to bypass the iframe overlay + await closeBtn.evaluate((el: HTMLElement) => el.click()); await assertVisibilityState('hidden', heading, text, closeBtn); }); diff --git a/workspaces/lightspeed/e2e-tests/pages/LightspeedPage.ts b/workspaces/lightspeed/e2e-tests/pages/LightspeedPage.ts index e7a699bf73..6914df0093 100644 --- a/workspaces/lightspeed/e2e-tests/pages/LightspeedPage.ts +++ b/workspaces/lightspeed/e2e-tests/pages/LightspeedPage.ts @@ -49,7 +49,18 @@ export async function selectDisplayMode( } export async function openChatHistoryDrawer(page: Page, t: LightspeedMessages) { - await page.getByRole('button', { name: t['aria.chatHistoryMenu'] }).click(); + const chatHistoryMenuButton = page.getByRole('button', { + name: t['aria.chatHistoryMenu'], + }); + const expandHistoryButton = page.getByRole('button', { + name: t['tooltip.expandHistoryPanel'], + }); + + if (await chatHistoryMenuButton.isVisible()) { + await chatHistoryMenuButton.click(); + } else if (await expandHistoryButton.isVisible()) { + await expandHistoryButton.click(); + } } export async function closeChatHistoryDrawer( @@ -74,9 +85,12 @@ export async function expectChatbotControlsVisible( t: LightspeedMessages, ) { await expect(page.locator('.pf-chatbot__header')).toBeVisible(); - await expect( - page.getByRole('button', { name: t['aria.chatHistoryMenu'] }), - ).toBeVisible(); + const chatHistoryMenuButton = page.getByRole('button', { + name: t['aria.chatHistoryMenu'], + }); + if (await chatHistoryMenuButton.isVisible().catch(() => false)) { + await expect(chatHistoryMenuButton).toBeVisible(); + } await expect( page.getByRole('button', { name: t['aria.settings.label'] }), ).toBeVisible(); @@ -344,12 +358,12 @@ export async function verifyMcpSettingsPanel( } } - await expect(page.getByLabel('Chatbot', { exact: true })) - .toMatchAriaSnapshot(` - - button "${t['aria.chatHistoryMenu']}" - - button "${t['aria.chatbotSelector']}" - - button "${t['aria.settings.label']}" - `); + await expect( + page.getByRole('button', { name: t['aria.chatbotSelector'] }), + ).toBeVisible(); + await expect( + page.getByRole('button', { name: t['aria.settings.label'] }), + ).toBeVisible(); await closeMcpSettingsPanel(page, t); await expectMcpServersSettingsHeading(page, false, t); diff --git a/workspaces/lightspeed/e2e-tests/utils/fileUpload.ts b/workspaces/lightspeed/e2e-tests/utils/fileUpload.ts index eedd3c4b8b..aa3c8bdd90 100644 --- a/workspaces/lightspeed/e2e-tests/utils/fileUpload.ts +++ b/workspaces/lightspeed/e2e-tests/utils/fileUpload.ts @@ -30,13 +30,22 @@ export async function triggerFileChooser( return fileChooser; } -export async function uploadFiles(page: Page, filePath: string[]) { - // button name stays the same, only tooltip is translated - const attachButton = page.getByRole('button', { name: 'Attach' }); - await expect(attachButton).toBeVisible(); - - const fileChooser = await triggerFileChooser(page, attachButton); - await fileChooser.setFiles(filePath); +export async function uploadFiles( + page: Page, + filePath: string[], + translations: LightspeedMessages, +) { + // The attach button is now a dropdown toggle with a PlusIcon + // aria-label uses 'tooltip.attach' translation + const plusButton = page.getByRole('button', { + name: translations['tooltip.attach'], + }); + await expect(plusButton).toBeVisible(); + + // Use the hidden file input directly - this bypasses the dropdown menu + // The input has the multiple attribute so it can accept multiple files + const fileInput = page.locator('input[data-testid="attachment-input"]'); + await fileInput.setInputFiles(filePath); } export async function uploadAndAssertDuplicate( @@ -47,7 +56,7 @@ export async function uploadAndAssertDuplicate( testInfo: TestInfo, ) { await validateSuccessfulUpload(page, fileName, translations, testInfo); - await uploadFiles(page, [filePath]); + await uploadFiles(page, [filePath], translations); await expect( page.getByRole('heading', { name: translations['chatbox.fileUpload.failed'], @@ -88,7 +97,11 @@ export async function validateSuccessfulUpload( .getByRole('button', { name: translations['modal.close'] }), ).toBeVisible(); - await page.getByRole('button', { name: translations['modal.edit'] }).click(); + // Use evaluate to click buttons via JavaScript to bypass the iframe overlay + const editButton = page.getByRole('button', { + name: translations['modal.edit'], + }); + await editButton.evaluate((el: HTMLElement) => el.click()); await runAccessibilityTests(page, testInfo); await expect( @@ -98,11 +111,15 @@ export async function validateSuccessfulUpload( page.getByRole('button', { name: translations['modal.cancel'] }), ).toBeVisible(); - await page.getByRole('button', { name: translations['modal.save'] }).click(); - await page + const saveButton = page.getByRole('button', { + name: translations['modal.save'], + }); + await saveButton.evaluate((el: HTMLElement) => el.click()); + + const closeButton = page .getByRole('contentinfo') - .locator(`role=button[name="${translations['modal.close']}"]`) - .click(); + .getByRole('button', { name: translations['modal.close'] }); + await closeButton.evaluate((el: HTMLElement) => el.click()); } export async function validateFailedUpload( @@ -117,7 +134,9 @@ export async function validateFailedUpload( await expect(alertHeader).toBeVisible(); await expect(alertText).toBeVisible(); - await page.getByRole('button', { name: 'Close Danger alert' }).click(); + // Use evaluate to click the button via JavaScript to bypass the iframe overlay + const closeButton = page.getByRole('button', { name: 'Close Danger alert' }); + await closeButton.evaluate((el: HTMLElement) => el.click()); await expect(alertHeader).toBeHidden(); await expect(alertText).toBeHidden(); } diff --git a/workspaces/lightspeed/e2e-tests/utils/sidebar.ts b/workspaces/lightspeed/e2e-tests/utils/sidebar.ts index 290d149e11..ce0a91c286 100644 --- a/workspaces/lightspeed/e2e-tests/utils/sidebar.ts +++ b/workspaces/lightspeed/e2e-tests/utils/sidebar.ts @@ -23,9 +23,20 @@ export async function assertChatDialogInitialState( await expect(page.getByLabel('Chatbot', { exact: true })).toContainText( translations['chatbox.header.title'], ); - await expect( - page.getByRole('button', { name: translations['aria.chatHistoryMenu'] }), - ).toBeVisible(); + + const chatHistoryMenuButton = page.getByRole('button', { + name: translations['aria.chatHistoryMenu'], + }); + const closeDrawerButton = page.getByRole('button', { + name: translations['aria.closeDrawerPanel'], + }); + + if (await chatHistoryMenuButton.isVisible().catch(() => false)) { + await expect(chatHistoryMenuButton).toBeVisible(); + } else { + await expect(closeDrawerButton).toBeVisible(); + } + await assertDrawerState(page, 'open', translations); await expect(page.getByLabel(translations['conversation.category.recent'])) @@ -53,10 +64,27 @@ export async function openChatDrawer( page: Page, translations: LightspeedMessages, ) { - const toggleButton = page.getByRole('button', { + const chatHistoryMenuButton = page.getByRole('button', { name: translations['aria.chatHistoryMenu'], }); - await toggleButton.click(); + const expandHistoryButton = page.getByRole('button', { + name: translations['tooltip.expandHistoryPanel'], + }); + + // Try the hamburger menu first (overlay/docked mode) + if (await chatHistoryMenuButton.isVisible().catch(() => false)) { + await chatHistoryMenuButton.click(); + } else { + // In fullscreen mode, use the expand button from CollapsedHistoryStrip + await expect(expandHistoryButton).toBeVisible({ timeout: 5000 }); + await expandHistoryButton.click(); + } + + // Wait for the drawer to open + const closeButton = page.getByRole('button', { + name: translations['aria.closeDrawerPanel'], + }); + await expect(closeButton).toBeVisible({ timeout: 5000 }); } export async function assertDrawerState( diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/AttachPlusMenu.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/AttachPlusMenu.tsx index 872b96dfdd..763611bf9d 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/AttachPlusMenu.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/AttachPlusMenu.tsx @@ -210,6 +210,7 @@ export const AttachPlusMenu = ({ Date: Thu, 7 May 2026 00:58:50 +0530 Subject: [PATCH 05/12] fixing chat horizontal scroll Signed-off-by: its-mitesh-kumar --- .../plugins/lightspeed/src/components/LightSpeedChat.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx index b37abc3e81..7afe73a378 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx @@ -505,6 +505,9 @@ const useStyles = makeStyles(theme => ({ minHeight: 0, width: '100%', minWidth: 0, + whiteSpace: 'normal', + wordBreak: 'break-word', + overflowWrap: 'break-word', }, mcpSettingsPane: { width: '100%', @@ -515,6 +518,7 @@ const useStyles = makeStyles(theme => ({ display: 'flex', flexDirection: 'column', minHeight: 0, + overflow: 'auto', }, mcpCollapsedDrawerOrderFix: { '& .pf-v6-c-drawer.pf-m-panel-left > .pf-v6-c-drawer__main > .pf-v6-c-drawer__content, & .pf-v5-c-drawer.pf-m-panel-left > .pf-v5-c-drawer__main > .pf-v5-c-drawer__content': From d228bec99c85a6cdbfe590c852fdf3d379bd5600 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Thu, 7 May 2026 19:25:52 +0530 Subject: [PATCH 06/12] addressing the comments Signed-off-by: its-mitesh-kumar --- .../plugins/lightspeed/package.json | 2 +- .../src/components/AttachPlusMenu.tsx | 10 +- .../src/components/CollapsedHistoryStrip.tsx | 2 +- .../src/components/LightSpeedChat.tsx | 142 +++++------------- .../components/MessageBarModelSelector.tsx | 26 ++-- .../__tests__/AttachPlusMenu.test.tsx | 10 +- .../plugins/lightspeed/src/translations/de.ts | 3 +- .../plugins/lightspeed/src/translations/es.ts | 2 +- .../plugins/lightspeed/src/translations/fr.ts | 2 +- .../plugins/lightspeed/src/translations/it.ts | 2 +- .../plugins/lightspeed/src/translations/ja.ts | 2 +- .../lightspeed/src/translations/ref.ts | 2 +- workspaces/lightspeed/yarn.lock | 10 +- 13 files changed, 73 insertions(+), 142 deletions(-) diff --git a/workspaces/lightspeed/plugins/lightspeed/package.json b/workspaces/lightspeed/plugins/lightspeed/package.json index 0509773b30..165322b50e 100644 --- a/workspaces/lightspeed/plugins/lightspeed/package.json +++ b/workspaces/lightspeed/plugins/lightspeed/package.json @@ -65,7 +65,7 @@ "@mui/icons-material": "^6.1.8", "@mui/material": "^5.12.2", "@mui/styles": "5.18.0", - "@patternfly/chatbot": "6.5.0", + "@patternfly/chatbot": "6.6.0-prerelease.6", "@patternfly/react-core": "6.4.1", "@patternfly/react-icons": "^6.3.1", "@patternfly/react-table": "^6.4.1", diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/AttachPlusMenu.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/AttachPlusMenu.tsx index 763611bf9d..b15cb0b610 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/AttachPlusMenu.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/AttachPlusMenu.tsx @@ -46,9 +46,9 @@ const useStyles = makeStyles(theme => ({ minWidth: 0, borderRadius: '50%', backgroundColor: 'transparent', - color: '#6a7282', + color: theme.palette.text.secondary, '&:hover': { - backgroundColor: 'rgba(0, 0, 0, 0.05)', + backgroundColor: theme.palette.action.hover, }, }, dropdown: { @@ -85,17 +85,17 @@ const useStyles = makeStyles(theme => ({ gap: 8, fontSize: 14, fontWeight: 500, - color: '#151515', + color: theme.palette.text.primary, }, menuItemDescription: { fontSize: 14, - color: '#707070', + color: theme.palette.text.secondary, paddingLeft: 26, }, paperclipIcon: { width: 17.5, height: 20, - color: '#151515', + color: theme.palette.text.primary, }, hiddenInput: { display: 'none', diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/CollapsedHistoryStrip.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/CollapsedHistoryStrip.tsx index c01b74cf9e..1a90fdfce8 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/CollapsedHistoryStrip.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/CollapsedHistoryStrip.tsx @@ -80,7 +80,7 @@ const useStyles = makeStyles(theme => ({ padding: 0, minWidth: 0, lineHeight: 1, - color: '#0066CC', + color: 'var(--pf-t--global--color--brand--default)', '&:hover': { color: 'var(--pf-t--global--color--brand--hover)', }, diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx index 7afe73a378..73169f8624 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx @@ -109,7 +109,6 @@ import { } from '../utils/lightspeed-chatbox-utils'; import Attachment from './Attachment'; import { useFileAttachmentContext } from './AttachmentContext'; -import { AttachPlusMenu } from './AttachPlusMenu'; import { CollapsedHistoryStrip, EditSquareIcon } from './CollapsedHistoryStrip'; import { DeleteModal } from './DeleteModal'; import FilePreview from './FilePreview'; @@ -346,59 +345,16 @@ const useStyles = makeStyles(theme => ({ margin: '0 auto', }, }, - messageBarContainer: { + fullscreenMessageBar: { backgroundColor: 'var(--pf-t--global--background--color--secondary--default)', border: '1px solid var(--pf-t--global--border--color--default)', borderRadius: 24, - padding: 4, - paddingBottom: 48, - width: '100%', - maxWidth: 'unset', - margin: '0 auto', - position: 'relative', - cursor: 'text', - '& .pf-chatbot__message-bar': { - backgroundColor: 'transparent', - position: 'static', - }, - '& .pf-chatbot__message-bar::after': { - border: 'none !important', - display: 'none !important', - }, - '& .pf-chatbot__message-bar-input': { - backgroundColor: 'transparent', - }, - '& .pf-v6-c-form-control, & .pf-v5-c-form-control': { - backgroundColor: 'transparent', - '--pf-v6-c-form-control--BackgroundColor': 'transparent', - '--pf-v5-c-form-control--BackgroundColor': 'transparent', - }, - '& textarea': { - backgroundColor: 'transparent !important', - border: 'none !important', - boxShadow: 'none !important', - outline: 'none !important', - }, - '& textarea:focus, & textarea:hover, & textarea:focus-visible': { - border: 'none !important', - boxShadow: 'none !important', - outline: 'none !important', - }, - '& .pf-chatbot__message-bar-actions': { - position: 'absolute', - bottom: 4, - right: 8, + padding: theme.spacing(0.5), + '&::after': { + display: 'none', }, }, - messageBarBottomRow: { - position: 'absolute', - bottom: 4, - left: 8, - display: 'flex', - alignItems: 'center', - gap: 8, - }, sortDropdown: { padding: 0, margin: 0, @@ -1707,58 +1663,38 @@ export const LightspeedChat = ({ > {isFullscreenMode ? ( -
{ - const textarea = (e.currentTarget as HTMLElement).querySelector( - 'textarea', - ); - if (textarea) textarea.focus(); - }} - onKeyDown={e => { - if (e.key === 'Enter' || e.key === ' ') { - const textarea = (e.currentTarget as HTMLElement).querySelector( - 'textarea', - ); - if (textarea) textarea.focus(); - } - }} - > - , + }, + microphone: { + tooltipContent: { + active: t('tooltip.microphone.active'), + inactive: t('tooltip.microphone.inactive'), }, - }} - allowedFileTypes={supportedFileTypes} - onAttachRejected={onAttachRejected} - placeholder={t('chatbox.message.placeholder')} - /> -
- + }, + send: { + tooltipContent: t('tooltip.send'), + }, + }} + additionalActions={ -
-
+ } + forceMultilineLayout + allowedFileTypes={supportedFileTypes} + onAttachRejected={onAttachRejected} + placeholder={t('chatbox.message.placeholder')} + /> ) : ( ({ +const useStyles = makeStyles(theme => ({ selectorToggle: { display: 'flex', alignItems: 'center', gap: 4, - color: '#6a7282', + color: theme.palette.text.secondary, fontSize: 14, fontWeight: 500, cursor: 'pointer', @@ -50,7 +49,7 @@ const useStyles = makeStyles(() => ({ border: 'none', background: 'transparent', '&:hover': { - backgroundColor: 'rgba(0, 0, 0, 0.05)', + backgroundColor: theme.palette.action.hover, }, '&:disabled': { cursor: 'not-allowed', @@ -63,9 +62,6 @@ const useStyles = makeStyles(() => ({ margin: 0, }, }, - groupTitle: { - fontWeight: 'bold', - }, })); export const MessageBarModelSelector = ({ @@ -114,15 +110,13 @@ export const MessageBarModelSelector = ({ > {models.map(model => ( - - - {model.label} - - + + {model.label} + ))} diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AttachPlusMenu.test.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AttachPlusMenu.test.tsx index 77ded9b28f..9996152cce 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AttachPlusMenu.test.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AttachPlusMenu.test.tsx @@ -64,7 +64,7 @@ describe('AttachPlusMenu', () => { await waitFor(() => { expect(screen.getByText('Attach')).toBeInTheDocument(); expect( - screen.getByText('Attach a JSON, YAML, TXT, or XML file'), + screen.getByText('Attach a JSON, YAML, or TXT file'), ).toBeInTheDocument(); }); }); @@ -169,18 +169,16 @@ describe('AttachPlusMenu', () => { await waitFor(() => { expect( - screen.getByText('Attach a JSON, YAML, TXT, or XML file'), + screen.getByText('Attach a JSON, YAML, or TXT file'), ).toBeInTheDocument(); }); - const attachMenuItem = screen.getByText( - 'Attach a JSON, YAML, TXT, or XML file', - ); + const attachMenuItem = screen.getByText('Attach a JSON, YAML, or TXT file'); await userEvent.click(attachMenuItem); await waitFor(() => { expect( - screen.queryByText('Attach a JSON, YAML, TXT, or XML file'), + screen.queryByText('Attach a JSON, YAML, or TXT file'), ).not.toBeInTheDocument(); }); }); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/de.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/de.ts index bb67a19f2c..16bb7d67d2 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/de.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/de.ts @@ -230,8 +230,7 @@ const lightspeedTranslationDe = createTranslationMessages({ 'tooltip.fab.open': 'Lightspeed öffnen', 'tooltip.fab.close': 'Lightspeed schließen', 'attach.menu.title': 'Anhängen', - 'attach.menu.description': - 'Eine JSON-, YAML-, TXT- oder XML-Datei anhängen', + 'attach.menu.description': 'Eine JSON-, YAML- oder TXT-Datei anhängen', 'modal.title.preview': 'Anhang in der Vorschau anzeigen', 'modal.title.edit': 'Anhang bearbeiten', 'icon.lightspeed.alt': 'Lightspeed-Symbol', diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts index 251908d1bc..cd8d6d7023 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts @@ -229,7 +229,7 @@ const lightspeedTranslationEs = createTranslationMessages({ 'tooltip.settings': 'Opciones de chatbot', 'tooltip.close': 'Cerrar', 'attach.menu.title': 'Adjuntar', - 'attach.menu.description': 'Adjuntar un archivo JSON, YAML, TXT o XML', + 'attach.menu.description': 'Adjuntar un archivo JSON, YAML o TXT', 'history.section.pinned': 'Fijados', 'history.section.recent': 'Recientes', 'tooltip.fab.open': 'Abrir Lightspeed', diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/fr.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/fr.ts index 739d9a7637..b868c0122f 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/fr.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/fr.ts @@ -229,7 +229,7 @@ const lightspeedTranslationFr = createTranslationMessages({ 'tooltip.fab.open': 'Ouvrir Lightspeed', 'tooltip.fab.close': 'Fermer Lightspeed', 'attach.menu.title': 'Attacher', - 'attach.menu.description': 'Attacher un fichier JSON, YAML, TXT ou XML', + 'attach.menu.description': 'Attacher un fichier JSON, YAML ou TXT', 'modal.title.preview': 'Aperçu de la pièce jointe', 'modal.title.edit': 'Modifier la pièce jointe', diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/it.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/it.ts index e0036ae869..69445b577c 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/it.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/it.ts @@ -228,7 +228,7 @@ const lightspeedTranslationIt = createTranslationMessages({ 'tooltip.settings': 'Opzioni chatbot', 'tooltip.close': 'Chiudi', 'attach.menu.title': 'Allega', - 'attach.menu.description': 'Allega un file JSON, YAML, TXT o XML', + 'attach.menu.description': 'Allega un file JSON, YAML o TXT', 'history.section.pinned': 'Fissati', 'history.section.recent': 'Recenti', 'tooltip.fab.open': 'Apri Lightspeed', diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/ja.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/ja.ts index 238c1e28fa..74bb831bcd 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/ja.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/ja.ts @@ -228,7 +228,7 @@ const lightspeedTranslationJa = createTranslationMessages({ 'tooltip.fab.open': 'Lightspeed を開く', 'tooltip.fab.close': 'Lightspeed を閉じる', 'attach.menu.title': '添付', - 'attach.menu.description': 'JSON、YAML、TXT、または XML ファイルを添付', + 'attach.menu.description': 'JSON、YAML、または TXT ファイルを添付', 'modal.title.preview': '添付ファイルのプレビュー', 'modal.title.edit': '添付ファイルの編集', 'icon.lightspeed.alt': 'lightspeed アイコン', diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts index 4f0742eeaf..65e914adfd 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts @@ -253,7 +253,7 @@ export const lightspeedMessages = { // Attach menu 'attach.menu.title': 'Attach', - 'attach.menu.description': 'Attach a JSON, YAML, TXT, or XML file', + 'attach.menu.description': 'Attach a JSON, YAML, or TXT file', // History panel sections 'history.section.pinned': 'Pinned', diff --git a/workspaces/lightspeed/yarn.lock b/workspaces/lightspeed/yarn.lock index d5057eb9b3..dabe903d56 100644 --- a/workspaces/lightspeed/yarn.lock +++ b/workspaces/lightspeed/yarn.lock @@ -8503,9 +8503,9 @@ __metadata: languageName: node linkType: hard -"@patternfly/chatbot@npm:6.5.0": - version: 6.5.0 - resolution: "@patternfly/chatbot@npm:6.5.0" +"@patternfly/chatbot@npm:6.6.0-prerelease.6": + version: 6.6.0-prerelease.6 + resolution: "@patternfly/chatbot@npm:6.6.0-prerelease.6" dependencies: "@patternfly/react-code-editor": "npm:^6.1.0" "@patternfly/react-core": "npm:^6.1.0" @@ -8533,7 +8533,7 @@ __metadata: optional: false monaco-editor: optional: false - checksum: 10c0/d9a153e0121179cf8ff74b4e652d615b81b6703dcb46245620c2e93361c1fa7f48b7a6a019b96992b411ce4949450e132de87047a0dd28022e8c910548696ccf + checksum: 10c0/6a60b7a48134e0dc3188835196b68f983383378c3d6a96f7276a14083633d70307a0dc45c543c1df05d5e20f6d09323b341bda12218bb7fb28b452a883b7d6c7 languageName: node linkType: hard @@ -11245,7 +11245,7 @@ __metadata: "@mui/icons-material": "npm:^6.1.8" "@mui/material": "npm:^5.12.2" "@mui/styles": "npm:5.18.0" - "@patternfly/chatbot": "npm:6.5.0" + "@patternfly/chatbot": "npm:6.6.0-prerelease.6" "@patternfly/react-core": "npm:6.4.1" "@patternfly/react-icons": "npm:^6.3.1" "@patternfly/react-table": "npm:^6.4.1" From 023fcf3cea8caec35484e4dbf8ee5d6026f23271 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Thu, 7 May 2026 19:37:07 +0530 Subject: [PATCH 07/12] deleting unused files Signed-off-by: its-mitesh-kumar --- .../src/components/AttachPlusMenu.tsx | 221 ------------------ .../__tests__/AttachPlusMenu.test.tsx | 194 --------------- 2 files changed, 415 deletions(-) delete mode 100644 workspaces/lightspeed/plugins/lightspeed/src/components/AttachPlusMenu.tsx delete mode 100644 workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AttachPlusMenu.test.tsx diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/AttachPlusMenu.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/AttachPlusMenu.tsx deleted file mode 100644 index b15cb0b610..0000000000 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/AttachPlusMenu.tsx +++ /dev/null @@ -1,221 +0,0 @@ -/* - * 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 { ChangeEvent, Ref, useRef, useState } from 'react'; -import { DropEvent, FileRejection } from 'react-dropzone'; - -import { makeStyles } from '@material-ui/core'; -import { - Dropdown, - DropdownItem, - DropdownList, - MenuToggle, - MenuToggleElement, -} from '@patternfly/react-core'; -import { PaperclipIcon, PlusIcon } from '@patternfly/react-icons'; - -import { useTranslation } from '../hooks/useTranslation'; - -type AttachPlusMenuProps = { - onAttach: ( - data: File[], - event: DropEvent | ChangeEvent, - ) => void; - allowedFileTypes?: { [key: string]: string[] }; - onAttachRejected?: (rejections: FileRejection[]) => void; -}; - -const useStyles = makeStyles(theme => ({ - plusButton: { - width: 40, - height: 40, - padding: 0, - minWidth: 0, - borderRadius: '50%', - backgroundColor: 'transparent', - color: theme.palette.text.secondary, - '&:hover': { - backgroundColor: theme.palette.action.hover, - }, - }, - dropdown: { - padding: 0, - '& ul': { - padding: 0, - }, - '& .pf-v6-c-menu__content, & .pf-v5-c-menu__content': { - paddingTop: 4, - paddingBottom: 4, - }, - }, - menuItem: { - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-start', - gap: 4, - padding: theme.spacing(1.5), - cursor: 'pointer', - minWidth: 280, - '--pf-v6-c-menu__item--PaddingTop': `${theme.spacing(1.5)}px`, - '--pf-v6-c-menu__item--PaddingBottom': `${theme.spacing(1.5)}px`, - }, - menuItemContent: { - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-start', - gap: 4, - width: '100%', - }, - menuItemHeader: { - display: 'flex', - alignItems: 'center', - gap: 8, - fontSize: 14, - fontWeight: 500, - color: theme.palette.text.primary, - }, - menuItemDescription: { - fontSize: 14, - color: theme.palette.text.secondary, - paddingLeft: 26, - }, - paperclipIcon: { - width: 17.5, - height: 20, - color: theme.palette.text.primary, - }, - hiddenInput: { - display: 'none', - }, -})); - -export const AttachPlusMenu = ({ - onAttach, - allowedFileTypes, - onAttachRejected, -}: AttachPlusMenuProps) => { - const [isMenuOpen, setIsMenuOpen] = useState(false); - const classes = useStyles(); - const { t } = useTranslation(); - - const fileInputRef = useRef(null); - - const handleAttachClick = () => { - setIsMenuOpen(false); - fileInputRef.current?.click(); - }; - - const handleFileChange = (event: ChangeEvent) => { - const files = Array.from(event.target.files || []); - if (files.length === 0) return; - - if (allowedFileTypes && onAttachRejected) { - const allowedExtensions = Object.values(allowedFileTypes).flat(); - const accepted: File[] = []; - const rejected: FileRejection[] = []; - - files.forEach(file => { - const ext = `.${file.name.split('.').pop()?.toLowerCase()}`; - if (allowedExtensions.includes(ext)) { - accepted.push(file); - } else { - rejected.push({ - file, - errors: [ - { - code: 'file-invalid-type', - message: `File type ${ext} is not supported`, - }, - ], - }); - } - }); - - if (rejected.length > 0) { - onAttachRejected(rejected); - } - - if (accepted.length > 0) { - onAttach(accepted, event); - } - } else { - onAttach(files, event); - } - - if (fileInputRef.current) { - fileInputRef.current.value = ''; - } - }; - - const acceptTypes = allowedFileTypes - ? Object.entries(allowedFileTypes) - .map(([mime, exts]) => [mime, ...exts]) - .flat() - .join(',') - : undefined; - - const toggle = (toggleRef: Ref) => ( - setIsMenuOpen(!isMenuOpen)} - isExpanded={isMenuOpen} - variant="plain" - className={classes.plusButton} - aria-label={t('tooltip.attach')} - > - - - ); - - return ( - <> - setIsMenuOpen(false)} - onOpenChange={isOpen => setIsMenuOpen(isOpen)} - toggle={toggle} - popperProps={{ position: 'left' }} - > - - -
-
- - {t('attach.menu.title')} -
-
- {t('attach.menu.description')} -
-
-
-
-
- - - ); -}; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AttachPlusMenu.test.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AttachPlusMenu.test.tsx deleted file mode 100644 index 9996152cce..0000000000 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AttachPlusMenu.test.tsx +++ /dev/null @@ -1,194 +0,0 @@ -/* - * 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 { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -import { mockUseTranslation } from '../../test-utils/mockTranslations'; -import { AttachPlusMenu } from '../AttachPlusMenu'; - -jest.mock('../../hooks/useTranslation', () => ({ - useTranslation: jest.fn(() => mockUseTranslation()), -})); - -describe('AttachPlusMenu', () => { - const mockOnAttach = jest.fn(); - const mockOnAttachRejected = jest.fn(); - const allowedFileTypes = { - 'text/plain': ['.txt'], - 'application/json': ['.json'], - 'application/yaml': ['.yaml', '.yml'], - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should render the plus button toggle', () => { - render(); - - const toggleButton = screen.getByRole('button', { name: 'Attach' }); - expect(toggleButton).toBeInTheDocument(); - }); - - it('should open dropdown menu when toggle is clicked', async () => { - render(); - - const toggleButton = screen.getByRole('button', { name: 'Attach' }); - await userEvent.click(toggleButton); - - await waitFor(() => { - expect(screen.getByText('Attach')).toBeInTheDocument(); - }); - }); - - it('should show attach menu item with description', async () => { - render(); - - const toggleButton = screen.getByRole('button', { name: 'Attach' }); - await userEvent.click(toggleButton); - - await waitFor(() => { - expect(screen.getByText('Attach')).toBeInTheDocument(); - expect( - screen.getByText('Attach a JSON, YAML, or TXT file'), - ).toBeInTheDocument(); - }); - }); - - it('should have hidden file input', () => { - render(); - - const fileInput = screen.getByTestId('attachment-input'); - expect(fileInput).toBeInTheDocument(); - expect(fileInput).toHaveAttribute('type', 'file'); - }); - - it('should set correct accept attribute when allowedFileTypes provided', () => { - render( - , - ); - - const fileInput = screen.getByTestId( - 'attachment-input', - ) as HTMLInputElement; - expect(fileInput.accept).toContain('text/plain'); - expect(fileInput.accept).toContain('.txt'); - expect(fileInput.accept).toContain('application/json'); - expect(fileInput.accept).toContain('.json'); - }); - - it('should call onAttach when valid files are selected', async () => { - render(); - - const fileInput = screen.getByTestId('attachment-input'); - const file = new File(['test content'], 'test.txt', { type: 'text/plain' }); - - fireEvent.change(fileInput, { target: { files: [file] } }); - - expect(mockOnAttach).toHaveBeenCalledWith([file], expect.any(Object)); - }); - - it('should call onAttachRejected for invalid file types', async () => { - render( - , - ); - - const fileInput = screen.getByTestId('attachment-input'); - const invalidFile = new File(['test content'], 'test.pdf', { - type: 'application/pdf', - }); - - fireEvent.change(fileInput, { target: { files: [invalidFile] } }); - - expect(mockOnAttachRejected).toHaveBeenCalledWith([ - expect.objectContaining({ - file: invalidFile, - errors: expect.arrayContaining([ - expect.objectContaining({ - code: 'file-invalid-type', - }), - ]), - }), - ]); - expect(mockOnAttach).not.toHaveBeenCalled(); - }); - - it('should accept valid files and reject invalid files in mixed selection', async () => { - render( - , - ); - - const fileInput = screen.getByTestId('attachment-input'); - const validFile = new File(['valid'], 'valid.txt', { type: 'text/plain' }); - const invalidFile = new File(['invalid'], 'invalid.exe', { - type: 'application/octet-stream', - }); - - fireEvent.change(fileInput, { - target: { files: [validFile, invalidFile] }, - }); - - expect(mockOnAttach).toHaveBeenCalledWith([validFile], expect.any(Object)); - expect(mockOnAttachRejected).toHaveBeenCalledWith([ - expect.objectContaining({ - file: invalidFile, - }), - ]); - }); - - it('should close dropdown after clicking attach menu item', async () => { - render(); - - const toggleButton = screen.getByRole('button', { name: 'Attach' }); - await userEvent.click(toggleButton); - - await waitFor(() => { - expect( - screen.getByText('Attach a JSON, YAML, or TXT file'), - ).toBeInTheDocument(); - }); - - const attachMenuItem = screen.getByText('Attach a JSON, YAML, or TXT file'); - await userEvent.click(attachMenuItem); - - await waitFor(() => { - expect( - screen.queryByText('Attach a JSON, YAML, or TXT file'), - ).not.toBeInTheDocument(); - }); - }); - - it('should not call onAttach when no files are selected', () => { - render(); - - const fileInput = screen.getByTestId('attachment-input'); - fireEvent.change(fileInput, { target: { files: [] } }); - - expect(mockOnAttach).not.toHaveBeenCalled(); - }); -}); From 0e249329e6874379e0e5a161f6a9d90ed68dbae6 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Thu, 7 May 2026 20:03:10 +0530 Subject: [PATCH 08/12] reusing SidebarExpandIcon Signed-off-by: its-mitesh-kumar --- .../src/components/CollapsedHistoryStrip.tsx | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/CollapsedHistoryStrip.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/CollapsedHistoryStrip.tsx index 1a90fdfce8..db1bb667a6 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/CollapsedHistoryStrip.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/CollapsedHistoryStrip.tsx @@ -18,24 +18,12 @@ import { makeStyles } from '@material-ui/core'; import { Button, Tooltip } from '@patternfly/react-core'; import { useTranslation } from '../hooks/useTranslation'; +import { SidebarExpandIcon } from './notebooks/SidebarCollapseIcon'; type IconProps = { className?: string; }; -const ExpandPanelIcon = ({ className }: IconProps) => ( - - - -); - export const EditSquareIcon = ({ className }: IconProps) => ( - + From 2e80921f907b88157ac87d4dd1977f0103cbbd51 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Thu, 7 May 2026 22:07:12 +0530 Subject: [PATCH 09/12] updating the e2e Signed-off-by: its-mitesh-kumar --- .../lightspeed/e2e-tests/utils/fileUpload.ts | 18 +++++++++++++++--- .../src/components/LightSpeedChat.tsx | 2 ++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/workspaces/lightspeed/e2e-tests/utils/fileUpload.ts b/workspaces/lightspeed/e2e-tests/utils/fileUpload.ts index aa3c8bdd90..eb11179610 100644 --- a/workspaces/lightspeed/e2e-tests/utils/fileUpload.ts +++ b/workspaces/lightspeed/e2e-tests/utils/fileUpload.ts @@ -45,6 +45,13 @@ export async function uploadFiles( // Use the hidden file input directly - this bypasses the dropdown menu // The input has the multiple attribute so it can accept multiple files const fileInput = page.locator('input[data-testid="attachment-input"]'); + + // Clear the input first to ensure change event fires even for the same file + // This is necessary because browsers don't fire 'change' if the same file is selected again + await fileInput.evaluate((el: HTMLInputElement) => { + el.value = ''; + }); + await fileInput.setInputFiles(filePath); } @@ -53,15 +60,20 @@ export async function uploadAndAssertDuplicate( filePath: string, fileName: string, translations: LightspeedMessages, - testInfo: TestInfo, + _testInfo: TestInfo, ) { - await validateSuccessfulUpload(page, fileName, translations, testInfo); + // First, verify the initial upload was successful by checking the file button is visible + await expect(page.getByRole('button', { name: fileName })).toBeVisible(); + + // Upload the same file again to trigger duplicate detection await uploadFiles(page, [filePath], translations); + + // Assert the duplicate file error alert appears await expect( page.getByRole('heading', { name: translations['chatbox.fileUpload.failed'], }), - ).toBeVisible(); + ).toBeVisible({ timeout: 10000 }); await expect( page.getByText(translations['file.upload.error.alreadyExists']), ).toBeVisible(); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx index 73169f8624..c18c8e0b4e 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx @@ -1682,6 +1682,7 @@ export const LightspeedChat = ({ attach: { inputTestId: 'attachment-input', tooltipContent: t('tooltip.attach'), + 'aria-label': t('tooltip.attach'), icon: , }, microphone: { @@ -1729,6 +1730,7 @@ export const LightspeedChat = ({ attach: { inputTestId: 'attachment-input', tooltipContent: t('tooltip.attach'), + 'aria-label': t('tooltip.attach'), }, microphone: { tooltipContent: { From 0b7442163a7cdaca1369d15398854fdd458a88ac Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Fri, 8 May 2026 03:00:02 +0530 Subject: [PATCH 10/12] moving collapse icon bit top right Signed-off-by: its-mitesh-kumar --- .../lightspeed/src/components/CollapsedHistoryStrip.tsx | 5 ++++- .../plugins/lightspeed/src/components/LightSpeedChat.tsx | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/CollapsedHistoryStrip.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/CollapsedHistoryStrip.tsx index db1bb667a6..d6d1e63d7e 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/CollapsedHistoryStrip.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/CollapsedHistoryStrip.tsx @@ -56,12 +56,15 @@ const useStyles = makeStyles(theme => ({ height: '100%', }, iconButton: { - padding: 0, + padding: 8, minWidth: 0, lineHeight: 1, + borderRadius: '50%', color: 'var(--pf-t--global--icon--color--regular)', '&:hover': { color: 'var(--pf-t--global--icon--color--hover)', + backgroundColor: + 'var(--pf-t--global--background--color--action--plain--hover)', }, }, newChatIconButton: { diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx index c18c8e0b4e..27959cb93d 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx @@ -507,6 +507,10 @@ const useStyles = makeStyles(theme => ({ { display: 'none', }, + '& .pf-v6-c-drawer__close, & .pf-v5-c-drawer__close': { + marginTop: -48, + marginRight: -24, + }, '& .pf-v6-c-drawer__close .pf-v6-c-button svg, & .pf-v5-c-drawer__close .pf-v5-c-button svg': { display: 'none', From 7fecb4970cc5cdf6585fce9c5157abc089b2cb24 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Fri, 8 May 2026 17:29:42 +0530 Subject: [PATCH 11/12] addressing the comments Signed-off-by: its-mitesh-kumar --- .../src/components/LightSpeedChat.tsx | 239 ++++++------------ 1 file changed, 79 insertions(+), 160 deletions(-) diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx index 27959cb93d..9cf654ec19 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx @@ -123,6 +123,18 @@ import { RenameNotebookModal } from './notebooks/RenameNotebookModal'; import PermissionRequiredState from './PermissionRequiredState'; import { RenameConversationModal } from './RenameConversationModal'; +const COLLAPSE_PANEL_ICON_SVG = `url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16 21V3H14V21H16ZM12 17V7L7 12L12 17Z' fill='black'/%3E%3C/svg%3E") no-repeat center`; + +const ConditionalWrapper = ({ + condition, + wrapper, + children, +}: { + condition: boolean; + wrapper: (children: React.ReactNode) => React.ReactNode; + children: React.ReactNode; +}) => (condition ? wrapper(children) : children); + const useStyles = makeStyles(theme => ({ body: { // remove default margin and padding from common elements @@ -488,6 +500,10 @@ const useStyles = makeStyles(theme => ({ transition: 'none !important', }, }, + // TODO: These PatternFly drawer overrides are needed because PF Chatbot doesn't + // provide clean APIs for custom expand/collapse icons and positioning. + // Remove once PatternFly supports these features. + // See: https://github.com/patternfly/chatbot/issues/834 fullscreenChatLayout: { display: 'flex', flexDirection: 'row', @@ -525,8 +541,8 @@ const useStyles = makeStyles(theme => ({ display: 'block', width: 24, height: 24, - mask: `url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16 21V3H14V21H16ZM12 17V7L7 12L12 17Z' fill='black'/%3E%3C/svg%3E") no-repeat center`, - WebkitMask: `url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16 21V3H14V21H16ZM12 17V7L7 12L12 17Z' fill='black'/%3E%3C/svg%3E") no-repeat center`, + mask: COLLAPSE_PANEL_ICON_SVG, + WebkitMask: COLLAPSE_PANEL_ICON_SVG, backgroundColor: 'currentColor', }, }, @@ -1666,40 +1682,42 @@ export const LightspeedChat = ({ } > - {isFullscreenMode ? ( - , - }, - microphone: { - tooltipContent: { - active: t('tooltip.microphone.active'), - inactive: t('tooltip.microphone.inactive'), - }, - }, - send: { - tooltipContent: t('tooltip.send'), + }), + }, + microphone: { + tooltipContent: { + active: t('tooltip.microphone.active'), + inactive: t('tooltip.microphone.inactive'), }, - }} - additionalActions={ + }, + send: { + tooltipContent: t('tooltip.send'), + }, + }} + additionalActions={ + isFullscreenMode ? ( - } - forceMultilineLayout - allowedFileTypes={supportedFileTypes} - onAttachRejected={onAttachRejected} - placeholder={t('chatbox.message.placeholder')} - /> - ) : ( - - )} + ) : undefined + } + forceMultilineLayout={isFullscreenMode} + allowedFileTypes={supportedFileTypes} + onAttachRejected={onAttachRejected} + placeholder={t('chatbox.message.placeholder')} + /> @@ -1931,19 +1915,26 @@ export const LightspeedChat = ({
)} - {showChatPanel && isFullscreenMode && ( -
- {!isChatHistoryDrawerOpen && ( - setIsChatHistoryDrawerOpen(true)} - onNewChat={onNewChat} - newChatDisabled={newChatCreated} - /> + {showChatPanel && ( + ( +
+ {!isChatHistoryDrawerOpen && ( + setIsChatHistoryDrawerOpen(true)} + onNewChat={onNewChat} + newChatDisabled={newChatCreated} + /> + )} + {children} +
)} + > , + icon: isFullscreenMode ? : , }} handleTextInputChange={handleFilter} searchInputPlaceholder={t('chatbox.search.placeholder')} @@ -2010,79 +2001,7 @@ export const LightspeedChat = ({ } /> -
- )} - {showChatPanel && !isFullscreenMode && ( - , - }} - handleTextInputChange={handleFilter} - searchInputPlaceholder={t('chatbox.search.placeholder')} - searchInputAriaLabel={t('aria.search.placeholder')} - searchInputProps={{ - value: filterValue, - onClear: () => { - setFilterValue(''); - }, - }} - searchActionEnd={sortDropdown} - noResultsState={ - filterValue && - Object.keys(filterConversations(filterValue)).length === 0 - ? { - bodyText: t('chatbox.emptyState.noResults.body'), - titleText: t('chatbox.emptyState.noResults.title'), - icon: SearchIcon, - } - : undefined - } - drawerContent={ - handleAttach(data, e)} - displayMode={ChatbotDisplayMode.embedded} - infoText={t('chatbox.fileUpload.infoText')} - allowedFileTypes={supportedFileTypes} - onAttachRejected={onAttachRejected} - > - {showAlert && uploadError.message && ( -
- setUploadError({ message: null })} - > - {uploadError.message} - -
- )} - {mainPanelContent} -
- } - /> + )} {showNotebooksPanel && !notebooksPermissionLoading && From 5171b9658a162ee0c0356a6ef196f1c872f32f21 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Fri, 8 May 2026 21:22:16 +0530 Subject: [PATCH 12/12] adding divider Signed-off-by: its-mitesh-kumar --- .../lightspeed/src/components/LightSpeedChat.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx index 9cf654ec19..26dd50dc0f 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx @@ -99,7 +99,7 @@ import { useLightspeedDrawerContext } from '../hooks/useLightspeedDrawerContext' import { useLightspeedUpdatePermission } from '../hooks/useLightspeedUpdatePermission'; import { useTranslation } from '../hooks/useTranslation'; import { useWelcomePrompts } from '../hooks/useWelcomePrompts'; -import roundedLogo from '../images/rounded-logo.svg'; +import logo from '../images/logo.svg'; import { ConversationSummary, NotebookSession } from '../types'; import { getAttachments } from '../utils/attachment-utils'; import { @@ -147,6 +147,8 @@ const useStyles = makeStyles(theme => ({ padding: `${theme.spacing(3)}px ${theme.spacing(3)}px 0 ${theme.spacing( 3, )}px !important`, + backgroundColor: + 'var(--pf-t--global--background--color--floating--default) !important', }, errorContainer: { padding: theme.spacing(3), @@ -183,7 +185,7 @@ const useStyles = makeStyles(theme => ({ }, }, tabs: { - padding: `${theme.spacing(2)}px ${theme.spacing(3)}px 0`, + padding: `0 ${theme.spacing(2)}px`, backgroundColor: 'var(--pf-t--global--background--color--floating--default)', '& .pf-v6-c-tabs__item, & .pf-v5-c-tabs__item': { @@ -208,6 +210,12 @@ const useStyles = makeStyles(theme => ({ tabsDivider: { borderTop: '1px solid var(--pf-t--global--border--color--default)', }, + headerDivider: { + paddingTop: 8, + borderBottom: '1px solid var(--pf-t--global--border--color--default)', + backgroundColor: + 'var(--pf-t--global--background--color--floating--default)', + }, notebooksContainer: { padding: theme.spacing(3), height: '100%', @@ -1870,7 +1878,7 @@ export const LightspeedChat = ({ {isFullscreenMode && ( <> {t('icon.lightspeed.alt')} @@ -1901,6 +1909,7 @@ export const LightspeedChat = ({ onMcpSettingsClick={() => setIsMcpSettingsOpen(true)} /> + {isFullscreenMode &&
} {isFullscreenMode && ( <>