diff --git a/workspaces/intelligent-assistant/.changeset/notebook-overlay-docked-modes.md b/workspaces/intelligent-assistant/.changeset/notebook-overlay-docked-modes.md new file mode 100644 index 00000000000..d8366e1ec0c --- /dev/null +++ b/workspaces/intelligent-assistant/.changeset/notebook-overlay-docked-modes.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-intelligent-assistant': minor +--- + +implement docked and overlay display modes for Notebook diff --git a/workspaces/intelligent-assistant/e2e-tests/lightspeed.notebooks-compact.test.ts b/workspaces/intelligent-assistant/e2e-tests/lightspeed.notebooks-compact.test.ts new file mode 100644 index 00000000000..94639784784 --- /dev/null +++ b/workspaces/intelligent-assistant/e2e-tests/lightspeed.notebooks-compact.test.ts @@ -0,0 +1,280 @@ +/* + * 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 { test, expect, type Page } from '@playwright/test'; + +import { NotebookSurfacePage } from './pages/NotebookSurfacePage'; +import type { LightspeedMessages } from './utils/translations'; +import { bootstrapLightspeedE2ePage } from './utils/lightspeedE2eSetup'; +import { + openChatbot, + selectDisplayMode, + type DisplayMode, +} from './pages/LightspeedPage'; +import { + localeNotebookUpload1Path, + NOTEBOOK_SESSION_MAX_DOCUMENTS, +} from './utils/notebooks'; + +async function switchToCompactNotebooks( + page: Page, + t: LightspeedMessages, + mode: DisplayMode, +) { + await page.goto('/'); + await openChatbot(page, t); + await selectDisplayMode(page, t, mode); + await page.getByRole('tab', { name: t['tabs.notebooks'] }).click(); +} + +for (const mode of ['Overlay', 'Dock to window'] as const) { + test.describe(`Notebooks in ${mode} mode`, () => { + test.describe.configure({ mode: 'serial' }); + + let sharedPage: Page; + let translations: LightspeedMessages; + let notebooks: NotebookSurfacePage; + + test.beforeAll(async ({ browser }) => { + const boot = await bootstrapLightspeedE2ePage(browser); + sharedPage = boot.page; + translations = boot.translations; + notebooks = new NotebookSurfacePage(sharedPage, translations); + }); + + test('tabs are visible and notebooks tab selectable', async () => { + await switchToCompactNotebooks(sharedPage, translations, mode); + + await expect( + sharedPage.getByRole('tab', { name: translations['tabs.chat'] }), + ).toBeVisible(); + const notebooksTab = sharedPage.getByRole('tab', { + name: translations['tabs.notebooks'], + }); + await expect(notebooksTab).toBeVisible(); + await expect(notebooksTab).toHaveAttribute('aria-selected', 'true'); + }); + + test('empty notebook list shows create action', async () => { + await notebooks.expectNotebookListHeaderControlsVisible(); + }); + + test('create notebook and verify compact editor layout', async () => { + await notebooks.clickCreateNotebookFromEmptyList(); + + await expect(notebooks.uploadResourceHeading()).toBeVisible(); + await expect(notebooks.uploadResourceActionButton()).toBeVisible(); + }); + + test('header actions visible in compact mode: close, add, sidebar toggle', async () => { + const header = sharedPage.locator('.pf-chatbot__header'); + + await expect( + header.getByRole('button', { + name: translations['notebook.view.close'], + }), + ).toBeVisible(); + await expect( + header.getByRole('button', { + name: translations['notebook.view.documents.add'], + }), + ).toBeVisible(); + + const collapseLabel = translations['notebook.view.sidebar.collapse']; + const expandLabel = translations['notebook.view.sidebar.expand']; + const sidebarToggle = header.getByRole('button', { + name: new RegExp(`${collapseLabel}|${expandLabel}`), + }); + await expect(sidebarToggle).toBeVisible(); + }); + + test('NotebookView topBar close button hidden in compact mode', async () => { + const closeButtons = sharedPage.getByRole('button', { + name: translations['notebook.view.close'], + }); + await expect(closeButtons).toHaveCount(1); + }); + + test('upload modal opens and renders within panel', async () => { + const header = sharedPage.locator('.pf-chatbot__header'); + const addButton = header.getByRole('button', { + name: translations['notebook.view.documents.add'], + }); + await addButton.click(); + + // In compact mode, disablePortal renders the MUI Dialog inline. The + // ChatbotModal already has role="dialog", so scope to the MUI one. + const dialog = sharedPage.locator( + '[role="dialog"][aria-labelledby="add-document-modal-title"]', + ); + await expect(dialog).toBeVisible({ timeout: 10_000 }); + await expect(dialog.locator('#add-document-modal-title')).toBeVisible(); + await expect( + dialog.locator( + `text=${translations['notebook.upload.modal.dragDropTitle']}`, + ), + ).toBeVisible(); + + await dialog + .locator('button', { hasText: translations['modal.cancel'] }) + .click(); + }); + + test('sidebar toggle mirrors icon direction', async () => { + const header = sharedPage.locator('.pf-chatbot__header'); + const collapseLabel = translations['notebook.view.sidebar.collapse']; + const expandLabel = translations['notebook.view.sidebar.expand']; + + const toggle = header.getByRole('button', { + name: new RegExp(`${collapseLabel}|${expandLabel}`), + }); + await expect(toggle).toBeVisible(); + + const initialLabel = await toggle.getAttribute('aria-label'); + + await toggle.click(); + await sharedPage.waitForTimeout(300); + + const newLabel = await toggle.getAttribute('aria-label'); + expect(newLabel).not.toBe(initialLabel); + + const expectedLabel = + initialLabel === collapseLabel ? expandLabel : collapseLabel; + expect(newLabel).toBe(expectedLabel); + + await toggle.click(); + await sharedPage.waitForTimeout(300); + const restoredLabel = await toggle.getAttribute('aria-label'); + expect(restoredLabel).toBe(initialLabel); + }); + + test('file picker works in compact upload modal', async ({}, testInfo) => { + const { absolutePath } = localeNotebookUpload1Path(testInfo.project.name); + + const header = sharedPage.locator('.pf-chatbot__header'); + await header + .getByRole('button', { + name: translations['notebook.view.documents.add'], + }) + .click(); + + const dialog = sharedPage.locator( + '[role="dialog"][aria-labelledby="add-document-modal-title"]', + ); + await expect(dialog).toBeVisible({ timeout: 10_000 }); + + const browseButton = dialog.locator('button', { + hasText: translations['notebook.upload.modal.browseButton'], + }); + const [fileChooser] = await Promise.all([ + sharedPage.waitForEvent('filechooser'), + browseButton.click(), + ]); + await fileChooser.setFiles([absolutePath]); + + const stagedCaption = translations['notebook.upload.modal.selectedFiles'] + .replace('{{count}}', '1') + .replace('{{max}}', String(NOTEBOOK_SESSION_MAX_DOCUMENTS)); + await expect(dialog.locator(`text=${stagedCaption}`)).toBeVisible({ + timeout: 5_000, + }); + + await dialog + .locator('button', { hasText: translations['modal.cancel'] }) + .click(); + }); + + test('switch tabs preserves notebook state', async () => { + await sharedPage + .getByRole('tab', { name: translations['tabs.chat'] }) + .click(); + await expect( + sharedPage.getByRole('tab', { name: translations['tabs.chat'] }), + ).toHaveAttribute('aria-selected', 'true'); + + await sharedPage + .getByRole('tab', { name: translations['tabs.notebooks'] }) + .click(); + await expect( + sharedPage.getByRole('tab', { + name: translations['tabs.notebooks'], + }), + ).toHaveAttribute('aria-selected', 'true'); + + // Notebook editor still shows (not reverted to list view) + await expect(notebooks.uploadResourceHeading()).toBeVisible(); + }); + + test('close notebook via header action', async () => { + const header = sharedPage.locator('.pf-chatbot__header'); + await header + .getByRole('button', { + name: translations['notebook.view.close'], + }) + .click(); + + await expect(notebooks.myNotebooksHeading()).toBeVisible(); + await expect(notebooks.newestUntitledNotebookCard()).toBeVisible(); + }); + + test('display mode switch preserves notebooks tab', async () => { + const otherMode: DisplayMode = + mode === 'Overlay' ? 'Dock to window' : 'Overlay'; + + await selectDisplayMode(sharedPage, translations, otherMode); + + await expect( + sharedPage.getByRole('tab', { + name: translations['tabs.notebooks'], + }), + ).toHaveAttribute('aria-selected', 'true'); + + await expect(notebooks.myNotebooksHeading()).toBeVisible(); + }); + + test('switch to fullscreen preserves notebooks tab', async () => { + await selectDisplayMode(sharedPage, translations, 'Fullscreen'); + + await expect( + sharedPage.getByRole('tab', { + name: translations['tabs.notebooks'], + }), + ).toBeVisible(); + }); + + test('cleanup: delete created notebook', async () => { + await selectDisplayMode(sharedPage, translations, mode); + + await expect( + sharedPage.getByRole('tab', { + name: translations['tabs.notebooks'], + }), + ).toBeVisible(); + await sharedPage + .getByRole('tab', { name: translations['tabs.notebooks'] }) + .click(); + + const card = notebooks.newestUntitledNotebookCard(); + if ((await card.count()) > 0) { + await notebooks.notebookCardOverflowMenuButton(card).click(); + await notebooks.deleteNotebookOverflowMenuItem().click(); + const confirmDelete = + notebooks.notebookDeleteConfirmationDialog('Untitled Notebook'); + await confirmDelete.confirmDeletion(); + } + }); + }); +} diff --git a/workspaces/intelligent-assistant/e2e-tests/pages/LightspeedPage.ts b/workspaces/intelligent-assistant/e2e-tests/pages/LightspeedPage.ts index 5fe50e5d3ef..4f9920d9642 100644 --- a/workspaces/intelligent-assistant/e2e-tests/pages/LightspeedPage.ts +++ b/workspaces/intelligent-assistant/e2e-tests/pages/LightspeedPage.ts @@ -39,7 +39,10 @@ export async function selectDisplayMode( t: LightspeedMessages, mode: DisplayMode, ) { - await page.getByRole('button', { name: t['aria.options.label'] }).click(); + await page + .locator('.pf-chatbot__header') + .getByRole('button', { name: t['aria.options.label'] }) + .click(); const modeMap: Record = { Overlay: t['settings.displayMode.overlay'], 'Dock to window': t['settings.displayMode.docked'], diff --git a/workspaces/intelligent-assistant/e2e-tests/pages/NotebookDeleteDialogPage.ts b/workspaces/intelligent-assistant/e2e-tests/pages/NotebookDeleteDialogPage.ts index bf05fdbfd70..9f2175b702d 100644 --- a/workspaces/intelligent-assistant/e2e-tests/pages/NotebookDeleteDialogPage.ts +++ b/workspaces/intelligent-assistant/e2e-tests/pages/NotebookDeleteDialogPage.ts @@ -26,11 +26,11 @@ export class NotebookDeleteDialogPage { private readonly notebookDisplayName: string, ) {} - /** Dialog anchored by visible notebook title (matches MUI `DeleteNotebookModal` content). */ + /** Dialog anchored by accessible name derived from aria-labelledby (matches MUI `DeleteNotebookModal`). */ dialog(): Locator { - return this.page - .getByRole('dialog') - .filter({ hasText: this.notebookDisplayName }); + return this.page.getByRole('dialog', { + name: new RegExp(this.notebookDisplayName), + }); } deleteNotebookConfirmButton(): Locator { @@ -51,6 +51,11 @@ export class NotebookDeleteDialogPage { } async confirmDeletion(): Promise { - await this.deleteNotebookConfirmButton().click(); + const deleteBtn = this.page.locator( + '#delete-notebook-modal-body ~ div button', + { hasText: this.t['notebooks.delete.action'] }, + ); + await deleteBtn.waitFor({ state: 'visible', timeout: 30_000 }); + await deleteBtn.click({ force: true }); } } diff --git a/workspaces/intelligent-assistant/e2e-tests/utils/lightspeedE2eSetup.ts b/workspaces/intelligent-assistant/e2e-tests/utils/lightspeedE2eSetup.ts index dbe3c07f1aa..c64e10d4ae3 100644 --- a/workspaces/intelligent-assistant/e2e-tests/utils/lightspeedE2eSetup.ts +++ b/workspaces/intelligent-assistant/e2e-tests/utils/lightspeedE2eSetup.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +/// import type { Browser, Page } from '@playwright/test'; import { models, conversations, mockedShields } from '../fixtures/responses'; import { openLightspeed, switchToLocale } from './testHelper'; @@ -39,17 +40,28 @@ export type LightspeedE2eBootstrap = { }; async function loginAsGuest(page: Page) { - const enter = page.getByRole('button', { name: 'Enter' }); - await enter.click(); - await page.waitForTimeout(2000); + const maxAttempts = 3; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + const enter = page.getByRole('button', { name: 'Enter' }); + await enter.click(); + await page.waitForTimeout(2000); - if (process.env.APP_MODE !== 'nfs') { - await page - .getByRole('heading', { name: 'Red Hat Catalog' }) - .waitFor({ state: 'visible', timeout: 5_000 }); + if (process.env.APP_MODE !== 'nfs') { + try { + await page + .getByRole('heading', { name: 'Red Hat Catalog' }) + .waitFor({ state: 'visible', timeout: 10_000 }); + return; + } catch { + if (attempt === maxAttempts) throw new Error('loginAsGuest failed'); + await page.reload(); + await page.waitForTimeout(2000); + } + } else { + return; + } } } - /** * One logged-in Lightspeed session with the same dev-mode mocks as the legacy * monolithic suite. Each Playwright test file should call this from `beforeAll`. diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightSpeedChat.tsx b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightSpeedChat.tsx index c275ca126de..1f1825e3820 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightSpeedChat.tsx +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightSpeedChat.tsx @@ -45,7 +45,6 @@ import { ChatbotFooter, ChatbotHeader, ChatbotHeaderMain, - ChatbotHeaderMenu, ChatbotHeaderTitle, FileDropZone, MessageBar, @@ -54,14 +53,11 @@ import { } from '@patternfly/chatbot'; import ChatbotConversationHistoryNav from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav'; import { - Alert, - AlertActionCloseButton, - AlertGroup, - AlertVariant, DropdownItem, Label, MenuToggle, MenuToggleElement, + Button as PfButton, Select, SelectList, SelectOption, @@ -127,10 +123,16 @@ import { LightspeedChatBoxHeader } from './LightspeedChatBoxHeader'; import { McpServersSettings } from './McpServersSettings'; import { MessageBarModelSelector } from './MessageBarModelSelector'; import { DeleteNotebookModal } from './notebooks/DeleteNotebookModal'; +import { NotebookHeaderActions } from './notebooks/NotebookHeaderActions'; import { NotebooksTab } from './notebooks/NotebooksTab'; import { NotebookView } from './notebooks/NotebookView'; +import { + SidebarCollapseIcon, + SidebarExpandIcon, +} from './notebooks/SidebarCollapseIcon'; import PermissionRequiredState from './PermissionRequiredState'; import { RenameConversationModal } from './RenameConversationModal'; +import { ToastAlertGroup } from './ToastAlertGroup'; 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`; @@ -146,8 +148,6 @@ const ConditionalWrapper = ({ const useStyles = makeStyles(theme => ({ body: { - // remove default margin and padding from common elements - // lists excluded for proper formatting '& h1, & h2, & h3, & h4, & h5, & h6, & p, & li': { margin: 0, padding: 0, @@ -163,6 +163,16 @@ const useStyles = makeStyles(theme => ({ overflow: 'hidden', }, }, + bodyCompact: { + height: '100% !important', + minHeight: '0 !important', + overflow: 'hidden', + '& .pf-chatbot-container': { + minHeight: '0 !important', + display: 'flex', + flexDirection: 'column', + }, + }, header: { padding: `${theme.spacing(3)}px ${theme.spacing(3)}px 0 ${theme.spacing( 3, @@ -184,13 +194,35 @@ const useStyles = makeStyles(theme => ({ backgroundColor: 'var(--pf-t--global--background--color--floating--default) !important', }, - headerMenu: { - // align hamburger icon with title - '& .pf-v6-c-button': { - display: 'flex', - alignItems: 'center', + chatHeaderActions: { + display: 'flex', + alignItems: 'center', + gap: theme.spacing(0.5), + }, + compactDrawerPanel: { + '&.pf-v6-c-drawer__panel': { + width: '100%', + minWidth: '100%', + maxWidth: '100%', + flexBasis: '100%', }, }, + headerNewChatButton: { + '&.pf-v6-c-button': { + color: 'var(--pf-t--global--color--brand--default)', + '&:hover': { + color: 'var(--pf-t--global--color--brand--hover)', + }, + '&:disabled, &.pf-m-disabled': { + color: 'var(--pf-t--global--text--color--disabled)', + }, + }, + }, + notebookHeaderActions: { + display: 'flex', + alignItems: 'center', + gap: theme.spacing(0.5), + }, headerLogo: { width: 48, height: 48, @@ -221,10 +253,14 @@ const useStyles = makeStyles(theme => ({ display: 'flex', alignItems: 'center', justifyContent: 'space-between', + flexWrap: 'wrap', + gap: theme.spacing(1), marginBottom: theme.spacing(4), }, notebooksHeading: { marginBottom: 0, + whiteSpace: 'nowrap', + fontSize: '1.25rem', }, notebooksHeadingEmpty: { '&&': { @@ -280,6 +316,14 @@ const useStyles = makeStyles(theme => ({ gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', }, }, + notebooksGridCompact: { + display: 'grid', + gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', + gap: theme.spacing(2), + width: '100%', + maxWidth: '100%', + paddingBottom: theme.spacing(3), + }, notebookCard: { borderRadius: theme.spacing(1.5), display: 'flex', @@ -431,18 +475,6 @@ const useStyles = makeStyles(theme => ({ backgroundColor: 'var(--pf-t--global--background--color--floating--default) !important', }, - toastAlertGroup: { - '--pf-v6-c-alert-group--m-toast--InsetInlineEnd': `${theme.spacing(2.5)}px`, - '--pf-v6-c-alert-group--m-toast--InsetBlockStart': `${theme.spacing(2.5)}px`, - '--pf-v6-c-alert-group--m-toast--MaxWidth': '350px', - '--pf-v6-c-alert-group--m-toast--ZIndex': '9999', - }, - toastAlert: { - maxWidth: '350px', - '& .pf-v6-c-alert__title': { - margin: 0, - }, - }, // When present, pushes welcome content to bottom (zoom out). Scroll up to see important box (zoom in). chatbotContentSpacer: { flex: 1, @@ -673,6 +705,8 @@ export const LightspeedChat = ({ consumePendingOverlayThreadHandoff, shellViewTab, setShellViewTab, + activeNotebookId, + setActiveNotebookId, } = useLightspeedDrawerContext(); const isFullscreenMode = displayMode === ChatbotDisplayMode.embedded; const location = useLocation(); @@ -683,9 +717,7 @@ export const LightspeedChat = ({ const [filterValue, setFilterValue] = useState(''); const [announcement, setAnnouncement] = useState(''); const [activeTab, setActiveTab] = useState(() => { - if (!isFullscreenMode) { - return 0; - } + // Route matches only occur in fullscreen mode; compact modes don't navigate to /notebooks URLs. if (notebooksRouteMatch || notebookViewRouteMatch) { return 1; } @@ -712,30 +744,25 @@ export const LightspeedChat = ({ null, ); const [deleteNotebookId, setDeleteNotebookId] = useState(null); - const [activeNotebook, setActiveNotebook] = useState( - null, - ); - const { - data: routeNotebook, - isLoading: routeNotebookLoading, - isError: routeNotebookError, - } = useNotebookSession(routeNotebookId); + const effectiveNotebookId = + routeNotebookId ?? (!isFullscreenMode ? activeNotebookId : undefined); + const { data: activeNotebook, isError: activeNotebookError } = + useNotebookSession(effectiveNotebookId); useEffect(() => { - if (routeNotebookId && routeNotebook && !routeNotebookLoading) { - setActiveNotebook(routeNotebook); - } else if (routeNotebookId && routeNotebookError) { - navigate(`${LIGHTSPEED_PATH}/notebooks`, { replace: true }); - } else if (!routeNotebookId && notebooksRouteMatch) { - setActiveNotebook(null); + if (effectiveNotebookId && activeNotebookError) { + if (isFullscreenMode) { + navigate(`${LIGHTSPEED_PATH}/notebooks`, { replace: true }); + } else { + setActiveNotebookId(undefined); + } } }, [ - routeNotebookId, - routeNotebook, - routeNotebookLoading, - routeNotebookError, - notebooksRouteMatch, + effectiveNotebookId, + activeNotebookError, + isFullscreenMode, navigate, + setActiveNotebookId, ]); const [notebookAlerts, setNotebookAlerts] = useState[]>( @@ -750,6 +777,11 @@ export const LightspeedChat = ({ }); const { data: notebookDocuments = [], isFetching: isDocumentsFetching } = useNotebookDocuments(activeNotebook?.session_id); + const [notebookUploadsInProgress, setNotebookUploadsInProgress] = + useState(false); + const [notebookSidebarCollapsed, setNotebookSidebarCollapsed] = + useState(!isFullscreenMode); + const [notebookUploadModalOpen, setNotebookUploadModalOpen] = useState(false); const [conversationId, setConversationId] = useState(''); const [requestId, setRequestId] = useState(''); const [newChatCreated, setNewChatCreated] = useState(false); @@ -768,7 +800,7 @@ export const LightspeedChat = ({ const wasStoppedByUserRef = useRef(false); const { isReady, lastOpenedId, setLastOpenedId, clearLastOpenedId } = useLastOpenedConversation(user); - const showChatPanel = !isFullscreenMode || activeTab === 0; + const showChatPanel = activeTab === 0; const showNotebooksPanel = (notebooksEnabled || isOnNotebookRoute) && activeTab !== 0; const [isChatHistoryDrawerOpen, setIsChatHistoryDrawerOpen] = @@ -806,17 +838,19 @@ export const LightspeedChat = ({ const handleNotebookTabSelect = (_event: SyntheticEvent, nextTab: number) => { setActiveTab(nextTab); setShellViewTab(nextTab); - if (nextTab === 1) { - navigate(`${LIGHTSPEED_PATH}/notebooks`); - if (notebooksPermissionResolved) { - refetchNotebooks(); + if (isFullscreenMode) { + if (nextTab === 1) { + navigate(`${LIGHTSPEED_PATH}/notebooks`); + } else { + navigate( + routeConversationId + ? `${LIGHTSPEED_PATH}/conversation/${routeConversationId}` + : LIGHTSPEED_PATH, + ); } - } else { - navigate( - routeConversationId - ? `${LIGHTSPEED_PATH}/conversation/${routeConversationId}` - : LIGHTSPEED_PATH, - ); + } + if (nextTab === 1 && notebooksPermissionResolved) { + refetchNotebooks(); } }; @@ -845,16 +879,24 @@ export const LightspeedChat = ({ { name: UNTITLED_NOTEBOOK_NAME }, { onSuccess: (session: NotebookSession) => { - navigate(`${LIGHTSPEED_PATH}/notebooks/${session.session_id}`); + if (isFullscreenMode) { + navigate(`${LIGHTSPEED_PATH}/notebooks/${session.session_id}`); + } else { + setActiveNotebookId(session.session_id); + } }, }, ); - }, [createNotebookMutation, navigate]); + }, [createNotebookMutation, isFullscreenMode, navigate, setActiveNotebookId]); const handleCloseNotebook = useCallback(() => { - navigate(`${LIGHTSPEED_PATH}/notebooks`); + if (isFullscreenMode) { + navigate(`${LIGHTSPEED_PATH}/notebooks`); + } else { + setActiveNotebookId(undefined); + } refetchNotebooks(); - }, [navigate, refetchNotebooks]); + }, [isFullscreenMode, navigate, refetchNotebooks, setActiveNotebookId]); const handleRemoveNotebookAlert = (key: React.Key) => { setNotebookAlerts(prevAlerts => @@ -1871,32 +1913,10 @@ export const LightspeedChat = ({ return ( <> - {notebookAlerts.length > 0 && ( - - {notebookAlerts.map(({ key, title, variant }) => ( - handleRemoveNotebookAlert(key as React.Key)} - actionClose={ - handleRemoveNotebookAlert(key as React.Key)} - /> - } - /> - ))} - - )} + {isDeleteModalOpen && ( n.session_id === deleteNotebookId)?.name ?? '' } + isCompact={!isFullscreenMode} /> )} {showChatPanel && !isFullscreenMode && ( - + + + {isChatHistoryDrawerOpen ? ( + + ) : ( + + )} + + + {!isChatHistoryDrawerOpen && ( + + + + + + )} + + )} + {!isFullscreenMode && showNotebooksPanel && activeNotebook && ( + setNotebookUploadModalOpen(true)} + uploadsInProgress={notebookUploadsInProgress} + uploadModalOpen={notebookUploadModalOpen} + sidebarCollapsed={notebookSidebarCollapsed} + onSidebarCollapsedChange={setNotebookSidebarCollapsed} /> )} {isFullscreenMode && ( @@ -1976,8 +2042,10 @@ export const LightspeedChat = ({ onMcpSettingsClick={() => setIsMcpSettingsOpen(true)} /> - {isFullscreenMode &&
} - {isFullscreenMode && shouldShowTabs && ( + {(isFullscreenMode || shouldShowTabs) && ( +
+ )} + {shouldShowTabs && ( )} {showNotebooksPanel && !notebooksPermissionLoading && hasNotebooksAccess && !activeNotebook && ( - { - navigate(`${LIGHTSPEED_PATH}/notebooks/${notebook.session_id}`); +
+ > + { + if (isFullscreenMode) { + navigate( + `${LIGHTSPEED_PATH}/notebooks/${notebook.session_id}`, + ); + } else { + setActiveNotebookId(notebook.session_id); + } + }} + onRename={handleRenameNotebook} + onDelete={setDeleteNotebookId} + onCreateNotebook={handleCreateNotebook} + t={t} + /> +
)} {showNotebooksPanel && !notebooksPermissionLoading && diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightspeedDrawerContext.tsx b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightspeedDrawerContext.tsx index 0f54b922799..601d31bc1d0 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightspeedDrawerContext.tsx +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightspeedDrawerContext.tsx @@ -50,8 +50,8 @@ export interface LightspeedDrawerContextType { * Set the display mode (overlay, docked, or fullscreen/embedded). * When entering embedded mode, optional `embeddedNotebooks` navigates to * `/lightspeed/notebooks` (or a session URL) instead of the chat route. - * Leaving embedded for overlay or docked resets the shell tab to Chat - * (Notebooks is only available in fullscreen). + * Notebooks are available in all display modes; the shell tab and active + * notebook are preserved across mode switches. */ setDisplayMode: ( mode: ChatbotDisplayMode, @@ -105,6 +105,13 @@ export interface LightspeedDrawerContextType { */ shellViewTab: number; setShellViewTab: (tab: number) => void; + /** + * ID of the currently active notebook session, persisted across + * overlay/docked/fullscreen remounts so display-mode switches preserve + * the open notebook. + */ + activeNotebookId: string | undefined; + setActiveNotebookId: (id: string | undefined) => void; } const CONTEXT_KEY = '__lightspeed_drawer_context__' as keyof typeof globalThis; diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightspeedDrawerProvider.tsx b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightspeedDrawerProvider.tsx index 560c490f541..3e7eb5f1db2 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightspeedDrawerProvider.tsx +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightspeedDrawerProvider.tsx @@ -31,7 +31,7 @@ const useStyles = makeStyles(theme => ({ bottom: `calc(${theme?.spacing?.(2) ?? '16px'} + 5em)`, right: `calc(${theme?.spacing?.(2) ?? '16px'} + 1.5em)`, maxWidth: 'min(30rem, calc(100vw - 32px)) !important', - overflowX: 'hidden' as const, + overflow: 'hidden' as const, transition: 'margin-right 0.3s ease', 'body.docked-drawer-open &': { marginRight: DOCKED_CONTENT_OFFSET, diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/ToastAlertGroup.tsx b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/ToastAlertGroup.tsx new file mode 100644 index 00000000000..1421da0c3ea --- /dev/null +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/ToastAlertGroup.tsx @@ -0,0 +1,82 @@ +/* + * 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 React from 'react'; + +import { makeStyles } from '@material-ui/core'; +import { + Alert, + AlertActionCloseButton, + AlertGroup, + AlertVariant, + type AlertProps, +} from '@patternfly/react-core'; + +const useStyles = makeStyles(theme => ({ + toastAlertGroup: { + '--pf-v6-c-alert-group--m-toast--InsetInlineEnd': `${theme.spacing(2.5)}px`, + '--pf-v6-c-alert-group--m-toast--InsetBlockStart': `${theme.spacing(2.5)}px`, + '--pf-v6-c-alert-group--m-toast--MaxWidth': '350px', + '--pf-v6-c-alert-group--m-toast--ZIndex': '9999', + }, + toastAlert: { + maxWidth: '350px', + '& .pf-v6-c-alert__title': { + margin: 0, + }, + }, +})); + +type ToastAlertGroupProps = { + alerts: Partial[]; + onRemoveAlert: (key: React.Key) => void; +}; + +export const ToastAlertGroup = ({ + alerts, + onRemoveAlert, +}: ToastAlertGroupProps) => { + const classes = useStyles(); + + if (alerts.length === 0) return null; + + return ( + + {alerts.map(({ key, title, variant }) => ( + onRemoveAlert(key as React.Key)} + actionClose={ + onRemoveAlert(key as React.Key)} + /> + } + /> + ))} + + ); +}; diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/__tests__/LightspeedChat.test.tsx b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/__tests__/LightspeedChat.test.tsx index 78e3d9b6486..e1ccf91d426 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/__tests__/LightspeedChat.test.tsx +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/__tests__/LightspeedChat.test.tsx @@ -258,6 +258,8 @@ describe('LightspeedChat', () => { consumePendingOverlayThreadHandoff: jest.fn(() => false), shellViewTab: 0, setShellViewTab: jest.fn(), + activeNotebookId: undefined, + setActiveNotebookId: jest.fn(), }); localStorage.clear(); @@ -677,6 +679,8 @@ describe('LightspeedChat', () => { consumePendingOverlayThreadHandoff: jest.fn(() => false), shellViewTab: 1, setShellViewTab: jest.fn(), + activeNotebookId: undefined, + setActiveNotebookId: jest.fn(), }); render(setupLightspeedChat('/intelligent-assistant/notebooks')); @@ -701,7 +705,7 @@ describe('LightspeedChat', () => { ); }); - it('should not render Chat/Notebooks tabs in overlay mode', async () => { + it('should render Chat/Notebooks tabs in overlay mode', async () => { mockUseLightspeedDrawerContext.mockReturnValue({ isChatbotActive: true, toggleChatbot: jest.fn(), @@ -718,6 +722,8 @@ describe('LightspeedChat', () => { consumePendingOverlayThreadHandoff: jest.fn(() => false), shellViewTab: 0, setShellViewTab: jest.fn(), + activeNotebookId: undefined, + setActiveNotebookId: jest.fn(), }); render(setupLightspeedChat()); @@ -726,15 +732,13 @@ describe('LightspeedChat', () => { expect(screen.getByLabelText('Options')).toBeInTheDocument(); }); + expect(screen.getByRole('tab', { name: 'Chat' })).toBeInTheDocument(); expect( - screen.queryByRole('tab', { name: 'Chat' }), - ).not.toBeInTheDocument(); - expect( - screen.queryByRole('tab', { name: 'Notebooks' }), - ).not.toBeInTheDocument(); + screen.getByRole('tab', { name: 'Notebooks' }), + ).toBeInTheDocument(); }); - it('should not render Chat/Notebooks tabs in docked mode', async () => { + it('should render Chat/Notebooks tabs in docked mode', async () => { mockUseLightspeedDrawerContext.mockReturnValue({ isChatbotActive: true, toggleChatbot: jest.fn(), @@ -751,6 +755,8 @@ describe('LightspeedChat', () => { consumePendingOverlayThreadHandoff: jest.fn(() => false), shellViewTab: 0, setShellViewTab: jest.fn(), + activeNotebookId: undefined, + setActiveNotebookId: jest.fn(), }); render(setupLightspeedChat()); @@ -759,12 +765,10 @@ describe('LightspeedChat', () => { expect(screen.getByLabelText('Options')).toBeInTheDocument(); }); + expect(screen.getByRole('tab', { name: 'Chat' })).toBeInTheDocument(); expect( - screen.queryByRole('tab', { name: 'Chat' }), - ).not.toBeInTheDocument(); - expect( - screen.queryByRole('tab', { name: 'Notebooks' }), - ).not.toBeInTheDocument(); + screen.getByRole('tab', { name: 'Notebooks' }), + ).toBeInTheDocument(); }); it('should show current display mode as selected in full-screen mode', async () => { @@ -784,6 +788,8 @@ describe('LightspeedChat', () => { consumePendingOverlayThreadHandoff: jest.fn(() => false), shellViewTab: 0, setShellViewTab: jest.fn(), + activeNotebookId: undefined, + setActiveNotebookId: jest.fn(), }); render(setupLightspeedChat()); @@ -818,6 +824,8 @@ describe('LightspeedChat', () => { consumePendingOverlayThreadHandoff: jest.fn(() => false), shellViewTab: 0, setShellViewTab: jest.fn(), + activeNotebookId: undefined, + setActiveNotebookId: jest.fn(), }); render(setupLightspeedChat()); @@ -852,6 +860,8 @@ describe('LightspeedChat', () => { consumePendingOverlayThreadHandoff: jest.fn(() => false), shellViewTab: 0, setShellViewTab: jest.fn(), + activeNotebookId: undefined, + setActiveNotebookId: jest.fn(), }); render(setupLightspeedChat()); @@ -946,6 +956,8 @@ describe('LightspeedChat', () => { consumePendingOverlayThreadHandoff: jest.fn(() => false), shellViewTab: 0, setShellViewTab: jest.fn(), + activeNotebookId: undefined, + setActiveNotebookId: jest.fn(), }); }); @@ -982,6 +994,8 @@ describe('LightspeedChat', () => { consumePendingOverlayThreadHandoff: jest.fn(() => false), shellViewTab: 1, setShellViewTab: jest.fn(), + activeNotebookId: undefined, + setActiveNotebookId: jest.fn(), }); render(setupLightspeedChat('/intelligent-assistant')); @@ -1057,6 +1071,8 @@ describe('LightspeedChat', () => { consumePendingOverlayThreadHandoff: jest.fn(() => false), shellViewTab: 0, setShellViewTab: jest.fn(), + activeNotebookId: undefined, + setActiveNotebookId: jest.fn(), }); render(setupLightspeedChat('/intelligent-assistant/notebooks')); @@ -1092,6 +1108,8 @@ describe('LightspeedChat', () => { consumePendingOverlayThreadHandoff: jest.fn(() => false), shellViewTab: 0, setShellViewTab: jest.fn(), + activeNotebookId: undefined, + setActiveNotebookId: jest.fn(), }); }); diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/__tests__/LightspeedDrawerProvider.test.tsx b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/__tests__/LightspeedDrawerProvider.test.tsx index 363edffefb3..911e7aff0c3 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/__tests__/LightspeedDrawerProvider.test.tsx +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/__tests__/LightspeedDrawerProvider.test.tsx @@ -99,6 +99,8 @@ function baseContextValue(): LightspeedDrawerContextType { consumePendingOverlayThreadHandoff: jest.fn(() => false), shellViewTab: 0, setShellViewTab: jest.fn(), + activeNotebookId: undefined, + setActiveNotebookId: jest.fn(), }; } diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/__tests__/LightspeedDrawerStateExposer.test.tsx b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/__tests__/LightspeedDrawerStateExposer.test.tsx index 73995ebf7c7..0b94799cc70 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/__tests__/LightspeedDrawerStateExposer.test.tsx +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/__tests__/LightspeedDrawerStateExposer.test.tsx @@ -43,6 +43,8 @@ describe('LightspeedDrawerStateExposer', () => { setDraftFileContents: jest.fn(), shellViewTab: 0, setShellViewTab: jest.fn(), + activeNotebookId: undefined, + setActiveNotebookId: jest.fn(), ...overrides, }); diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/__tests__/LightspeedFAB.test.tsx b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/__tests__/LightspeedFAB.test.tsx index 67af1716dd3..3027100a9b4 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/__tests__/LightspeedFAB.test.tsx +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/__tests__/LightspeedFAB.test.tsx @@ -43,6 +43,8 @@ describe('LightspeedFAB', () => { setDraftFileContents: jest.fn(), shellViewTab: 0, setShellViewTab: jest.fn(), + activeNotebookId: undefined, + setActiveNotebookId: jest.fn(), ...overrides, }); diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/AddDocumentModal.tsx b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/AddDocumentModal.tsx index 7b85fbb5946..4d25c8f69bf 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/AddDocumentModal.tsx +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/AddDocumentModal.tsx @@ -41,31 +41,18 @@ import { getNotebookAcceptedFileTypes, validateFiles, } from '../../utils/notebook-upload-utils'; +import { getScopedDialogProps } from '../../utils/scoped-dialog-utils'; import { FileListItem } from './FileListItem'; +import { notebookDialogStyles } from './notebookDialogStyles'; const useStyles = makeStyles(theme => ({ - dialogPaper: { - borderRadius: 24, - maxWidth: 578, - }, - dialogTitle: { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - padding: '24px 24px 16px', - }, - titleText: { - fontWeight: 500, - fontSize: '1.25rem', - lineHeight: '1.625rem', + ...notebookDialogStyles(theme), + titleTextCompact: { + fontWeight: 600, + fontSize: '1.125rem', + lineHeight: '1.5rem', letterSpacing: '-0.25px', }, - closeButton: { - color: theme.palette.text.primary, - }, - dialogContent: { - padding: '0 24px 24px', - }, errorAlert: { marginBottom: theme.spacing(2), }, @@ -100,6 +87,11 @@ const useStyles = makeStyles(theme => ({ justifyContent: 'flex-end', gap: theme.spacing(1), }, + dialogActionsCompact: { + padding: '12px 16px !important', + justifyContent: 'flex-end', + gap: theme.spacing(1), + }, addButton: { textTransform: 'none', }, @@ -120,6 +112,7 @@ type AddDocumentModalProps = { onDuplicatesFound?: (files: File[]) => void; filesToAdd?: File[]; onFilesAdded?: () => void; + isCompact?: boolean; }; export const AddDocumentModal = ({ @@ -134,13 +127,13 @@ export const AddDocumentModal = ({ onDuplicatesFound, filesToAdd, onFilesAdded, + isCompact = false, }: AddDocumentModalProps) => { const classes = useStyles(); 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; @@ -224,17 +217,26 @@ export const AddDocumentModal = ({ onClose(); }; + const scopedProps = getScopedDialogProps(isCompact); + return ( - - + + {t('notebook.upload.modal.title')} {selectedFiles.length > 0 && ` (${selectedFiles.length}/${NOTEBOOK_MAX_FILES - existingDocumentNames.length})`} @@ -249,7 +251,11 @@ export const AddDocumentModal = ({ - + {validationErrors.length > 0 && ( {validationErrors @@ -315,7 +321,11 @@ export const AddDocumentModal = ({ )} - + -
+ {!isCompact && ( +
+ +
+ )}
{renderMainContent()}
@@ -809,6 +807,7 @@ export const NotebookView = ({ onDuplicatesFound={handleDuplicatesFound} filesToAdd={filesToAddToModal} onFilesAdded={handleFilesAddedToModal} + isCompact={isCompact} /> f.name)} + isCompact={isCompact} /> setDeleteDocumentTarget(null)} onConfirm={confirmDeleteDocument} documentName={deleteDocumentTarget?.name ?? ''} + isCompact={isCompact} />
); diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/OverwriteConfirmModal.tsx b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/OverwriteConfirmModal.tsx index 61327087ef8..9eddb6cbfa8 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/OverwriteConfirmModal.tsx +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/OverwriteConfirmModal.tsx @@ -26,31 +26,18 @@ import IconButton from '@mui/material/IconButton'; import Typography from '@mui/material/Typography'; import { useTranslation } from '../../hooks/useTranslation'; +import { getScopedDialogProps } from '../../utils/scoped-dialog-utils'; import { FileTypeIcon } from './FileTypeIcon'; +import { notebookDialogStyles } from './notebookDialogStyles'; const useStyles = makeStyles(theme => ({ - dialogPaper: { - borderRadius: 24, - maxWidth: 578, - }, - dialogTitle: { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - padding: '24px 24px 16px', - }, - titleText: { - fontWeight: 500, - fontSize: '1.25rem', - lineHeight: '1.625rem', + ...notebookDialogStyles(theme), + titleTextCompact: { + fontWeight: 600, + fontSize: '1rem', + lineHeight: '1.375rem', letterSpacing: '-0.25px', }, - closeButton: { - color: theme.palette.text.primary, - }, - dialogContent: { - padding: '0 24px 24px', - }, fileList: { margin: 0, padding: 0, @@ -65,6 +52,14 @@ const useStyles = makeStyles(theme => ({ '1px solid var(--pf-t--global--border--color--default, #c7c7c7)', cursor: 'pointer', }, + fileItemCompact: { + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + padding: `${theme.spacing(1)}px 0`, + borderBottom: + '1px solid var(--pf-t--global--border--color--default, #c7c7c7)', + }, fileName: { flex: 1, minWidth: 0, @@ -74,22 +69,55 @@ const useStyles = makeStyles(theme => ({ fontSize: '0.875rem', lineHeight: '1.25rem', }, + fileNameCompact: { + flex: 1, + minWidth: 0, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + fontSize: '0.8125rem', + lineHeight: '1.125rem', + }, dialogActions: { justifyContent: 'left', padding: theme.spacing(2.5), gap: theme.spacing(1), }, + dialogActionsCompact: { + justifyContent: 'flex-start', + padding: '12px 16px !important', + gap: theme.spacing(1), + }, overwriteButton: { textTransform: 'none', borderRadius: 999, }, + overwriteButtonCompact: { + textTransform: 'none', + borderRadius: 999, + fontSize: '0.8125rem', + padding: '4px 16px', + }, cancelButton: { textTransform: 'none', borderRadius: 999, }, + cancelButtonCompact: { + textTransform: 'none', + borderRadius: 999, + fontSize: '0.8125rem', + padding: '4px 16px', + }, warningAlert: { borderRadius: '6px', }, + warningAlertCompact: { + borderRadius: '6px', + fontSize: '0.8125rem', + '& .MuiAlert-icon': { + fontSize: '1.125rem', + }, + }, })); type OverwriteConfirmModalProps = { @@ -97,6 +125,7 @@ type OverwriteConfirmModalProps = { onClose: () => void; onConfirm: () => void; fileNames: string[]; + isCompact?: boolean; }; export const OverwriteConfirmModal = ({ @@ -104,21 +133,31 @@ export const OverwriteConfirmModal = ({ onClose, onConfirm, fileNames, + isCompact = false, }: OverwriteConfirmModalProps) => { const classes = useStyles(); const { t } = useTranslation(); + const scopedProps = getScopedDialogProps(isCompact); + return ( - - + + {t('notebook.overwrite.modal.title')} - + - - + + {t('notebook.overwrite.modal.description')}
    {fileNames.map(name => ( -
  • +
  • - {name} + + {name} +
  • ))}
- + diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/SidebarCollapseIcon.tsx b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/SidebarCollapseIcon.tsx index b3937cfe562..64a83355e90 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/SidebarCollapseIcon.tsx +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/SidebarCollapseIcon.tsx @@ -16,35 +16,39 @@ type IconProps = { className?: string; + size?: number; }; -export const SidebarCollapseIcon = ({ className }: IconProps) => ( +export const SidebarCollapseIcon = ({ className, size = 24 }: IconProps) => ( ); -export const SidebarExpandIcon = ({ className }: IconProps) => ( +export const SidebarExpandIcon = ({ className, size = 24 }: IconProps) => ( ); -type AddCircleFilledIconProps = IconProps & { +type AddCircleFilledIconProps = { + className?: string; disabled?: boolean; }; diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/UploadResourceScreen.tsx b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/UploadResourceScreen.tsx index 2653946517a..61f261c1812 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/UploadResourceScreen.tsx +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/UploadResourceScreen.tsx @@ -72,7 +72,6 @@ export const UploadResourceScreen = ({ }: UploadResourceScreenProps) => { const classes = useStyles(); const { t } = useTranslation(); - return (
diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/notebookDialogStyles.ts b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/notebookDialogStyles.ts new file mode 100644 index 00000000000..14d6278c175 --- /dev/null +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/notebookDialogStyles.ts @@ -0,0 +1,56 @@ +/* + * 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 type { Theme } from '@material-ui/core/styles'; + +export const notebookDialogStyles = (theme: Theme) => + ({ + dialogPaper: { + borderRadius: 24, + maxWidth: 578, + }, + dialogPaperCompact: { + borderRadius: 12, + maxWidth: '100%', + }, + dialogTitle: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '24px 24px 16px', + }, + dialogTitleCompact: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '16px 16px 12px !important', + }, + titleText: { + fontWeight: 500, + fontSize: '1.25rem', + lineHeight: '1.625rem', + letterSpacing: '-0.25px', + }, + closeButton: { + color: theme.palette.text.primary, + }, + dialogContent: { + padding: '0 24px 24px', + }, + dialogContentCompact: { + padding: '0 16px 16px !important', + }, + }) as const; diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/hooks/__tests__/useLightspeedDrawerContext.test.tsx b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/hooks/__tests__/useLightspeedDrawerContext.test.tsx index 64b60f9d134..24dec0802b7 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/hooks/__tests__/useLightspeedDrawerContext.test.tsx +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/hooks/__tests__/useLightspeedDrawerContext.test.tsx @@ -37,6 +37,8 @@ describe('useLightspeedDrawerContext', () => { consumePendingOverlayThreadHandoff: jest.fn(() => false), shellViewTab: 0, setShellViewTab: jest.fn(), + activeNotebookId: undefined, + setActiveNotebookId: jest.fn(), }; it('should return context value when used within provider', () => { diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/hooks/__tests__/useLightspeedProviderState.test.tsx b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/hooks/__tests__/useLightspeedProviderState.test.tsx index 065afb8762b..b2a56447390 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/hooks/__tests__/useLightspeedProviderState.test.tsx +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/hooks/__tests__/useLightspeedProviderState.test.tsx @@ -386,7 +386,7 @@ describe('useLightspeedProviderState', () => { }); }); - it('resets shellViewTab to Chat when leaving embedded for overlay while on Notebooks', async () => { + it('preserves shellViewTab when leaving embedded for overlay while on Notebooks', async () => { renderWithRouter(['/catalog']); screen.getByTestId('set-shell-notebooks-tab').click(); @@ -406,7 +406,7 @@ describe('useLightspeedProviderState', () => { await waitFor(() => { expect(screen.getByTestId('pathname')).toHaveTextContent('/catalog'); - expect(screen.getByTestId('shell-view-tab')).toHaveTextContent('0'); + expect(screen.getByTestId('shell-view-tab')).toHaveTextContent('1'); }); }); }); diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/hooks/useLightspeedProviderState.ts b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/hooks/useLightspeedProviderState.ts index 13555d3b594..c6f0cc1dab7 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/hooks/useLightspeedProviderState.ts +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/hooks/useLightspeedProviderState.ts @@ -67,6 +67,9 @@ export function useLightspeedProviderState(): { FileContent[] >([]); const [shellViewTab, setShellViewTabState] = useState(0); + const [activeNotebookId, setActiveNotebookId] = useState( + undefined, + ); const shellViewTabRef = useRef(shellViewTab); shellViewTabRef.current = shellViewTab; const setShellViewTab = useCallback((tab: number) => { @@ -311,9 +314,6 @@ export function useLightspeedProviderState(): { } setIsOpen(true); } else { - // Notebooks exist only in fullscreen; leaving embedded for overlay/docked - // must not keep shellViewTab on Notebooks (next fullscreen open should be Chat). - setShellViewTab(0); if (isLightspeedRoute) { leavingLightspeedForNonEmbeddedShellRef.current = true; pendingOverlayThreadHandoffRef.current = true; @@ -329,7 +329,6 @@ export function useLightspeedProviderState(): { leaveLightspeedRouteForShellDisplayMode, navigate, setPersistedDisplayMode, - setShellViewTab, syncShellDrawerForMode, ], ); @@ -356,6 +355,8 @@ export function useLightspeedProviderState(): { consumePendingOverlayThreadHandoff, shellViewTab, setShellViewTab, + activeNotebookId, + setActiveNotebookId, }), [ isOpen, @@ -372,6 +373,8 @@ export function useLightspeedProviderState(): { consumePendingOverlayThreadHandoff, shellViewTab, setShellViewTab, + activeNotebookId, + setActiveNotebookId, ], ); diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/utils/scoped-dialog-utils.ts b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/utils/scoped-dialog-utils.ts new file mode 100644 index 00000000000..89f1201ead6 --- /dev/null +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/utils/scoped-dialog-utils.ts @@ -0,0 +1,50 @@ +/* + * 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 type { DialogProps } from '@mui/material/Dialog'; + +export function getScopedDialogProps(isCompact: boolean): Partial { + if (!isCompact) return {}; + return { + disablePortal: true, + disableScrollLock: true, + fullWidth: true, + maxWidth: false, + sx: { + position: 'absolute', + inset: 0, + margin: 0, + '& [class*="Backdrop-root"]': { + position: 'absolute', + }, + }, + PaperProps: { + sx: { + marginTop: '16px !important', + marginBottom: '16px !important', + marginLeft: '40px !important', + marginRight: '40px !important', + borderRadius: '12px !important', + width: 'calc(100% - 80px) !important', + maxWidth: 'calc(100% - 80px) !important', + maxHeight: 'calc(100% - 32px) !important', + overflowX: 'hidden', + overflowY: 'auto', + boxSizing: 'border-box', + }, + }, + }; +}