From e0dd1fab45be55e381fae91a11a6c83dbcab64c4 Mon Sep 17 00:00:00 2001 From: rohitratannagar Date: Thu, 30 Jul 2026 07:13:44 +0530 Subject: [PATCH 01/11] feat(intelligent-assistant): implement docked and overlay display modes for Notebook Enable notebook functionality in docked and overlay (non-fullscreen) display modes. Previously, notebooks were only accessible in fullscreen/embedded mode. - Add activeNotebookId to drawer context for state management without routes - Show Chat/Notebooks tabs in overlay and docked modes - Manage notebook selection via state instead of URL navigation in compact modes - Add header actions (close, add document, toggle sidebar) for compact modes - Use single-column grid layout for notebook cards in narrow panels - Fix NotebookView flex chain for proper sizing within docked panels - Override PF Chatbot embedded CSS (min-height, overflow) to prevent content overflow in constrained containers - Make DrawerPanelContent take full width when in compact mode - Update tests for new context fields and tab visibility RHIDP-14656 Assisted-by: Claude Opus 4.6 Co-Authored-By: Claude Opus 4.6 --- .../src/components/LightSpeedChat.tsx | 168 ++- .../components/LightspeedDrawerContext.tsx | 11 +- .../components/LightspeedDrawerProvider.tsx | 2 +- .../__tests__/LightspeedChat.test.tsx | 38 +- .../LightspeedDrawerProvider.test.tsx | 2 + .../LightspeedDrawerStateExposer.test.tsx | 2 + .../__tests__/LightspeedFAB.test.tsx | 2 + .../src/components/notebooks/NotebookView.tsx | 1042 +++++++++-------- .../notebooks/UploadResourceScreen.tsx | 1 - .../useLightspeedDrawerContext.test.tsx | 2 + .../src/hooks/useLightspeedProviderState.ts | 14 +- 11 files changed, 738 insertions(+), 546 deletions(-) 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 5ef597e663c..c9d5203b3fa 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightSpeedChat.tsx +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightSpeedChat.tsx @@ -62,6 +62,7 @@ import { Label, MenuToggle, MenuToggleElement, + Button as PFButton, Select, SelectList, SelectOption, @@ -71,11 +72,13 @@ import { } from '@patternfly/react-core'; import { PenIcon, + PlusCircleIcon, PlusIcon, SearchIcon, SortAmountDownAltIcon, SortAmountDownIcon, ThumbtackIcon, + TimesIcon, TrashIcon, } from '@patternfly/react-icons'; import { RhUiAiExperienceIcon } from '@patternfly/react-icons/dist/esm/icons/rh-ui-ai-experience-icon'; @@ -127,8 +130,12 @@ import { McpServersSettings } from './McpServersSettings'; import { MessageBarModelSelector } from './MessageBarModelSelector'; import { DeleteNotebookModal } from './notebooks/DeleteNotebookModal'; import { NotebooksTab } from './notebooks/NotebooksTab'; -import { NotebookView } from './notebooks/NotebookView'; +import { + NotebookView, + type NotebookViewHandle, +} from './notebooks/NotebookView'; import { RenameNotebookModal } from './notebooks/RenameNotebookModal'; +import { SidebarExpandIcon } from './notebooks/SidebarCollapseIcon'; import PermissionRequiredState from './PermissionRequiredState'; import { RenameConversationModal } from './RenameConversationModal'; @@ -146,8 +153,14 @@ const ConditionalWrapper = ({ const useStyles = makeStyles(theme => ({ body: { - // remove default margin and padding from common elements - // lists excluded for proper formatting + height: '100% !important', + minHeight: '0 !important', + overflow: 'hidden', + '& .pf-chatbot-container': { + minHeight: '0 !important', + display: 'flex', + flexDirection: 'column', + }, '& h1, & h2, & h3, & h4, & h5, & h6, & p, & li': { margin: 0, padding: 0, @@ -191,6 +204,11 @@ const useStyles = makeStyles(theme => ({ alignItems: 'center', }, }, + notebookHeaderActions: { + display: 'flex', + alignItems: 'center', + gap: theme.spacing(0.5), + }, headerLogo: { width: 48, height: 48, @@ -280,6 +298,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', @@ -666,6 +692,8 @@ export const LightspeedChat = ({ consumePendingOverlayThreadHandoff, shellViewTab, setShellViewTab, + activeNotebookId, + setActiveNotebookId, } = useLightspeedDrawerContext(); const isFullscreenMode = displayMode === ChatbotDisplayMode.embedded; const location = useLocation(); @@ -676,9 +704,6 @@ export const LightspeedChat = ({ const [filterValue, setFilterValue] = useState(''); const [announcement, setAnnouncement] = useState(''); const [activeTab, setActiveTab] = useState(() => { - if (!isFullscreenMode) { - return 0; - } if (notebooksRouteMatch || notebookViewRouteMatch) { return 1; } @@ -709,33 +734,44 @@ export const LightspeedChat = ({ const [activeNotebook, setActiveNotebook] = useState( null, ); + const effectiveNotebookId = + routeNotebookId || (!isFullscreenMode ? activeNotebookId : undefined); const { data: routeNotebook, isLoading: routeNotebookLoading, isError: routeNotebookError, - } = useNotebookSession(routeNotebookId); + } = useNotebookSession(effectiveNotebookId); useEffect(() => { - if (routeNotebookId && routeNotebook && !routeNotebookLoading) { + if (effectiveNotebookId && routeNotebook && !routeNotebookLoading) { setActiveNotebook(routeNotebook); - } else if (routeNotebookId && routeNotebookError) { - navigate(`${LIGHTSPEED_PATH}/notebooks`, { replace: true }); - } else if (!routeNotebookId && notebooksRouteMatch) { + setActiveNotebookId(routeNotebook.session_id); + } else if (effectiveNotebookId && routeNotebookError) { + if (isFullscreenMode) { + navigate(`${LIGHTSPEED_PATH}/notebooks`, { replace: true }); + } else { + setActiveNotebook(null); + setActiveNotebookId(undefined); + } + } else if (!effectiveNotebookId && notebooksRouteMatch) { setActiveNotebook(null); } }, [ - routeNotebookId, + effectiveNotebookId, routeNotebook, routeNotebookLoading, routeNotebookError, notebooksRouteMatch, + isFullscreenMode, navigate, + setActiveNotebookId, ]); const [notebookAlerts, setNotebookAlerts] = useState[]>( [], ); const createNotebookMutation = useCreateNotebook(); + const notebookViewRef = useRef(null); const { data: notebookDocuments = [], isFetching: isDocumentsFetching } = useNotebookDocuments(activeNotebook?.session_id); const [conversationId, setConversationId] = useState(''); @@ -756,7 +792,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] = @@ -794,17 +830,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(); } }; @@ -833,16 +871,26 @@ 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 { + setActiveNotebook(session); + setActiveNotebookId(session.session_id); + } }, }, ); - }, [createNotebookMutation, navigate]); + }, [createNotebookMutation, isFullscreenMode, navigate, setActiveNotebookId]); const handleCloseNotebook = useCallback(() => { - navigate(`${LIGHTSPEED_PATH}/notebooks`); + if (isFullscreenMode) { + navigate(`${LIGHTSPEED_PATH}/notebooks`); + } else { + setActiveNotebook(null); + setActiveNotebookId(undefined); + } refetchNotebooks(); - }, [navigate, refetchNotebooks]); + }, [isFullscreenMode, navigate, refetchNotebooks, setActiveNotebookId]); const handleRemoveNotebookAlert = (key: React.Key) => { setNotebookAlerts(prevAlerts => @@ -1941,6 +1989,46 @@ export const LightspeedChat = ({ aria-label={t('aria.chatHistoryMenu')} /> )} + {!isFullscreenMode && showNotebooksPanel && activeNotebook && ( +
+ + + + + + + notebookViewRef.current?.openUploadModal()} + aria-label={t('notebook.view.documents.add')} + size="sm" + > + + + + + notebookViewRef.current?.toggleSidebar()} + aria-label={t('notebook.view.sidebar.expand')} + size="sm" + > + + + +
+ )} {isFullscreenMode && ( <> setIsMcpSettingsOpen(true)} /> - {isFullscreenMode &&
} - {isFullscreenMode && shouldShowTabs && ( + {(isFullscreenMode || shouldShowTabs) && ( +
+ )} + {shouldShowTabs && ( )} {showNotebooksPanel && @@ -2171,11 +2263,25 @@ export const LightspeedChat = ({ { - navigate(`${LIGHTSPEED_PATH}/notebooks/${notebook.session_id}`); + if (isFullscreenMode) { + navigate( + `${LIGHTSPEED_PATH}/notebooks/${notebook.session_id}`, + ); + } else { + setActiveNotebook(notebook); + setActiveNotebookId(notebook.session_id); + } }} onRename={setRenameNotebookId} onDelete={setDeleteNotebookId} 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/__tests__/LightspeedChat.test.tsx b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/__tests__/LightspeedChat.test.tsx index 78e3d9b6486..d210386d104 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.queryByRole('tab', { name: 'Chat' }), - ).not.toBeInTheDocument(); + expect(screen.queryByRole('tab', { name: 'Chat' })).toBeInTheDocument(); expect( screen.queryByRole('tab', { name: 'Notebooks' }), - ).not.toBeInTheDocument(); + ).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.queryByRole('tab', { name: 'Chat' }), - ).not.toBeInTheDocument(); + expect(screen.queryByRole('tab', { name: 'Chat' })).toBeInTheDocument(); expect( screen.queryByRole('tab', { name: 'Notebooks' }), - ).not.toBeInTheDocument(); + ).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/NotebookView.tsx b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/NotebookView.tsx index b07d0bd4688..35bb65fb3f8 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/NotebookView.tsx +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/NotebookView.tsx @@ -14,7 +14,14 @@ * limitations under the License. */ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useRef, + useState, +} from 'react'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; @@ -72,12 +79,20 @@ const useStyles = makeStyles(theme => ({ flexDirection: 'column', flex: 1, minHeight: 0, - height: '100%', + minWidth: 0, + width: '100%', + overflow: 'hidden', backgroundColor: 'var(--pf-t--global--background--color--primary--default)', }, drawerContainer: { flex: 1, minHeight: 0, + minWidth: 0, + '& .pf-v6-c-drawer__content, & .pf-v5-c-drawer__content': { + display: 'flex', + flexDirection: 'column', + overflow: 'hidden', + }, '& .pf-v6-c-drawer__panel, & .pf-v5-c-drawer__panel': { backgroundColor: 'var(--pf-t--global--background--color--floating--default) !important', @@ -113,7 +128,8 @@ const useStyles = makeStyles(theme => ({ mainArea: { display: 'flex', flexDirection: 'row', - height: '100%', + flex: 1, + minHeight: 0, minWidth: 0, }, topBar: { @@ -131,10 +147,15 @@ const useStyles = makeStyles(theme => ({ flexDirection: 'column', flex: 1, minHeight: 0, + minWidth: 0, }, drawerContentBody: { backgroundColor: 'var(--pf-t--global--background--color--primary--default)', - height: '100%', + display: 'flex', + flexDirection: 'column', + flex: 1, + minHeight: 0, + minWidth: 0, }, contentColumn: { display: 'flex', @@ -197,6 +218,7 @@ const useStyles = makeStyles(theme => ({ display: 'flex', flexDirection: 'column', minHeight: 0, + minWidth: 0, backgroundColor: 'var(--pf-t--global--background--color--floating--default)', }, @@ -271,6 +293,11 @@ const useStyles = makeStyles(theme => ({ }, })); +export type NotebookViewHandle = { + openUploadModal: () => void; + toggleSidebar: () => void; +}; + type NotebookViewProps = { sessionId: string; notebookName?: string; @@ -283,540 +310,561 @@ type NotebookViewProps = { profileLoading: boolean; topicRestrictionEnabled: boolean; onClose: () => void; + isCompact?: boolean; }; -export const NotebookView = ({ - sessionId, - notebookName = UNTITLED_NOTEBOOK_NAME, - documents = [], - isDocumentsFetching = false, - metadata, - topicSummary, - userName, - avatar, - profileLoading, - topicRestrictionEnabled, - onClose, -}: NotebookViewProps) => { - const classes = useStyles(); - const { t } = useTranslation(); - const queryClient = useQueryClient(); - const configApi = useApi(configApiRef); - const notebooksApi = useApi(notebooksApiRef); - const { mutateAsync: notebookCreateMessage } = useCreateNotebookMessage(); - - // Use notebook-specific model from config instead of chat's selected model - const notebookModel = - configApi.getOptionalString( - 'intelligent-assistant.notebooks.queryDefaults.model', - ) || ''; - - const [conversationId, setConversationId] = useState( - metadata?.conversation_id ?? TEMP_CONVERSATION_ID, - ); - const [isSendButtonDisabled, setIsSendButtonDisabled] = useState(false); - const [announcement, setAnnouncement] = useState( - undefined, - ); - const [deletingDocumentIds, setDeletingDocumentIds] = useState>( - new Set(), - ); - const [deleteDocumentTarget, setDeleteDocumentTarget] = useState<{ - id: string; - name: string; - } | null>(null); - - const handleDeleteDocument = useCallback((documentId: string) => { - setDeleteDocumentTarget({ id: documentId, name: documentId }); - }, []); - - const onComplete = useCallback( - (message: string) => { - setIsSendButtonDisabled(false); - setAnnouncement(`Message from Bot: ${message}`); - queryClient.invalidateQueries({ - queryKey: ['conversationMessages', conversationId], - }); - }, - [queryClient, conversationId], - ); - - const onStart = useCallback((conv_id: string) => { - setConversationId(conv_id); - }, []); - - const createMessageAdapter = useCallback( - async (vars: CreateMessageVariables) => { - return notebookCreateMessage({ - prompt: vars.prompt, - sessionId, - }); - }, - [notebookCreateMessage, sessionId], - ); - - const { conversationMessages, handleInputPrompt, scrollToBottomRef } = - useConversationMessages( - conversationId, +export const NotebookView = forwardRef( + ( + { + sessionId, + notebookName = UNTITLED_NOTEBOOK_NAME, + documents = [], + isDocumentsFetching = false, + metadata, + topicSummary, userName, - notebookModel, - '', avatar, - onComplete, - onStart, - createMessageAdapter, + profileLoading, + topicRestrictionEnabled, + onClose, + isCompact = false, + }, + ref, + ) => { + const classes = useStyles(); + const { t } = useTranslation(); + const queryClient = useQueryClient(); + const configApi = useApi(configApiRef); + const notebooksApi = useApi(notebooksApiRef); + const { mutateAsync: notebookCreateMessage } = useCreateNotebookMessage(); + + // Use notebook-specific model from config instead of chat's selected model + const notebookModel = + configApi.getOptionalString( + 'intelligent-assistant.notebooks.queryDefaults.model', + ) || ''; + + const [conversationId, setConversationId] = useState( + metadata?.conversation_id ?? TEMP_CONVERSATION_ID, + ); + const [isSendButtonDisabled, setIsSendButtonDisabled] = useState(false); + const [announcement, setAnnouncement] = useState( + undefined, + ); + const [deletingDocumentIds, setDeletingDocumentIds] = useState>( + new Set(), + ); + const [deleteDocumentTarget, setDeleteDocumentTarget] = useState<{ + id: string; + name: string; + } | null>(null); + + const handleDeleteDocument = useCallback((documentId: string) => { + setDeleteDocumentTarget({ id: documentId, name: documentId }); + }, []); + + const onComplete = useCallback( + (message: string) => { + setIsSendButtonDisabled(false); + setAnnouncement(`Message from Bot: ${message}`); + queryClient.invalidateQueries({ + queryKey: ['conversationMessages', conversationId], + }); + }, + [queryClient, conversationId], ); - const [messages, setMessages] = - useState(conversationMessages); + const onStart = useCallback((conv_id: string) => { + setConversationId(conv_id); + }, []); - useEffect(() => { - setMessages(conversationMessages); - }, [conversationMessages]); + const createMessageAdapter = useCallback( + async (vars: CreateMessageVariables) => { + return notebookCreateMessage({ + prompt: vars.prompt, + sessionId, + }); + }, + [notebookCreateMessage, sessionId], + ); - const sendMessage = useCallback( - (message: string | number) => { - setAnnouncement( - t('conversation.announcement.userMessage' as any, { - prompt: message.toString(), - }), + const { conversationMessages, handleInputPrompt, scrollToBottomRef } = + useConversationMessages( + conversationId, + userName, + notebookModel, + '', + avatar, + onComplete, + onStart, + createMessageAdapter, ); - handleInputPrompt(message.toString(), []); - setIsSendButtonDisabled(true); - }, - [handleInputPrompt, t], - ); - - const notebookPrompts = useNotebookWelcomePrompts(); - const welcomePrompts = notebookPrompts.map(title => ({ - title, - onClick: () => sendMessage(title), - })); - - const [sidebarCollapsed, setSidebarCollapsed] = useState(false); - const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); - const [uploadingFileNames, setUploadingFileNames] = useState([]); - const [pendingUploads, setPendingUploads] = useState([]); - const [toastAlerts, setToastAlerts] = useState[]>([]); - const processedIds = useRef>(new Set()); - const [completedFileNames, setCompletedFileNames] = useState>( - new Set(), - ); - const [filesToOverwrite, setFilesToOverwrite] = useState([]); - const [isOverwriteModalOpen, setIsOverwriteModalOpen] = useState(false); - const [filesToAddToModal, setFilesToAddToModal] = useState([]); - - const confirmDeleteDocument = useCallback(async () => { - if (!deleteDocumentTarget) return; - const { id: documentId, name: documentName } = deleteDocumentTarget; - setDeleteDocumentTarget(null); - setDeletingDocumentIds(prev => new Set(prev).add(documentId)); - try { - await notebooksApi.deleteDocument(sessionId, documentId); - queryClient.invalidateQueries({ - queryKey: ['notebooks', 'documents', sessionId], + + const [messages, setMessages] = + useState(conversationMessages); + + useEffect(() => { + setMessages(conversationMessages); + }, [conversationMessages]); + + const sendMessage = useCallback( + (message: string | number) => { + setAnnouncement( + t('conversation.announcement.userMessage' as any, { + prompt: message.toString(), + }), + ); + handleInputPrompt(message.toString(), []); + setIsSendButtonDisabled(true); + }, + [handleInputPrompt, t], + ); + + const notebookPrompts = useNotebookWelcomePrompts(); + const welcomePrompts = notebookPrompts.map(title => ({ + title, + onClick: () => sendMessage(title), + })); + + const [sidebarCollapsed, setSidebarCollapsed] = useState(isCompact); + const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); + + useImperativeHandle(ref, () => ({ + openUploadModal: () => setIsUploadModalOpen(true), + toggleSidebar: () => setSidebarCollapsed(prev => !prev), + })); + const [uploadingFileNames, setUploadingFileNames] = useState([]); + const [pendingUploads, setPendingUploads] = useState([]); + const [toastAlerts, setToastAlerts] = useState[]>([]); + const processedIds = useRef>(new Set()); + const [completedFileNames, setCompletedFileNames] = useState>( + new Set(), + ); + const [filesToOverwrite, setFilesToOverwrite] = useState([]); + const [isOverwriteModalOpen, setIsOverwriteModalOpen] = useState(false); + const [filesToAddToModal, setFilesToAddToModal] = useState([]); + + const confirmDeleteDocument = useCallback(async () => { + if (!deleteDocumentTarget) return; + const { id: documentId, name: documentName } = deleteDocumentTarget; + setDeleteDocumentTarget(null); + setDeletingDocumentIds(prev => new Set(prev).add(documentId)); + try { + await notebooksApi.deleteDocument(sessionId, documentId); + queryClient.invalidateQueries({ + queryKey: ['notebooks', 'documents', sessionId], + }); + setToastAlerts(prev => [ + { + key: Date.now() + documentId, + title: (t as Function)('notebook.document.delete.success', { + documentName, + }) as string, + variant: 'success', + }, + ...prev, + ]); + } finally { + setDeletingDocumentIds(prev => { + const next = new Set(prev); + next.delete(documentId); + return next; + }); + } + }, [deleteDocumentTarget, notebooksApi, sessionId, queryClient, t]); + + const handleOpenUploadModal = () => setIsUploadModalOpen(true); + const handleCloseUploadModal = () => setIsUploadModalOpen(false); + + const handleFilesUploading = (files: File[]) => { + setUploadingFileNames(prev => { + const newNames = files + .map(f => f.name) + .filter(name => !prev.includes(name)); + return [...prev, ...newNames]; }); + }; + + const handleUploadStarted = (info: { + fileName: string; + documentId: string; + }) => { + processedIds.current.delete(info.documentId); + setPendingUploads(prev => [ + ...prev, + { fileName: info.fileName, documentId: info.documentId }, + ]); + }; + + const handleUploadFailed = (fileName: string) => { + setUploadingFileNames(prev => prev.filter(n => n !== fileName)); setToastAlerts(prev => [ { - key: Date.now() + documentId, - title: (t as Function)('notebook.document.delete.success', { - documentName, + key: Date.now() + fileName, + title: (t as Function)('notebook.upload.failed', { + fileName, }) as string, - variant: 'success', + variant: 'danger', }, ...prev, ]); - } finally { - setDeletingDocumentIds(prev => { - const next = new Set(prev); - next.delete(documentId); - return next; - }); - } - }, [deleteDocumentTarget, notebooksApi, sessionId, queryClient, t]); - - const handleOpenUploadModal = () => setIsUploadModalOpen(true); - const handleCloseUploadModal = () => setIsUploadModalOpen(false); - - const handleFilesUploading = (files: File[]) => { - setUploadingFileNames(prev => { - const newNames = files - .map(f => f.name) - .filter(name => !prev.includes(name)); - return [...prev, ...newNames]; - }); - }; - - const handleUploadStarted = (info: { - fileName: string; - documentId: string; - }) => { - processedIds.current.delete(info.documentId); - setPendingUploads(prev => [ - ...prev, - { fileName: info.fileName, documentId: info.documentId }, - ]); - }; - - const handleUploadFailed = (fileName: string) => { - setUploadingFileNames(prev => prev.filter(n => n !== fileName)); - setToastAlerts(prev => [ - { - key: Date.now() + fileName, - title: (t as Function)('notebook.upload.failed', { - fileName, - }) as string, - variant: 'danger', - }, - ...prev, - ]); - }; - - const handleDuplicatesFound = (files: File[]) => { - setFilesToOverwrite(files); - setIsOverwriteModalOpen(true); - }; - - const handleOverwriteConfirm = () => { - const files = filesToOverwrite; - setIsOverwriteModalOpen(false); - setFilesToOverwrite([]); - - if (files.length === 0) return; - - setFilesToAddToModal(files); - }; - - const handleFilesAddedToModal = () => { - setFilesToAddToModal([]); - }; - - const handleOverwriteCancel = () => { - setIsOverwriteModalOpen(false); - setFilesToOverwrite([]); - }; - - const pollingResults = useDocumentStatusPolling(sessionId, pendingUploads); - - useEffect(() => { - const completedOrFailed = pollingResults.filter( - r => - (r.status === 'completed' || - r.status === 'failed' || - r.status === 'cancelled') && - !processedIds.current.has(r.documentId), - ); + }; + + const handleDuplicatesFound = (files: File[]) => { + setFilesToOverwrite(files); + setIsOverwriteModalOpen(true); + }; + + const handleOverwriteConfirm = () => { + const files = filesToOverwrite; + setIsOverwriteModalOpen(false); + setFilesToOverwrite([]); + + if (files.length === 0) return; + + setFilesToAddToModal(files); + }; + + const handleFilesAddedToModal = () => { + setFilesToAddToModal([]); + }; - if (completedOrFailed.length === 0) return; + const handleOverwriteCancel = () => { + setIsOverwriteModalOpen(false); + setFilesToOverwrite([]); + }; - const idsToRemove = new Set(); - const namesToRemove = new Set(); - const newAlerts: Partial[] = []; + const pollingResults = useDocumentStatusPolling(sessionId, pendingUploads); - const newCompletedNames = new Set(); + useEffect(() => { + const completedOrFailed = pollingResults.filter( + r => + (r.status === 'completed' || + r.status === 'failed' || + r.status === 'cancelled') && + !processedIds.current.has(r.documentId), + ); - for (const result of completedOrFailed) { - processedIds.current.add(result.documentId); - idsToRemove.add(result.documentId); - namesToRemove.add(result.fileName); - if (result.status === 'completed') { - newCompletedNames.add(result.fileName); + if (completedOrFailed.length === 0) return; + + const idsToRemove = new Set(); + const namesToRemove = new Set(); + const newAlerts: Partial[] = []; + + const newCompletedNames = new Set(); + + for (const result of completedOrFailed) { + processedIds.current.add(result.documentId); + idsToRemove.add(result.documentId); + namesToRemove.add(result.fileName); + if (result.status === 'completed') { + newCompletedNames.add(result.fileName); + } + + if (result.status !== 'completed') { + const errorDetail = result.error ? ` ${result.error}` : ''; + newAlerts.push({ + key: Date.now() + result.documentId, + title: `${ + (t as Function)('notebook.upload.failed', { + fileName: result.fileName, + }) as string + }${errorDetail}`, + variant: 'danger', + }); + } } - if (result.status !== 'completed') { - const errorDetail = result.error ? ` ${result.error}` : ''; - newAlerts.push({ - key: Date.now() + result.documentId, - title: `${ - (t as Function)('notebook.upload.failed', { - fileName: result.fileName, - }) as string - }${errorDetail}`, - variant: 'danger', + setPendingUploads(prev => + prev.filter(u => !idsToRemove.has(u.documentId)), + ); + setUploadingFileNames(prev => + prev.filter(name => !namesToRemove.has(name)), + ); + if (newCompletedNames.size > 0) { + setCompletedFileNames(prev => new Set([...prev, ...newCompletedNames])); + queryClient.invalidateQueries({ + queryKey: ['notebooks', 'documents', sessionId], }); } - } - - setPendingUploads(prev => prev.filter(u => !idsToRemove.has(u.documentId))); - setUploadingFileNames(prev => - prev.filter(name => !namesToRemove.has(name)), + setToastAlerts(prev => [...newAlerts, ...prev]); + }, [pollingResults, t, queryClient, sessionId]); + + const handleRemoveToastAlert = (key: React.Key) => { + setToastAlerts(prev => prev.filter(a => a.key !== key)); + }; + + const totalDocumentCount = documents.length + uploadingFileNames.length; + const hasUploadsInProgress = + pendingUploads.length > 0 || isDocumentsFetching; + const hasNoDocuments = documents.length === 0; + const isAddDisabled = + totalDocumentCount >= NOTEBOOK_MAX_FILES || hasUploadsInProgress; + + const panelContent = ( + + setSidebarCollapsed(prev => !prev)} + onAddDocument={handleOpenUploadModal} + onDeleteDocument={handleDeleteDocument} + /> + ); - if (newCompletedNames.size > 0) { - setCompletedFileNames(prev => new Set([...prev, ...newCompletedNames])); - queryClient.invalidateQueries({ - queryKey: ['notebooks', 'documents', sessionId], - }); - } - setToastAlerts(prev => [...newAlerts, ...prev]); - }, [pollingResults, t, queryClient, sessionId]); - - const handleRemoveToastAlert = (key: React.Key) => { - setToastAlerts(prev => prev.filter(a => a.key !== key)); - }; - - const totalDocumentCount = documents.length + uploadingFileNames.length; - const hasUploadsInProgress = pendingUploads.length > 0 || isDocumentsFetching; - const hasNoDocuments = documents.length === 0; - const isAddDisabled = - totalDocumentCount >= NOTEBOOK_MAX_FILES || hasUploadsInProgress; - - const panelContent = ( - - setSidebarCollapsed(prev => !prev)} - onAddDocument={handleOpenUploadModal} - onDeleteDocument={handleDeleteDocument} - /> - - ); - - const renderNotebookDisclaimerAlert = () => ( -
-
- - {t('disclaimer.withoutValidation')} - + + const renderNotebookDisclaimerAlert = () => ( +
+
+ + {t('disclaimer.withoutValidation')} + +
-
- ); + ); - const renderMainContent = () => { - if (hasNoDocuments && messages.length === 0) { - return ( - - 0} - /> - - ); - } - if (messages.length > 0) { - return ( - - - - ); - } - return ( -
-
- {renderNotebookDisclaimerAlert()} -
- - {notebookName} + const renderMainContent = () => { + if (hasNoDocuments && messages.length === 0) { + return ( + + 0} + /> - {topicSummary && ( - - {topicSummary} + ); + } + if (messages.length > 0) { + return ( + + + + ); + } + return ( +
+
+ {renderNotebookDisclaimerAlert()} +
+ + {notebookName} + {topicSummary && ( + + {topicSummary} + + )} +
+ {welcomePrompts.length > 0 && ( +
+ {welcomePrompts.map(prompt => ( + + ))} +
)}
- {welcomePrompts.length > 0 && ( -
- {welcomePrompts.map(prompt => ( - + ); + }; + + return ( +
+ {toastAlerts.length > 0 && ( + + {toastAlerts.map(({ key, title, variant }) => ( + handleRemoveToastAlert(key as React.Key)} + actionClose={ + handleRemoveToastAlert(key as React.Key)} + /> + } + /> ))} -
+ )} -
- ); - }; - - return ( -
- {toastAlerts.length > 0 && ( - - {toastAlerts.map(({ key, title, variant }) => ( - handleRemoveToastAlert(key as React.Key)} - actionClose={ - handleRemoveToastAlert(key as React.Key)} - /> - } - /> - ))} - - )} - - - -
- {sidebarCollapsed && ( -
- - - - { - if (hasUploadsInProgress) - return t('notebook.view.documents.uploadsInProgress'); - if (isAddDisabled) - return t('notebook.view.documents.maxReached'); - return t('notebook.view.documents.add'); - })()} - position="right" - > - - - -
- )} - -
-
- -
- -
{renderMainContent()}
- - {hasNoDocuments && - messages.length === 0 && - renderNotebookDisclaimerAlert()} - - - {hasNoDocuments ? ( + { + if (hasUploadsInProgress) + return t('notebook.view.documents.uploadsInProgress'); + if (isAddDisabled) + return t('notebook.view.documents.maxReached'); + return t('notebook.view.documents.add'); + })()} + position="right" > -
- -
+ + +
- ) : ( - +
+ )} + +
+ {!isCompact && ( +
+ +
)} - - + +
+ {renderMainContent()} +
+ + {hasNoDocuments && + messages.length === 0 && + renderNotebookDisclaimerAlert()} + + + {hasNoDocuments ? ( + +
+ +
+
+ ) : ( + + )} + +
+
-
- - - - - d.title)} - hasUploadsInProgress={hasUploadsInProgress} - onFilesUploading={handleFilesUploading} - onUploadStarted={handleUploadStarted} - onUploadFailed={handleUploadFailed} - onDuplicatesFound={handleDuplicatesFound} - filesToAdd={filesToAddToModal} - onFilesAdded={handleFilesAddedToModal} - /> - - f.name)} - /> - - setDeleteDocumentTarget(null)} - onConfirm={confirmDeleteDocument} - documentName={deleteDocumentTarget?.name ?? ''} - /> -
- ); -}; + + + + + d.title)} + hasUploadsInProgress={hasUploadsInProgress} + onFilesUploading={handleFilesUploading} + onUploadStarted={handleUploadStarted} + onUploadFailed={handleUploadFailed} + onDuplicatesFound={handleDuplicatesFound} + filesToAdd={filesToAddToModal} + onFilesAdded={handleFilesAddedToModal} + /> + + f.name)} + /> + + setDeleteDocumentTarget(null)} + onConfirm={confirmDeleteDocument} + documentName={deleteDocumentTarget?.name ?? ''} + /> +
+ ); + }, +); 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/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/useLightspeedProviderState.ts b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/hooks/useLightspeedProviderState.ts index 13555d3b594..e8929fe9086 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, setActiveNotebookIdState] = useState< + string | undefined + >(undefined); const shellViewTabRef = useRef(shellViewTab); shellViewTabRef.current = shellViewTab; const setShellViewTab = useCallback((tab: number) => { @@ -74,6 +77,9 @@ export function useLightspeedProviderState(): { shellViewTabRef.current = next; setShellViewTabState(next); }, []); + const setActiveNotebookId = useCallback((id: string | undefined) => { + setActiveNotebookIdState(id); + }, []); const openedViaFABRef = useRef(false); const dockedAfterLeavingFullscreenRef = useRef(false); /** True while navigating off /lightspeed after user chose overlay/docked (URL can lag persisted mode). */ @@ -311,9 +317,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 +332,6 @@ export function useLightspeedProviderState(): { leaveLightspeedRouteForShellDisplayMode, navigate, setPersistedDisplayMode, - setShellViewTab, syncShellDrawerForMode, ], ); @@ -356,6 +358,8 @@ export function useLightspeedProviderState(): { consumePendingOverlayThreadHandoff, shellViewTab, setShellViewTab, + activeNotebookId, + setActiveNotebookId, }), [ isOpen, @@ -372,6 +376,8 @@ export function useLightspeedProviderState(): { consumePendingOverlayThreadHandoff, shellViewTab, setShellViewTab, + activeNotebookId, + setActiveNotebookId, ], ); From 3d4dec7a03a941d6859aa429fb86dd4f7f6e9cee Mon Sep 17 00:00:00 2001 From: rohitratannagar Date: Thu, 30 Jul 2026 07:27:29 +0530 Subject: [PATCH 02/11] fix(intelligent-assistant): update test to preserve notebook tab across display mode switch Notebooks now work in overlay/docked modes, so shellViewTab should remain on Notebooks (1) when switching from embedded to overlay instead of resetting to Chat (0). Also adds changeset for the feature. Co-Authored-By: Claude Opus 4.6 --- .../.changeset/notebook-overlay-docked-modes.md | 5 +++++ .../src/hooks/__tests__/useLightspeedProviderState.test.tsx | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 workspaces/intelligent-assistant/.changeset/notebook-overlay-docked-modes.md 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/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'); }); }); }); From e4d419a2009e3182b954b2708bfcc2ac23f626c4 Mon Sep 17 00:00:00 2001 From: rohitratannagar Date: Thu, 30 Jul 2026 09:38:28 +0530 Subject: [PATCH 03/11] Delete, upload, rename modals render within the docked/overlay panel bounds Signed-off-by: rohitratannagar --- .../src/components/LightSpeedChat.tsx | 90 +++++++++++-------- .../components/notebooks/AddDocumentModal.tsx | 56 ++++++++++-- .../notebooks/DeleteDocumentModal.tsx | 43 +++++++-- .../notebooks/DeleteNotebookModal.tsx | 51 +++++++++-- .../src/components/notebooks/NotebookView.tsx | 8 +- .../notebooks/OverwriteConfirmModal.tsx | 4 + .../notebooks/RenameNotebookModal.tsx | 57 ++++++++++-- .../src/utils/scoped-dialog-utils.ts | 48 ++++++++++ 8 files changed, 294 insertions(+), 63 deletions(-) create mode 100644 workspaces/intelligent-assistant/plugins/intelligent-assistant/src/utils/scoped-dialog-utils.ts 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 c9d5203b3fa..bf5dbe4353a 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightSpeedChat.tsx +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightSpeedChat.tsx @@ -1957,17 +1957,7 @@ export const LightspeedChat = ({ currentName={ notebooks.find(n => n.session_id === renameNotebookId)?.name ?? '' } - /> - )} - {deleteNotebookId && ( - setDeleteNotebookId(null)} - onDeleted={handleNotebookDeleted} - sessionId={deleteNotebookId} - name={ - notebooks.find(n => n.session_id === deleteNotebookId)?.name ?? '' - } + isCompact={!isFullscreenMode} /> )} { - if (isFullscreenMode) { - navigate( - `${LIGHTSPEED_PATH}/notebooks/${notebook.session_id}`, - ); - } else { - setActiveNotebook(notebook); - setActiveNotebookId(notebook.session_id); - } +
+ > + { + if (isFullscreenMode) { + navigate( + `${LIGHTSPEED_PATH}/notebooks/${notebook.session_id}`, + ); + } else { + setActiveNotebook(notebook); + setActiveNotebookId(notebook.session_id); + } + }} + onRename={setRenameNotebookId} + onDelete={setDeleteNotebookId} + onCreateNotebook={handleCreateNotebook} + t={t} + /> + {deleteNotebookId && ( + setDeleteNotebookId(null)} + onDeleted={handleNotebookDeleted} + sessionId={deleteNotebookId} + name={ + notebooks.find(n => n.session_id === deleteNotebookId) + ?.name ?? '' + } + isCompact={!isFullscreenMode} + /> + )} +
)} {showNotebooksPanel && !notebooksPermissionLoading && 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..c25a4e9ab26 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,6 +41,7 @@ import { getNotebookAcceptedFileTypes, validateFiles, } from '../../utils/notebook-upload-utils'; +import { getScopedDialogProps } from '../../utils/scoped-dialog-utils'; import { FileListItem } from './FileListItem'; const useStyles = makeStyles(theme => ({ @@ -48,24 +49,44 @@ const useStyles = makeStyles(theme => ({ borderRadius: 24, maxWidth: 578, }, + dialogPaperCompact: { + borderRadius: 12, + maxWidth: '100%', + padding: theme.spacing(0.5), + }, dialogTitle: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '24px 24px 16px', }, + dialogTitleCompact: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '12px 16px 8px', + }, titleText: { fontWeight: 500, fontSize: '1.25rem', lineHeight: '1.625rem', letterSpacing: '-0.25px', }, + titleTextCompact: { + fontWeight: 500, + fontSize: '1rem', + lineHeight: '1.375rem', + letterSpacing: '-0.25px', + }, closeButton: { color: theme.palette.text.primary, }, dialogContent: { padding: '0 24px 24px', }, + dialogContentCompact: { + padding: '0 16px 16px', + }, errorAlert: { marginBottom: theme.spacing(2), }, @@ -100,6 +121,11 @@ const useStyles = makeStyles(theme => ({ justifyContent: 'flex-end', gap: theme.spacing(1), }, + dialogActionsCompact: { + padding: '12px 16px', + justifyContent: 'flex-end', + gap: theme.spacing(1), + }, addButton: { textTransform: 'none', }, @@ -120,6 +146,7 @@ type AddDocumentModalProps = { onDuplicatesFound?: (files: File[]) => void; filesToAdd?: File[]; onFilesAdded?: () => void; + isCompact?: boolean; }; export const AddDocumentModal = ({ @@ -134,13 +161,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 +251,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 +285,11 @@ export const AddDocumentModal = ({ - + {validationErrors.length > 0 && ( {validationErrors @@ -315,7 +355,11 @@ export const AddDocumentModal = ({ )} - + - ))} -
)}
- ); - }; - - return ( -
- {toastAlerts.length > 0 && ( - - {toastAlerts.map(({ key, title, variant }) => ( - handleRemoveToastAlert(key as React.Key)} - actionClose={ - handleRemoveToastAlert(key as React.Key)} - /> - } - /> + {welcomePrompts.length > 0 && ( +
+ {welcomePrompts.map(prompt => ( + ))} - +
)} - + ); + }; + + return ( +
+ {toastAlerts.length > 0 && ( + - - -
- {sidebarCollapsed && !isCompact && ( -
- ( + handleRemoveToastAlert(key as React.Key)} + actionClose={ + handleRemoveToastAlert(key as React.Key)} + /> + } + /> + ))} + + )} + + + +
+ {sidebarCollapsed && !isCompact && ( +
+ + + + { + if (hasUploadsInProgress) + return t('notebook.view.documents.uploadsInProgress'); + if (isAddDisabled) + return t('notebook.view.documents.maxReached'); + return t('notebook.view.documents.add'); + })()} + position="right" + > + - - { - if (hasUploadsInProgress) - return t('notebook.view.documents.uploadsInProgress'); - if (isAddDisabled) - return t('notebook.view.documents.maxReached'); - return t('notebook.view.documents.add'); - })()} - position="right" + + +
+ )} + +
+ {!isCompact && ( +
+ - - + {t('notebook.view.close')} +
)} -
- {!isCompact && ( -
- -
- )} - -
- {renderMainContent()} -
+
{renderMainContent()}
- {hasNoDocuments && - messages.length === 0 && - renderNotebookDisclaimerAlert()} + {hasNoDocuments && + messages.length === 0 && + renderNotebookDisclaimerAlert()} - - {hasNoDocuments ? ( - -
- -
-
- ) : ( - - )} - + {hasNoDocuments ? ( + +
+ +
+
+ ) : ( + -
-
+ )} + +
- - - - - d.title)} - hasUploadsInProgress={hasUploadsInProgress} - onFilesUploading={handleFilesUploading} - onUploadStarted={handleUploadStarted} - onUploadFailed={handleUploadFailed} - onDuplicatesFound={handleDuplicatesFound} - filesToAdd={filesToAddToModal} - onFilesAdded={handleFilesAddedToModal} - isCompact={isCompact} - /> - - f.name)} - isCompact={isCompact} - /> - - setDeleteDocumentTarget(null)} - onConfirm={confirmDeleteDocument} - documentName={deleteDocumentTarget?.name ?? ''} - isCompact={isCompact} - /> -
- ); - }, -); +
+ + + + + d.title)} + hasUploadsInProgress={hasUploadsInProgress} + onFilesUploading={handleFilesUploading} + onUploadStarted={handleUploadStarted} + onUploadFailed={handleUploadFailed} + 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 ebce6cde55d..75e489629f3 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 @@ -34,24 +34,43 @@ const useStyles = makeStyles(theme => ({ 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', }, + titleTextCompact: { + fontWeight: 600, + fontSize: '1rem', + lineHeight: '1.375rem', + letterSpacing: '-0.25px', + }, closeButton: { color: theme.palette.text.primary, }, dialogContent: { padding: '0 24px 24px', }, + dialogContentCompact: { + padding: '0 16px 16px !important', + }, fileList: { margin: 0, padding: 0, @@ -66,6 +85,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, @@ -75,22 +102,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 = { @@ -111,18 +171,26 @@ export const OverwriteConfirmModal = ({ 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/utils/scoped-dialog-utils.ts b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/utils/scoped-dialog-utils.ts index 0f094825b2c..89f1201ead6 100644 --- 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 @@ -27,7 +27,6 @@ export function getScopedDialogProps(isCompact: boolean): Partial { position: 'absolute', inset: 0, margin: 0, - // padding: 0, '& [class*="Backdrop-root"]': { position: 'absolute', }, From 421274be364a174024c3ec2f873a14a08b9f3777 Mon Sep 17 00:00:00 2001 From: rohitratannagar Date: Fri, 7 Aug 2026 04:22:11 +0530 Subject: [PATCH 08/11] chore: retrigger CI From 7c8ed4d805b971ad7bc4b634905e23c83847684a Mon Sep 17 00:00:00 2001 From: rohitratannagar Date: Mon, 10 Aug 2026 03:19:13 +0530 Subject: [PATCH 09/11] chore: retrigger CI From 92ed5f2c2af5e047bd9482755ed2e33edb9fe86b Mon Sep 17 00:00:00 2001 From: rohitratannagar Date: Tue, 11 Aug 2026 11:21:11 +0530 Subject: [PATCH 10/11] fix sonar issue Signed-off-by: rohitratannagar --- .../src/components/LightSpeedChat.tsx | 47 ++--------- .../src/components/ToastAlertGroup.tsx | 82 +++++++++++++++++++ .../__tests__/LightspeedChat.test.tsx | 8 +- .../components/notebooks/AddDocumentModal.tsx | 37 +-------- .../src/components/notebooks/NotebookView.tsx | 46 ++--------- .../notebooks/OverwriteConfirmModal.tsx | 37 +-------- .../notebooks/SidebarCollapseIcon.tsx | 3 +- .../notebooks/notebookDialogStyles.ts | 56 +++++++++++++ .../src/hooks/useLightspeedProviderState.ts | 9 +- 9 files changed, 161 insertions(+), 164 deletions(-) create mode 100644 workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/ToastAlertGroup.tsx create mode 100644 workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/notebookDialogStyles.ts 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 b8e95e8d4fc..1c420a2a3d2 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightSpeedChat.tsx +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/LightSpeedChat.tsx @@ -54,10 +54,6 @@ import { } from '@patternfly/chatbot'; import ChatbotConversationHistoryNav from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav'; import { - Alert, - AlertActionCloseButton, - AlertGroup, - AlertVariant, DropdownItem, Label, MenuToggle, @@ -132,6 +128,7 @@ import { NotebooksTab } from './notebooks/NotebooksTab'; import { NotebookView } from './notebooks/NotebookView'; 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`; @@ -457,18 +454,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, @@ -1907,32 +1892,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 && ( ({ + 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 d210386d104..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 @@ -732,9 +732,9 @@ describe('LightspeedChat', () => { expect(screen.getByLabelText('Options')).toBeInTheDocument(); }); - expect(screen.queryByRole('tab', { name: 'Chat' })).toBeInTheDocument(); + expect(screen.getByRole('tab', { name: 'Chat' })).toBeInTheDocument(); expect( - screen.queryByRole('tab', { name: 'Notebooks' }), + screen.getByRole('tab', { name: 'Notebooks' }), ).toBeInTheDocument(); }); @@ -765,9 +765,9 @@ describe('LightspeedChat', () => { expect(screen.getByLabelText('Options')).toBeInTheDocument(); }); - expect(screen.queryByRole('tab', { name: 'Chat' })).toBeInTheDocument(); + expect(screen.getByRole('tab', { name: 'Chat' })).toBeInTheDocument(); expect( - screen.queryByRole('tab', { name: 'Notebooks' }), + screen.getByRole('tab', { name: 'Notebooks' }), ).toBeInTheDocument(); }); 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 004473e2718..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 @@ -43,49 +43,16 @@ import { } 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, - }, - 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', - }, + ...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', - }, - dialogContentCompact: { - padding: '0 16px 16px !important', - }, errorAlert: { marginBottom: theme.spacing(2), }, diff --git a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/NotebookView.tsx b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/NotebookView.tsx index 2408305543c..bfea20d0991 100644 --- a/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/NotebookView.tsx +++ b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/components/notebooks/NotebookView.tsx @@ -27,9 +27,6 @@ import { } from '@patternfly/chatbot'; import { Alert, - AlertActionCloseButton, - AlertGroup, - AlertVariant, Button, Drawer, DrawerContent, @@ -60,6 +57,7 @@ import { useTranslation } from '../../hooks/useTranslation'; import { NotebookSessionMetadata, SessionDocument } from '../../types'; import { ChatbotFootnoteWithIcon } from '../../utils/lightspeed-chatbox-utils'; import { LightspeedChatBox } from '../LightspeedChatBox'; +import { ToastAlertGroup } from '../ToastAlertGroup'; import { AddDocumentModal } from './AddDocumentModal'; import { DeleteDocumentModal } from './DeleteDocumentModal'; import { DocumentSidebar } from './DocumentSidebar'; @@ -186,18 +184,6 @@ const useStyles = makeStyles(theme => ({ maxWidth: 'unset', margin: '0 auto', }, - 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, - }, - }, welcomeContainer: { display: 'flex', flexDirection: 'column', @@ -695,32 +681,10 @@ export const NotebookView = ({ className={classes.root} style={isCompact ? { position: 'relative' as const } : undefined} > - {toastAlerts.length > 0 && ( - - {toastAlerts.map(({ key, title, variant }) => ( - handleRemoveToastAlert(key as React.Key)} - actionClose={ - handleRemoveToastAlert(key as React.Key)} - /> - } - /> - ))} - - )} + ({ - 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', - }, + ...notebookDialogStyles(theme), titleTextCompact: { fontWeight: 600, fontSize: '1rem', lineHeight: '1.375rem', letterSpacing: '-0.25px', }, - closeButton: { - color: theme.palette.text.primary, - }, - dialogContent: { - padding: '0 24px 24px', - }, - dialogContentCompact: { - padding: '0 16px 16px !important', - }, fileList: { margin: 0, padding: 0, 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 4f55ba094be..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 @@ -47,7 +47,8 @@ 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/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/useLightspeedProviderState.ts b/workspaces/intelligent-assistant/plugins/intelligent-assistant/src/hooks/useLightspeedProviderState.ts index e8929fe9086..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,9 +67,9 @@ export function useLightspeedProviderState(): { FileContent[] >([]); const [shellViewTab, setShellViewTabState] = useState(0); - const [activeNotebookId, setActiveNotebookIdState] = useState< - string | undefined - >(undefined); + const [activeNotebookId, setActiveNotebookId] = useState( + undefined, + ); const shellViewTabRef = useRef(shellViewTab); shellViewTabRef.current = shellViewTab; const setShellViewTab = useCallback((tab: number) => { @@ -77,9 +77,6 @@ export function useLightspeedProviderState(): { shellViewTabRef.current = next; setShellViewTabState(next); }, []); - const setActiveNotebookId = useCallback((id: string | undefined) => { - setActiveNotebookIdState(id); - }, []); const openedViaFABRef = useRef(false); const dockedAfterLeavingFullscreenRef = useRef(false); /** True while navigating off /lightspeed after user chose overlay/docked (URL can lag persisted mode). */ From 8648ca32e5bc14078b97f4c11882672fa233f1db Mon Sep 17 00:00:00 2001 From: rohitratannagar Date: Tue, 11 Aug 2026 23:45:30 +0530 Subject: [PATCH 11/11] matching the chat header from the prototype Signed-off-by: rohitratannagar --- .../src/components/LightSpeedChat.tsx | 85 ++++++++++++++++--- 1 file changed, 72 insertions(+), 13 deletions(-) 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 1c420a2a3d2..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, @@ -58,6 +57,7 @@ import { Label, MenuToggle, MenuToggleElement, + Button as PfButton, Select, SelectList, SelectOption, @@ -126,6 +126,10 @@ 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'; @@ -190,11 +194,28 @@ 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: { @@ -1935,13 +1956,47 @@ export const LightspeedChat = ({ {showChatPanel && !isFullscreenMode && ( - +
+ + + {isChatHistoryDrawerOpen ? ( + + ) : ( + + )} + + + {!isChatHistoryDrawerOpen && ( + + + + + + )} +
)} {!isFullscreenMode && showNotebooksPanel && activeNotebook && (