From 709c940bdb19d43fa493f4a1aa607f9229ad3bc8 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Tue, 28 Apr 2026 02:02:56 +0530 Subject: [PATCH 1/9] fix(lightspeed): improve notebook upload modal and MessageBar UX Signed-off-by: its-mitesh-kumar --- .../.changeset/notebook-upload-modal-ux.md | 5 + .../plugins/lightspeed/report-alpha.api.md | 4 + .../__tests__/AddDocumentModal.test.tsx | 281 ++++++++++++++++++ .../__tests__/FileListItem.test.tsx | 133 +++++++++ .../components/notebooks/AddDocumentModal.tsx | 176 ++++++++--- .../src/components/notebooks/FileListItem.tsx | 125 ++++++++ .../src/components/notebooks/NotebookView.tsx | 35 ++- .../notebooks/OverwriteConfirmModal.tsx | 1 + .../lightspeed/src/translations/ref.ts | 5 + 9 files changed, 719 insertions(+), 46 deletions(-) create mode 100644 workspaces/lightspeed/.changeset/notebook-upload-modal-ux.md create mode 100644 workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AddDocumentModal.test.tsx create mode 100644 workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/FileListItem.test.tsx create mode 100644 workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/FileListItem.tsx diff --git a/workspaces/lightspeed/.changeset/notebook-upload-modal-ux.md b/workspaces/lightspeed/.changeset/notebook-upload-modal-ux.md new file mode 100644 index 00000000000..f663adee8c7 --- /dev/null +++ b/workspaces/lightspeed/.changeset/notebook-upload-modal-ux.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-lightspeed': minor +--- + +Improved notebook upload modal and MessageBar UX. diff --git a/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md b/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md index f954c7c2b76..4bb031f10d4 100644 --- a/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md +++ b/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md @@ -230,6 +230,7 @@ export const lightspeedTranslationRef: TranslationRef< readonly 'notebook.view.upload.heading': string; readonly 'notebook.view.upload.action': string; readonly 'notebook.view.input.placeholder': string; + readonly 'notebook.view.input.disabledTooltip': string; readonly 'notebook.view.sidebar.collapse': string; readonly 'notebook.view.sidebar.expand': string; readonly 'notebook.view.sidebar.resize': string; @@ -241,6 +242,9 @@ export const lightspeedTranslationRef: TranslationRef< readonly 'notebook.upload.modal.browseButton': string; readonly 'notebook.upload.modal.separator': string; readonly 'notebook.upload.modal.infoText': string; + readonly 'notebook.upload.modal.selectedFiles': string; + readonly 'notebook.upload.modal.addButton': string; + readonly 'notebook.upload.modal.removeFile': string; readonly 'notebook.upload.error.unsupportedType': string; readonly 'notebook.upload.error.fileTooLarge': string; readonly 'notebook.upload.error.tooManyFiles': string; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AddDocumentModal.test.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AddDocumentModal.test.tsx new file mode 100644 index 00000000000..a2e0e29bf1f --- /dev/null +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AddDocumentModal.test.tsx @@ -0,0 +1,281 @@ +/* + * 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 { mockUseTranslation } from '../../test-utils/mockTranslations'; +import { AddDocumentModal } from '../notebooks/AddDocumentModal'; + +jest.mock('../../hooks/useTranslation', () => ({ + useTranslation: jest.fn(() => mockUseTranslation()), +})); + +const mockMutateAsync = jest.fn(); +jest.mock('../../hooks/notebooks/useUploadDocument', () => ({ + useUploadDocument: () => ({ + mutateAsync: mockMutateAsync, + }), +})); + +describe('AddDocumentModal', () => { + const defaultProps = { + isOpen: true, + onClose: jest.fn(), + sessionId: 'test-session-id', + existingDocumentNames: [], + onFilesUploading: jest.fn(), + onUploadStarted: jest.fn(), + onUploadFailed: jest.fn(), + onDuplicatesFound: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockMutateAsync.mockResolvedValue({ document_id: 'test-doc-id' }); + }); + + it('should render the modal when open', () => { + render(); + + expect(screen.getByText('Add a document to Notebook')).toBeInTheDocument(); + expect(screen.getByText('Drag and drop files here')).toBeInTheDocument(); + }); + + it('should not render when isOpen is false', () => { + render(); + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + + it('should render Cancel and Add buttons', () => { + render(); + + expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Add (0)' })).toBeInTheDocument(); + }); + + it('should have Add button disabled when no files selected', () => { + render(); + + const addButton = screen.getByRole('button', { name: 'Add (0)' }); + expect(addButton).toBeDisabled(); + }); + + it('should call onClose when Cancel button is clicked', () => { + render(); + + const cancelButton = screen.getByRole('button', { name: 'Cancel' }); + fireEvent.click(cancelButton); + + expect(defaultProps.onClose).toHaveBeenCalledTimes(1); + }); + + it('should call onClose when close icon is clicked', () => { + render(); + + const closeButton = screen.getByRole('button', { name: 'Close' }); + fireEvent.click(closeButton); + + expect(defaultProps.onClose).toHaveBeenCalledTimes(1); + }); + + it('should display file list when files are dropped', async () => { + render(); + + const dropzone = screen + .getByText('Drag and drop files here') + .closest('div'); + const file = new File(['content'], 'test-file.txt', { type: 'text/plain' }); + + fireEvent.drop(dropzone!, { + dataTransfer: { + files: [file], + types: ['Files'], + }, + }); + + await waitFor(() => { + expect(screen.getByText('test-file.txt')).toBeInTheDocument(); + }); + }); + + it('should update Add button count when files are selected', async () => { + render(); + + const dropzone = screen + .getByText('Drag and drop files here') + .closest('div'); + const file = new File(['content'], 'test-file.txt', { type: 'text/plain' }); + + fireEvent.drop(dropzone!, { + dataTransfer: { + files: [file], + types: ['Files'], + }, + }); + + await waitFor(() => { + expect( + screen.getByRole('button', { name: 'Add (1)' }), + ).toBeInTheDocument(); + }); + }); + + it('should not auto-close modal after file drop', async () => { + render(); + + const dropzone = screen + .getByText('Drag and drop files here') + .closest('div'); + const file = new File(['content'], 'test-file.txt', { type: 'text/plain' }); + + fireEvent.drop(dropzone!, { + dataTransfer: { + files: [file], + types: ['Files'], + }, + }); + + await waitFor(() => { + expect(screen.getByText('test-file.txt')).toBeInTheDocument(); + }); + + expect(defaultProps.onClose).not.toHaveBeenCalled(); + }); + + it('should trigger upload and close when Add button is clicked', async () => { + render(); + + const dropzone = screen + .getByText('Drag and drop files here') + .closest('div'); + const file = new File(['content'], 'test-file.txt', { type: 'text/plain' }); + + fireEvent.drop(dropzone!, { + dataTransfer: { + files: [file], + types: ['Files'], + }, + }); + + await waitFor(() => { + expect( + screen.getByRole('button', { name: 'Add (1)' }), + ).not.toBeDisabled(); + }); + + fireEvent.click(screen.getByRole('button', { name: 'Add (1)' })); + + await waitFor(() => { + expect(defaultProps.onFilesUploading).toHaveBeenCalledWith([file]); + expect(mockMutateAsync).toHaveBeenCalledWith({ + sessionId: 'test-session-id', + file, + }); + expect(defaultProps.onClose).toHaveBeenCalled(); + }); + }); + + it('should allow removing files from the list', async () => { + render(); + + const dropzone = screen + .getByText('Drag and drop files here') + .closest('div'); + const file = new File(['content'], 'test-file.txt', { type: 'text/plain' }); + + fireEvent.drop(dropzone!, { + dataTransfer: { + files: [file], + types: ['Files'], + }, + }); + + await waitFor(() => { + expect(screen.getByText('test-file.txt')).toBeInTheDocument(); + }); + + const removeButton = screen.getByRole('button', { + name: 'Remove test-file.txt', + }); + fireEvent.click(removeButton); + + await waitFor(() => { + expect(screen.queryByText('test-file.txt')).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Add (0)' })).toBeDisabled(); + }); + }); + + it('should clear selected files when modal is closed', async () => { + const { rerender } = render(); + + const dropzone = screen + .getByText('Drag and drop files here') + .closest('div'); + const file = new File(['content'], 'test-file.txt', { type: 'text/plain' }); + + fireEvent.drop(dropzone!, { + dataTransfer: { + files: [file], + types: ['Files'], + }, + }); + + await waitFor(() => { + expect(screen.getByText('test-file.txt')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByRole('button', { name: 'Cancel' })); + + rerender(); + + expect(screen.queryByText('test-file.txt')).not.toBeInTheDocument(); + }); + + it('should call onDuplicatesFound for files that already exist', async () => { + render( + , + ); + + const dropzone = screen + .getByText('Drag and drop files here') + .closest('div'); + const existingFile = new File(['content'], 'existing-file.txt', { + type: 'text/plain', + }); + const newFile = new File(['content'], 'new-file.txt', { + type: 'text/plain', + }); + + fireEvent.drop(dropzone!, { + dataTransfer: { + files: [existingFile, newFile], + types: ['Files'], + }, + }); + + await waitFor(() => { + expect(defaultProps.onDuplicatesFound).toHaveBeenCalledWith([ + existingFile, + ]); + expect(screen.getByText('new-file.txt')).toBeInTheDocument(); + expect(screen.queryByText('existing-file.txt')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/FileListItem.test.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/FileListItem.test.tsx new file mode 100644 index 00000000000..4438fb7f756 --- /dev/null +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/FileListItem.test.tsx @@ -0,0 +1,133 @@ +/* + * 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 { FileListItem } from '../notebooks/FileListItem'; + +describe('FileListItem', () => { + const createFile = ( + name: string, + size: number, + type: string = 'text/plain', + ) => new File(['x'.repeat(size)], name, { type }); + + const defaultProps = { + file: createFile('test-file.txt', 1024), + onRemove: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render the file name', () => { + render(); + + expect(screen.getByText('test-file.txt')).toBeInTheDocument(); + }); + + it('should render the file type icon', () => { + render(); + + expect(screen.getByText('txt')).toBeInTheDocument(); + }); + + it('should render the file size in KB', () => { + render(); + + expect(screen.getByText('1 KB')).toBeInTheDocument(); + }); + + it('should render the file size in MB for larger files', () => { + const largeFile = createFile('large-file.pdf', 5 * 1024 * 1024); + render(); + + expect(screen.getByText('5 MB')).toBeInTheDocument(); + }); + + it('should render 0 B for empty files', () => { + const emptyFile = createFile('empty.txt', 0); + render(); + + expect(screen.getByText('0 B')).toBeInTheDocument(); + }); + + it('should call onRemove when remove button is clicked', () => { + render(); + + const removeButton = screen.getByRole('button', { name: 'Remove file' }); + fireEvent.click(removeButton); + + expect(defaultProps.onRemove).toHaveBeenCalledTimes(1); + }); + + it('should use custom aria-label for remove button', () => { + render( + , + ); + + expect( + screen.getByRole('button', { name: 'Remove test-file.txt' }), + ).toBeInTheDocument(); + }); + + it('should render different file type icons based on extension', () => { + const pdfFile = createFile('document.pdf', 1024); + const { rerender } = render( + , + ); + + expect(screen.getByText('pdf')).toBeInTheDocument(); + + const yamlFile = createFile('config.yaml', 1024); + rerender(); + + expect(screen.getByText('yaml')).toBeInTheDocument(); + }); + + it('should show title tooltip for long file names', () => { + const longNameFile = createFile( + 'this-is-a-very-long-filename-that-should-be-truncated.txt', + 1024, + ); + render(); + + const fileNameElement = screen.getByTitle( + 'this-is-a-very-long-filename-that-should-be-truncated.txt', + ); + expect(fileNameElement).toBeInTheDocument(); + }); + + it('should truncate long file names while preserving extension', () => { + const longNameFile = createFile( + 'this-is-a-very-long-filename-that-should-be-truncated.txt', + 1024, + ); + render(); + + expect( + screen.getByText('this-is-a-very-long-fil....txt'), + ).toBeInTheDocument(); + }); + + it('should not truncate short file names', () => { + const shortNameFile = createFile('short-name.pdf', 1024); + render(); + + expect(screen.getByText('short-name.pdf')).toBeInTheDocument(); + }); +}); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/AddDocumentModal.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/AddDocumentModal.tsx index cfb902cc999..e35efd1b319 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/AddDocumentModal.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/AddDocumentModal.tsx @@ -20,7 +20,10 @@ import { FileRejection } from 'react-dropzone'; import { makeStyles } from '@material-ui/core/styles'; import CloseIcon from '@mui/icons-material/Close'; import Alert from '@mui/material/Alert'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import IconButton from '@mui/material/IconButton'; @@ -38,6 +41,7 @@ import { getNotebookAcceptedFileTypes, validateFiles, } from '../../utils/notebook-upload-utils'; +import { FileListItem } from './FileListItem'; const useStyles = makeStyles(theme => ({ dialogPaper: { @@ -76,6 +80,32 @@ const useStyles = makeStyles(theme => ({ 'color-mix(in srgb, var(--pf-t--global--color--brand--default) 10%, transparent)', }, }, + fileListContainer: { + marginTop: theme.spacing(2), + maxHeight: 200, + overflowY: 'auto', + }, + fileListHeader: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: theme.spacing(1), + }, + fileCount: { + fontSize: '0.875rem', + color: theme.palette.text.secondary, + }, + dialogActions: { + padding: '16px 24px', + justifyContent: 'flex-end', + gap: theme.spacing(1), + }, + addButton: { + textTransform: 'none', + }, + cancelButton: { + textTransform: 'none', + }, })); type AddDocumentModalProps = { @@ -103,14 +133,16 @@ export const AddDocumentModal = ({ const { t } = useTranslation(); const uploadMutation = useUploadDocument(); const [validationErrors, setValidationErrors] = useState([]); + const [selectedFiles, setSelectedFiles] = useState([]); + + const totalExistingAndSelected = + existingDocumentNames.length + selectedFiles.length; + const remainingSlots = NOTEBOOK_MAX_FILES - totalExistingAndSelected; const handleFileDrop = (_event: unknown, files: File[]) => { setValidationErrors([]); - const { valid, errors } = validateFiles( - files, - existingDocumentNames.length, - ); + const { valid, errors } = validateFiles(files, totalExistingAndSelected); if (errors.length > 0) { setValidationErrors(errors); @@ -119,31 +151,47 @@ export const AddDocumentModal = ({ if (valid.length === 0) return; - const existingNamesSet = new Set(existingDocumentNames); + const existingNamesSet = new Set([ + ...existingDocumentNames, + ...selectedFiles.map(f => f.name), + ]); const newFiles = valid.filter(f => !existingNamesSet.has(f.name)); - const duplicateFiles = valid.filter(f => existingNamesSet.has(f.name)); + const duplicateFiles = valid.filter(f => + existingDocumentNames.includes(f.name), + ); + + if (duplicateFiles.length > 0) { + onDuplicatesFound?.(duplicateFiles); + } if (newFiles.length > 0) { - onFilesUploading?.(newFiles); - for (const file of newFiles) { - uploadMutation - .mutateAsync({ sessionId, file }) - .then(data => { - onUploadStarted?.({ - fileName: file.name, - documentId: data.document_id, - }); - }) - .catch(() => { - onUploadFailed?.(file.name); - }); - } + setSelectedFiles(prev => [...prev, ...newFiles]); } + }; - if (duplicateFiles.length > 0) { - onDuplicatesFound?.(duplicateFiles); + const handleRemoveFile = (index: number) => { + setSelectedFiles(prev => prev.filter((_, i) => i !== index)); + }; + + const handleAddFiles = () => { + if (selectedFiles.length === 0) return; + + onFilesUploading?.(selectedFiles); + for (const file of selectedFiles) { + uploadMutation + .mutateAsync({ sessionId, file }) + .then(data => { + onUploadStarted?.({ + fileName: file.name, + documentId: data.document_id, + }); + }) + .catch(() => { + onUploadFailed?.(file.name); + }); } + setSelectedFiles([]); setValidationErrors([]); onClose(); }; @@ -158,6 +206,7 @@ export const AddDocumentModal = ({ }; const handleClose = () => { + setSelectedFiles([]); setValidationErrors([]); onClose(); }; @@ -174,6 +223,8 @@ export const AddDocumentModal = ({ {t('notebook.upload.modal.title')} + {selectedFiles.length > 0 && + ` (${selectedFiles.length}/${NOTEBOOK_MAX_FILES - existingDocumentNames.length})`} )} - - } - titleText={t('notebook.upload.modal.dragDropTitle')} - titleTextSeparator={t('notebook.upload.modal.separator')} - infoText={t('notebook.upload.modal.infoText')} - browseButtonText={t('notebook.upload.modal.browseButton')} - /> - + {remainingSlots > 0 && ( + + } + titleText={t('notebook.upload.modal.dragDropTitle')} + titleTextSeparator={t('notebook.upload.modal.separator')} + infoText={t('notebook.upload.modal.infoText')} + browseButtonText={t('notebook.upload.modal.browseButton')} + /> + + )} + + {selectedFiles.length > 0 && ( + + + + {(t as Function)('notebook.upload.modal.selectedFiles', { + count: selectedFiles.length, + max: NOTEBOOK_MAX_FILES - existingDocumentNames.length, + })} + + + {selectedFiles.map((file, index) => ( + handleRemoveFile(index)} + removeAriaLabel={(t as Function)( + 'notebook.upload.modal.removeFile', + { + fileName: file.name, + }, + )} + /> + ))} + + )} + + + + + ); }; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/FileListItem.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/FileListItem.tsx new file mode 100644 index 00000000000..d6303b7d9fd --- /dev/null +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/FileListItem.tsx @@ -0,0 +1,125 @@ +/* + * 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/styles'; +import CloseIcon from '@mui/icons-material/Close'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; + +import { FileTypeIcon } from './FileTypeIcon'; + +const useStyles = makeStyles(theme => ({ + container: { + display: 'flex', + alignItems: 'center', + padding: '8px 12px', + borderRadius: 8, + backgroundColor: theme.palette.type === 'dark' ? '#2a2a2a' : '#f5f5f5', + marginBottom: 8, + '&:last-child': { + marginBottom: 0, + }, + }, + fileInfo: { + display: 'flex', + alignItems: 'center', + flex: 1, + minWidth: 0, + gap: 12, + }, + fileName: { + flex: 1, + minWidth: 0, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + fontSize: '0.875rem', + }, + fileSize: { + color: theme.palette.text.secondary, + fontSize: '0.75rem', + flexShrink: 0, + marginRight: 8, + }, + removeButton: { + padding: 4, + color: theme.palette.grey[600], + '&:hover': { + color: theme.palette.error.main, + }, + }, +})); + +type FileListItemProps = { + file: File; + onRemove: () => void; + removeAriaLabel?: string; +}; + +const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; +}; + +const MAX_FILENAME_LENGTH = 30; + +const truncateFileName = (fileName: string, maxLength: number): string => { + if (fileName.length <= maxLength) return fileName; + + const lastDot = fileName.lastIndexOf('.'); + const extension = lastDot >= 0 ? fileName.slice(lastDot) : ''; + const baseName = lastDot >= 0 ? fileName.slice(0, lastDot) : fileName; + + const availableLength = maxLength - extension.length - 3; + if (availableLength <= 0) return fileName; + + return `${baseName.slice(0, availableLength)}...${extension}`; +}; + +export const FileListItem = ({ + file, + onRemove, + removeAriaLabel = 'Remove file', +}: FileListItemProps) => { + const classes = useStyles(); + const displayName = truncateFileName(file.name, MAX_FILENAME_LENGTH); + + return ( + + + + + {displayName} + + + + {formatFileSize(file.size)} + + + + + + ); +}; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx index b498d20cd65..b66e1d67355 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx @@ -628,14 +628,33 @@ export const NotebookView = ({ )} - + {documents.length === 0 ? ( + +
+ +
+
+ ) : ( + + )}
diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/OverwriteConfirmModal.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/OverwriteConfirmModal.tsx index 8e52600a287..a4f2fe7da31 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/OverwriteConfirmModal.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/OverwriteConfirmModal.tsx @@ -63,6 +63,7 @@ const useStyles = makeStyles(theme => ({ padding: `${theme.spacing(2)}px 0`, borderBottom: '1px solid var(--pf-t--global--border--color--default, #c7c7c7)', + cursor: 'pointer', }, fileName: { flex: 1, diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts index 5f494fc6dbd..216af6177f0 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts @@ -61,6 +61,8 @@ export const lightspeedMessages = { 'notebook.view.upload.heading': 'Upload a resource to get started', 'notebook.view.upload.action': 'Upload a resource', 'notebook.view.input.placeholder': 'Ask about your documents...', + 'notebook.view.input.disabledTooltip': + 'Select at least one loaded resource to start chatting', 'notebook.view.sidebar.collapse': 'Collapse sidebar', 'notebook.view.sidebar.expand': 'Expand sidebar', 'notebook.view.sidebar.resize': 'Resize sidebar', @@ -75,6 +77,9 @@ export const lightspeedMessages = { 'notebook.upload.modal.separator': 'or', 'notebook.upload.modal.infoText': 'Accepted file types: .md, .txt, .pdf, .json, .yaml, .log', + 'notebook.upload.modal.selectedFiles': '{{count}} of {{max}} files selected', + 'notebook.upload.modal.addButton': 'Add ({{count}})', + 'notebook.upload.modal.removeFile': 'Remove {{fileName}}', 'notebook.upload.error.unsupportedType': 'Upload error: Unsupported file type(s) found. Please upload only supported file types.', 'notebook.upload.error.fileTooLarge': From 1757c5b5dffb7a248f224958d96f313e349d6416 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Tue, 28 Apr 2026 11:22:39 +0530 Subject: [PATCH 2/9] toast message auto dismiss Signed-off-by: its-mitesh-kumar --- .../lightspeed/src/components/notebooks/NotebookView.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx index b66e1d67355..eab77df495d 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx @@ -551,6 +551,8 @@ export const NotebookView = ({ variant={AlertVariant[variant ?? 'success']} title={title} className={classes.toastAlert} + timeout={2000} + onTimeout={() => handleRemoveToastAlert(key as React.Key)} actionClose={ Date: Tue, 28 Apr 2026 12:15:35 +0530 Subject: [PATCH 3/9] fix(lightspeed): fix overwrite flow Signed-off-by: its-mitesh-kumar --- .../src/components/LightSpeedChat.tsx | 2 +- .../components/notebooks/AddDocumentModal.tsx | 13 +++++++++- .../src/components/notebooks/NotebookView.tsx | 24 +++++++------------ 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx index 7eefc09d368..bf547a70f44 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx @@ -1483,7 +1483,7 @@ export const LightspeedChat = ({ variant={AlertVariant[variant ?? 'success']} title={title} className={classes.toastAlert} - timeout={8000} + timeout={2000} onTimeout={() => handleRemoveNotebookAlert(key as React.Key)} actionClose={ void; onUploadFailed?: (fileName: string) => void; onDuplicatesFound?: (files: File[]) => void; + filesToAdd?: File[]; + onFilesAdded?: () => void; }; export const AddDocumentModal = ({ @@ -128,6 +130,8 @@ export const AddDocumentModal = ({ onUploadStarted, onUploadFailed, onDuplicatesFound, + filesToAdd, + onFilesAdded, }: AddDocumentModalProps) => { const classes = useStyles(); const { t } = useTranslation(); @@ -139,6 +143,13 @@ export const AddDocumentModal = ({ existingDocumentNames.length + selectedFiles.length; const remainingSlots = NOTEBOOK_MAX_FILES - totalExistingAndSelected; + useEffect(() => { + if (filesToAdd && filesToAdd.length > 0) { + setSelectedFiles(prev => [...prev, ...filesToAdd]); + onFilesAdded?.(); + } + }, [filesToAdd, onFilesAdded]); + const handleFileDrop = (_event: unknown, files: File[]) => { setValidationErrors([]); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx index eab77df495d..f179055ee21 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx @@ -50,7 +50,6 @@ import { useDocumentStatusPolling, type PendingUpload, } from '../../hooks/notebooks/useDocumentStatusPolling'; -import { useUploadDocument } from '../../hooks/notebooks/useUploadDocument'; import { useConversationMessages } from '../../hooks/useConversationMessages'; import { CreateMessageVariables } from '../../hooks/useCreateCoversationMessage'; import { useTranslation } from '../../hooks/useTranslation'; @@ -219,7 +218,6 @@ export const NotebookView = ({ const { t } = useTranslation(); const queryClient = useQueryClient(); const notebooksApi = useApi(notebooksApiRef); - const uploadMutation = useUploadDocument(); const { mutateAsync: notebookCreateMessage } = useCreateNotebookMessage(); const [conversationId, setConversationId] = useState( @@ -331,6 +329,7 @@ export const NotebookView = ({ ); const [filesToOverwrite, setFilesToOverwrite] = useState([]); const [isOverwriteModalOpen, setIsOverwriteModalOpen] = useState(false); + const [filesToAddToModal, setFilesToAddToModal] = useState([]); const handleOpenUploadModal = () => setIsUploadModalOpen(true); const handleCloseUploadModal = () => setIsUploadModalOpen(false); @@ -376,20 +375,11 @@ export const NotebookView = ({ if (files.length === 0) return; - setUploadingFileNames(prev => [...prev, ...files.map(f => f.name)]); - for (const file of files) { - uploadMutation - .mutateAsync({ sessionId, file }) - .then(data => { - handleUploadStarted({ - fileName: file.name, - documentId: data.document_id, - }); - }) - .catch(() => { - handleUploadFailed(file.name); - }); - } + setFilesToAddToModal(files); + }; + + const handleFilesAddedToModal = () => { + setFilesToAddToModal([]); }; const handleOverwriteCancel = () => { @@ -674,6 +664,8 @@ export const NotebookView = ({ onUploadStarted={handleUploadStarted} onUploadFailed={handleUploadFailed} onDuplicatesFound={handleDuplicatesFound} + filesToAdd={filesToAddToModal} + onFilesAdded={handleFilesAddedToModal} /> Date: Tue, 28 Apr 2026 12:23:28 +0530 Subject: [PATCH 4/9] updating changeset Signed-off-by: its-mitesh-kumar --- workspaces/lightspeed/.changeset/overwrite-flow-fix.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 workspaces/lightspeed/.changeset/overwrite-flow-fix.md diff --git a/workspaces/lightspeed/.changeset/overwrite-flow-fix.md b/workspaces/lightspeed/.changeset/overwrite-flow-fix.md new file mode 100644 index 00000000000..26f10eec2e6 --- /dev/null +++ b/workspaces/lightspeed/.changeset/overwrite-flow-fix.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-lightspeed': patch +--- + +Fixed overwrite flow to add duplicate files to the Add Document modal instead of uploading immediately. Reduced notebook delete toast timeout to 2 seconds. From fe67a6f0046bdb1833cc0589a46f59906b0fb301 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Tue, 28 Apr 2026 13:12:40 +0530 Subject: [PATCH 5/9] showing model from app-config for displaying response Signed-off-by: its-mitesh-kumar --- .../plugins/lightspeed/src/components/LightSpeedChat.tsx | 1 + .../lightspeed/src/components/notebooks/NotebookView.tsx | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx index bf547a70f44..02ed963854f 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/LightSpeedChat.tsx @@ -1681,6 +1681,7 @@ export const LightspeedChat = ({ avatar={avatar} profileLoading={profileLoading} topicRestrictionEnabled={topicRestrictionEnabled} + selectedModel={selectedModel} onClose={handleCloseNotebook} /> )} diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx index f179055ee21..7f398dd030e 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx @@ -199,6 +199,7 @@ type NotebookViewProps = { avatar?: string; profileLoading: boolean; topicRestrictionEnabled: boolean; + selectedModel: string; onClose: () => void; }; @@ -212,6 +213,7 @@ export const NotebookView = ({ avatar, profileLoading, topicRestrictionEnabled, + selectedModel, onClose, }: NotebookViewProps) => { const classes = useStyles(); @@ -279,7 +281,7 @@ export const NotebookView = ({ useConversationMessages( conversationId, userName, - '', + selectedModel, '', avatar, onComplete, From cb187d288d5cc23a90bd43485e55c9c88e5b5bb2 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Tue, 28 Apr 2026 16:07:42 +0530 Subject: [PATCH 6/9] disabling upload when 10 files are there Signed-off-by: its-mitesh-kumar --- .../components/notebooks/DocumentSidebar.tsx | 36 ++++++++++++++----- .../src/components/notebooks/NotebookView.tsx | 35 ++++++++++++------ .../notebooks/SidebarCollapseIcon.tsx | 11 ++++-- .../plugins/lightspeed/src/translations/de.ts | 8 +++++ .../plugins/lightspeed/src/translations/es.ts | 8 +++++ .../plugins/lightspeed/src/translations/fr.ts | 8 +++++ .../plugins/lightspeed/src/translations/it.ts | 8 +++++ .../plugins/lightspeed/src/translations/ja.ts | 8 +++++ .../lightspeed/src/translations/ref.ts | 2 ++ 9 files changed, 104 insertions(+), 20 deletions(-) diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/DocumentSidebar.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/DocumentSidebar.tsx index 4f641a66858..7b280716c3a 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/DocumentSidebar.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/DocumentSidebar.tsx @@ -28,6 +28,7 @@ import { } from '@patternfly/react-core'; import { EllipsisVIcon, PlusCircleIcon } from '@patternfly/react-icons'; +import { NOTEBOOK_MAX_FILES } from '../../const'; import { useTranslation } from '../../hooks/useTranslation'; import { SessionDocument } from '../../types'; import { FileTypeIcon } from './FileTypeIcon'; @@ -157,6 +158,7 @@ export const DocumentSidebar = ({ name => !uploadedNames.has(name), ); const totalCount = documents.length + activePending.length; + const isAddDisabled = totalCount >= NOTEBOOK_MAX_FILES; return (
@@ -180,14 +182,32 @@ export const DocumentSidebar = ({ count: totalCount, } as any)} - + {isAddDisabled ? ( + + + + + + ) : ( + + )}
{(documents.length > 0 || activePending.length > 0) && ( diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx index 7f398dd030e..2b87a809985 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx @@ -44,7 +44,11 @@ import { TimesIcon } from '@patternfly/react-icons'; import { useQueryClient } from '@tanstack/react-query'; import { notebooksApiRef } from '../../api/notebooksApi'; -import { TEMP_CONVERSATION_ID, UNTITLED_NOTEBOOK_NAME } from '../../const'; +import { + NOTEBOOK_MAX_FILES, + TEMP_CONVERSATION_ID, + UNTITLED_NOTEBOOK_NAME, +} from '../../const'; import { useCreateNotebookMessage } from '../../hooks/notebooks/useCreateNotebookMessage'; import { useDocumentStatusPolling, @@ -454,6 +458,8 @@ export const NotebookView = ({ }; const hasDocuments = documents.length > 0 || uploadingFileNames.length > 0; + const totalDocumentCount = documents.length + uploadingFileNames.length; + const isAddDisabled = totalDocumentCount >= NOTEBOOK_MAX_FILES; const panelContent = ( - + + + )} diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/SidebarCollapseIcon.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/SidebarCollapseIcon.tsx index 117d22e556f..b3937cfe562 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/SidebarCollapseIcon.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/SidebarCollapseIcon.tsx @@ -44,7 +44,14 @@ export const SidebarExpandIcon = ({ className }: IconProps) => ( ); -export const AddCircleFilledIcon = ({ className }: IconProps) => ( +type AddCircleFilledIconProps = IconProps & { + disabled?: boolean; +}; + +export const AddCircleFilledIcon = ({ + className, + disabled, +}: AddCircleFilledIconProps) => ( ( > ); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/de.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/de.ts index 05018362c92..18723e71f39 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/de.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/de.ts @@ -64,10 +64,14 @@ const lightspeedTranslationDe = createTranslationMessages({ 'Laden Sie eine Ressource hoch, um zu beginnen', 'notebook.view.upload.action': 'Ressource hochladen', 'notebook.view.input.placeholder': 'Fragen Sie zu Ihren Dokumenten...', + 'notebook.view.input.disabledTooltip': + 'Wählen Sie mindestens eine geladene Ressource aus, um den Chat zu starten', 'notebook.view.sidebar.collapse': 'Seitenleiste einklappen', 'notebook.view.sidebar.expand': 'Seitenleiste ausklappen', 'notebook.view.sidebar.resize': 'Größe der Seitenleiste ändern', 'notebook.view.documents.uploading': 'Dokument wird hochgeladen', + 'notebook.view.documents.maxReached': + 'Maximal 10 Dokumente sind erlaubt. Löschen Sie ein Dokument, um ein neues hochzuladen.', 'notebook.upload.success': '{{fileName}} erfolgreich hochgeladen.', 'notebook.upload.failed': 'Hochladen von {{fileName}} fehlgeschlagen.', @@ -78,6 +82,10 @@ const lightspeedTranslationDe = createTranslationMessages({ 'notebook.upload.modal.separator': 'oder', 'notebook.upload.modal.infoText': 'Akzeptierte Dateitypen: .md, .txt, .pdf, .json, .yaml, .log', + 'notebook.upload.modal.selectedFiles': + '{{count}} von {{max}} Dateien ausgewählt', + 'notebook.upload.modal.addButton': 'Hinzufügen ({{count}})', + 'notebook.upload.modal.removeFile': '{{fileName}} entfernen', 'notebook.upload.error.unsupportedType': 'Upload-Fehler: Nicht unterstützte Dateitypen gefunden. Bitte laden Sie nur unterstützte Dateitypen hoch.', 'notebook.upload.error.fileTooLarge': diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts index 0a5f85bd796..2de64c3c598 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/es.ts @@ -63,10 +63,14 @@ const lightspeedTranslationEs = createTranslationMessages({ 'notebook.view.upload.heading': 'Sube un recurso para empezar', 'notebook.view.upload.action': 'Subir un recurso', 'notebook.view.input.placeholder': 'Pregunta sobre tus documentos...', + 'notebook.view.input.disabledTooltip': + 'Selecciona al menos un recurso cargado para comenzar a chatear', 'notebook.view.sidebar.collapse': 'Contraer barra lateral', 'notebook.view.sidebar.expand': 'Expandir barra lateral', 'notebook.view.sidebar.resize': 'Redimensionar barra lateral', 'notebook.view.documents.uploading': 'Subiendo documento', + 'notebook.view.documents.maxReached': + 'Se permiten un máximo de 10 documentos. Elimina un documento para subir uno nuevo.', 'notebook.upload.success': '{{fileName}} subido correctamente.', 'notebook.upload.failed': 'Error al subir {{fileName}}.', @@ -78,6 +82,10 @@ const lightspeedTranslationEs = createTranslationMessages({ 'notebook.upload.modal.separator': 'o', 'notebook.upload.modal.infoText': 'Tipos de archivo aceptados: .md, .txt, .pdf, .json, .yaml, .log', + 'notebook.upload.modal.selectedFiles': + '{{count}} de {{max}} archivos seleccionados', + 'notebook.upload.modal.addButton': 'Agregar ({{count}})', + 'notebook.upload.modal.removeFile': 'Eliminar {{fileName}}', 'notebook.upload.error.unsupportedType': 'Error de carga: se encontraron tipos de archivo no compatibles. Suba solo tipos de archivo compatibles.', 'notebook.upload.error.fileTooLarge': diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/fr.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/fr.ts index f10cf7a18f1..c14909809e7 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/fr.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/fr.ts @@ -64,10 +64,14 @@ const lightspeedTranslationFr = createTranslationMessages({ 'notebook.view.upload.action': 'Charger une ressource', 'notebook.view.input.placeholder': 'Posez des questions sur vos documents...', + 'notebook.view.input.disabledTooltip': + 'Sélectionnez au moins une ressource chargée pour commencer à discuter', 'notebook.view.sidebar.collapse': 'Réduire la barre latérale', 'notebook.view.sidebar.expand': 'Développer la barre latérale', 'notebook.view.sidebar.resize': 'Redimensionner la barre latérale', 'notebook.view.documents.uploading': 'Chargement du document', + 'notebook.view.documents.maxReached': + 'Maximum 10 documents autorisés. Supprimez un document pour en charger un nouveau.', 'notebook.upload.success': '{{fileName}} chargé avec succès.', 'notebook.upload.failed': 'Échec du chargement de {{fileName}}.', @@ -78,6 +82,10 @@ const lightspeedTranslationFr = createTranslationMessages({ 'notebook.upload.modal.separator': 'ou', 'notebook.upload.modal.infoText': 'Types de fichiers acceptés : .md, .txt, .pdf, .json, .yaml, .log', + 'notebook.upload.modal.selectedFiles': + '{{count}} sur {{max}} fichiers sélectionnés', + 'notebook.upload.modal.addButton': 'Ajouter ({{count}})', + 'notebook.upload.modal.removeFile': 'Supprimer {{fileName}}', 'notebook.upload.error.unsupportedType': 'Erreur de chargement : type(s) de fichier non pris en charge. Veuillez charger uniquement des types de fichiers pris en charge.', 'notebook.upload.error.fileTooLarge': diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/it.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/it.ts index f8390d281c0..b6f74ed4ab6 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/it.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/it.ts @@ -65,10 +65,14 @@ const lightspeedTranslationIt = createTranslationMessages({ 'notebook.view.upload.action': 'Carica una risorsa', 'notebook.view.input.placeholder': 'Chiedi informazioni sui tuoi documenti...', + 'notebook.view.input.disabledTooltip': + 'Seleziona almeno una risorsa caricata per iniziare a chattare', 'notebook.view.sidebar.collapse': 'Comprimi barra laterale', 'notebook.view.sidebar.expand': 'Espandi barra laterale', 'notebook.view.sidebar.resize': 'Ridimensiona barra laterale', 'notebook.view.documents.uploading': 'Caricamento documento', + 'notebook.view.documents.maxReached': + 'Sono consentiti al massimo 10 documenti. Elimina un documento per caricarne uno nuovo.', 'notebook.upload.success': '{{fileName}} caricato con successo.', 'notebook.upload.failed': 'Caricamento di {{fileName}} non riuscito.', @@ -79,6 +83,10 @@ const lightspeedTranslationIt = createTranslationMessages({ 'notebook.upload.modal.separator': 'o', 'notebook.upload.modal.infoText': 'Tipi di file accettati: .md, .txt, .pdf, .json, .yaml, .log', + 'notebook.upload.modal.selectedFiles': + '{{count}} di {{max}} file selezionati', + 'notebook.upload.modal.addButton': 'Aggiungi ({{count}})', + 'notebook.upload.modal.removeFile': 'Rimuovi {{fileName}}', 'notebook.upload.error.unsupportedType': 'Errore di caricamento: trovati tipi di file non supportati. Caricare solo tipi di file supportati.', 'notebook.upload.error.fileTooLarge': diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/ja.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/ja.ts index c265c627a7b..700dde7efb9 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/ja.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/ja.ts @@ -64,10 +64,14 @@ const lightspeedTranslationJa = createTranslationMessages({ 'リソースをアップロードして開始してください', 'notebook.view.upload.action': 'リソースをアップロード', 'notebook.view.input.placeholder': 'ドキュメントについて質問する...', + 'notebook.view.input.disabledTooltip': + 'チャットを開始するには、少なくとも1つのロード済みリソースを選択してください', 'notebook.view.sidebar.collapse': 'サイドバーを折りたたむ', 'notebook.view.sidebar.expand': 'サイドバーを展開する', 'notebook.view.sidebar.resize': 'サイドバーのサイズを変更する', 'notebook.view.documents.uploading': 'ドキュメントをアップロード中', + 'notebook.view.documents.maxReached': + '最大10個のドキュメントが許可されています。新しいドキュメントをアップロードするには、ドキュメントを削除してください。', 'notebook.upload.success': '{{fileName}} のアップロードに成功しました。', 'notebook.upload.failed': '{{fileName}} のアップロードに失敗しました。', @@ -78,6 +82,10 @@ const lightspeedTranslationJa = createTranslationMessages({ 'notebook.upload.modal.separator': 'または', 'notebook.upload.modal.infoText': '対応ファイル形式: .md, .txt, .pdf, .json, .yaml, .log', + 'notebook.upload.modal.selectedFiles': + '{{max}} 件中 {{count}} 件のファイルを選択', + 'notebook.upload.modal.addButton': '追加 ({{count}})', + 'notebook.upload.modal.removeFile': '{{fileName}} を削除', 'notebook.upload.error.unsupportedType': 'アップロードエラー: サポートされていないファイル形式が見つかりました。サポートされているファイル形式のみをアップロードしてください。', 'notebook.upload.error.fileTooLarge': diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts index 216af6177f0..a69b280c6ae 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts @@ -67,6 +67,8 @@ export const lightspeedMessages = { 'notebook.view.sidebar.expand': 'Expand sidebar', 'notebook.view.sidebar.resize': 'Resize sidebar', 'notebook.view.documents.uploading': 'Uploading document', + 'notebook.view.documents.maxReached': + 'Maximum 10 documents are allowed. Delete a document to upload a new document.', 'notebook.upload.success': '{{fileName}} Successfully Uploaded.', 'notebook.upload.failed': '{{fileName}} Upload Failed.', From 9442245a858a159c3f9ef7eb4d66fa654cec0498 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Tue, 28 Apr 2026 16:08:48 +0530 Subject: [PATCH 7/9] updating changeset Signed-off-by: its-mitesh-kumar --- workspaces/lightspeed/.changeset/notebook-upload-modal-ux.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspaces/lightspeed/.changeset/notebook-upload-modal-ux.md b/workspaces/lightspeed/.changeset/notebook-upload-modal-ux.md index f663adee8c7..7b0c68fa28c 100644 --- a/workspaces/lightspeed/.changeset/notebook-upload-modal-ux.md +++ b/workspaces/lightspeed/.changeset/notebook-upload-modal-ux.md @@ -1,5 +1,5 @@ --- -'@red-hat-developer-hub/backstage-plugin-lightspeed': minor +'@red-hat-developer-hub/backstage-plugin-lightspeed': patch --- Improved notebook upload modal and MessageBar UX. From 6bb33a9e3d2de10856baea41c78dc52d5eaf607a Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Tue, 28 Apr 2026 16:45:11 +0530 Subject: [PATCH 8/9] hadnling duplicates file Signed-off-by: its-mitesh-kumar --- .../src/components/notebooks/NotebookView.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx index 2b87a809985..2f9dd1ca28d 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx @@ -341,7 +341,12 @@ export const NotebookView = ({ const handleCloseUploadModal = () => setIsUploadModalOpen(false); const handleFilesUploading = (files: File[]) => { - setUploadingFileNames(prev => [...prev, ...files.map(f => f.name)]); + setUploadingFileNames(prev => { + const newNames = files + .map(f => f.name) + .filter(name => !prev.includes(name)); + return [...prev, ...newNames]; + }); }; const handleUploadStarted = (info: { @@ -415,9 +420,8 @@ export const NotebookView = ({ for (const result of completedOrFailed) { processedIds.current.add(result.documentId); idsToRemove.add(result.documentId); - if (result.status !== 'completed') { - namesToRemove.add(result.fileName); - } else { + namesToRemove.add(result.fileName); + if (result.status === 'completed') { newCompletedNames.add(result.fileName); } From 6891bad5a9ddf9972fd3459649c10d3f37ed5fe7 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Tue, 28 Apr 2026 19:22:28 +0530 Subject: [PATCH 9/9] updating api report Signed-off-by: its-mitesh-kumar --- workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md b/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md index 4bb031f10d4..2202fec3e40 100644 --- a/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md +++ b/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md @@ -235,6 +235,7 @@ export const lightspeedTranslationRef: TranslationRef< readonly 'notebook.view.sidebar.expand': string; readonly 'notebook.view.sidebar.resize': string; readonly 'notebook.view.documents.uploading': string; + readonly 'notebook.view.documents.maxReached': string; readonly 'notebook.upload.success': string; readonly 'notebook.upload.failed': string; readonly 'notebook.upload.modal.title': string;