Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions workspaces/lightspeed/.changeset/khaki-fans-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@red-hat-developer-hub/backstage-plugin-lightspeed': patch
---

### Lightspeed shell and Notebooks

- **Fullscreen**: Chat and Notebooks stay on separate tabs. While the Notebooks tab is active, chat-only header actions (for example chat history, pinned chats, and MCP settings) are hidden so the header matches the active surface.
- **Overlay and docked**: Only the **Chat** surface is shown; the Chat/Notebooks tab strip is not shown, because Notebooks is intended for the fullscreen experience only.
- **Leaving fullscreen from Notebooks**: If you switch from fullscreen while on Notebooks to overlay or docked, you land on **Chat** in the shell, and the next time you open fullscreen you start on **Chat** again (no lingering Notebooks selection).
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
FileRejection,
type DropEvent as ReactDropzoneDropEvent,
} from 'react-dropzone';
import { useMatch, useNavigate } from 'react-router-dom';
import { useLocation, useMatch, useNavigate } from 'react-router-dom';

import { Button, makeStyles } from '@material-ui/core';
import {
Expand Down Expand Up @@ -166,6 +166,7 @@ const useStyles = makeStyles(theme => ({
},
'& .pf-v6-c-tabs__link, & .pf-v5-c-tabs__link': {
backgroundColor: 'transparent',
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2),
fontWeight: 700,
cursor: 'pointer',
Expand Down Expand Up @@ -478,12 +479,41 @@ export const LightspeedChat = ({
const notebooksRouteMatch = useMatch('/lightspeed/notebooks');
const notebookViewRouteMatch = useMatch('/lightspeed/notebooks/:notebookId');
const routeNotebookId = notebookViewRouteMatch?.params?.notebookId;
const {
displayMode,
setDisplayMode,
currentConversationId: routeConversationId,
setCurrentConversationId,
draftMessage,
setDraftMessage,
consumePendingOverlayThreadHandoff,
shellViewTab,
setShellViewTab,
} = useLightspeedDrawerContext();
const isFullscreenMode = displayMode === ChatbotDisplayMode.embedded;
const location = useLocation();
const isNotebooksFullscreenPath =
location.pathname === '/lightspeed/notebooks' ||
location.pathname.startsWith('/lightspeed/notebooks/');
const user = useBackstageUserIdentity();
const [filterValue, setFilterValue] = useState<string>('');
const [announcement, setAnnouncement] = useState<string>('');
const [activeTab, setActiveTab] = useState<number>(
notebooksRouteMatch || notebookViewRouteMatch ? 1 : 0,
);
const [activeTab, setActiveTab] = useState<number>(() => {
if (!isFullscreenMode) {
return 0;
}
if (notebooksRouteMatch || notebookViewRouteMatch) {
return 1;
}
const p = location.pathname;
if (p.startsWith('/lightspeed/conversation/')) {
return 0;
}
if (shellViewTab === 1) {
return 1;
}
return 0;
});
const { allowed: hasNotebooksAccess, loading: notebooksPermissionLoading } =
useLightspeedNotebooksPermission();
const notebooksPermissionResolved =
Expand Down Expand Up @@ -547,27 +577,48 @@ export const LightspeedChat = ({
const wasStoppedByUserRef = useRef(false);
const { isReady, lastOpenedId, setLastOpenedId, clearLastOpenedId } =
useLastOpenedConversation(user);
const {
displayMode,
setDisplayMode,
currentConversationId: routeConversationId,
setCurrentConversationId,
draftMessage,
setDraftMessage,
consumePendingOverlayThreadHandoff,
} = useLightspeedDrawerContext();
const isFullscreenMode = displayMode === ChatbotDisplayMode.embedded;
// Chat vs Notebooks tabs are fullscreen-only; overlay and docked always show Chat.
const showChatPanel = !isFullscreenMode || activeTab === 0;
const showNotebooksPanel = isFullscreenMode && activeTab !== 0;
const [isChatHistoryDrawerOpen, setIsChatHistoryDrawerOpen] =
useState<boolean>(!isMobile && isFullscreenMode);

// Fullscreen: URL drives Chat vs Notebooks, but shellViewTab must win when entering
// fullscreen from overlay/docked on Notebooks while navigation still lands on /lightspeed.
useLayoutEffect(() => {
if (!isFullscreenMode) {
return;
}
if (isNotebooksFullscreenPath) {
setActiveTab(1);
setShellViewTab(1);
return;
}
const isBaseLightspeedChatRoute =
location.pathname === '/lightspeed' ||
location.pathname === '/lightspeed/';
if (shellViewTab === 1 && isBaseLightspeedChatRoute) {
navigate('/lightspeed/notebooks', { replace: true });
return;
}
setActiveTab(0);
setShellViewTab(0);
}, [
isFullscreenMode,
isNotebooksFullscreenPath,
shellViewTab,
location.pathname,
navigate,
setShellViewTab,
]);

const handleNotebookTabSelect = (
_event: React.MouseEvent<any>,
tabIndex: number | string,
) => {
const nextTab = Number(tabIndex);
setActiveTab(nextTab);
setShellViewTab(nextTab);
if (nextTab === 1) {
navigate('/lightspeed/notebooks');
if (notebooksPermissionResolved) {
Expand All @@ -582,6 +633,26 @@ export const LightspeedChat = ({
}
};

const setDisplayModeFromHeader = useCallback(
(mode: ChatbotDisplayMode) => {
if (mode !== ChatbotDisplayMode.embedded) {
setDisplayMode(mode);
return;
}
if (activeTab === 1) {
const sid = activeNotebook?.session_id;
setDisplayMode(
mode,
undefined,
sid ? { notebookSessionId: sid } : 'notebooks',
);
} else {
setDisplayMode(mode);
}
},
[setDisplayMode, activeTab, activeNotebook?.session_id],
);

const handleCreateNotebook = useCallback(() => {
createNotebookMutation.mutate(
{ name: UNTITLED_NOTEBOOK_NAME },
Expand Down Expand Up @@ -1640,13 +1711,15 @@ export const LightspeedChat = ({
>
<ChatbotHeader className={classes.header}>
<ChatbotHeaderMain>
<ChatbotHeaderMenu
aria-expanded={isChatHistoryDrawerOpen}
onMenuToggle={onChatHistoryDrawerToggle}
className={classes.headerMenu}
tooltipContent={t('tooltip.chatHistoryMenu')}
aria-label={t('aria.chatHistoryMenu')}
/>
{showChatPanel && (
<ChatbotHeaderMenu
aria-expanded={isChatHistoryDrawerOpen}
onMenuToggle={onChatHistoryDrawerToggle}
className={classes.headerMenu}
tooltipContent={t('tooltip.chatHistoryMenu')}
aria-label={t('aria.chatHistoryMenu')}
/>
)}
{isFullscreenMode && (
<ChatbotHeaderTitle className={classes.headerTitle}>
<Title headingLevel="h1" size="3xl">
Expand All @@ -1667,7 +1740,8 @@ export const LightspeedChat = ({
isPinningChatsEnabled={isPinningChatsEnabled}
isModelSelectorDisabled={isSendButtonDisabled}
hideModelSelector={showNotebooksPanel}
setDisplayMode={setDisplayMode}
showChatTabOptions={!showNotebooksPanel}
setDisplayMode={setDisplayModeFromHeader}
displayMode={displayMode}
onPinnedChatsToggle={handlePinningChatsToggle}
onMcpSettingsClick={() => setIsMcpSettingsOpen(true)}
Expand Down Expand Up @@ -1793,9 +1867,9 @@ export const LightspeedChat = ({
classes={classes}
openNotebookMenuId={openNotebookMenuId}
setOpenNotebookMenuId={setOpenNotebookMenuId}
onSelectNotebook={(notebook: NotebookSession) =>
navigate(`/lightspeed/notebooks/${notebook.session_id}`)
}
onSelectNotebook={(notebook: NotebookSession) => {
navigate(`/lightspeed/notebooks/${notebook.session_id}`);
}}
onRename={setRenameNotebookId}
onDelete={setDeleteNotebookId}
onCreateNotebook={handleCreateNotebook}
Expand All @@ -1813,7 +1887,10 @@ export const LightspeedChat = ({
variant="outlined"
color="primary"
style={{ borderRadius: '20px' }}
onClick={() => setActiveTab(0)}
onClick={() => {
setActiveTab(0);
setShellViewTab(0);
}}
>
{t('permission.notebooks.goBack')}
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ type LightspeedChatBoxHeaderProps = {
onMcpSettingsClick: () => void;
isModelSelectorDisabled?: boolean;
hideModelSelector?: boolean;
/** When false, omits pinned-chats and MCP entries (Chat tab only). */
showChatTabOptions?: boolean;
setDisplayMode: (mode: ChatbotDisplayMode) => void;
};

Expand Down Expand Up @@ -87,6 +89,7 @@ export const LightspeedChatBoxHeader = ({
onMcpSettingsClick,
isModelSelectorDisabled = false,
hideModelSelector = false,
showChatTabOptions = true,
setDisplayMode,
}: LightspeedChatBoxHeaderProps) => {
const [isOptionsMenuOpen, setIsOptionsMenuOpen] = useState(false);
Expand Down Expand Up @@ -213,40 +216,44 @@ export const LightspeedChatBoxHeader = ({
</DropdownItem>
</DropdownList>
</DropdownGroup>
<Divider />
<DropdownGroup>
<DropdownList>
{isPinningChatsEnabled ? (
<DropdownItem
value="disablePinningChats"
key="disablePinningChat"
icon={<ToggleOnOutlinedIcon sx={{ marginTop: '8px' }} />}
description={t('settings.pinned.enabled.description')}
onClick={() => handlePinningChatsToggle(false)}
>
{t('settings.pinned.disable')}
</DropdownItem>
) : (
<DropdownItem
value="enablePinningChats"
key="enablePinningChats"
icon={<ToggleOffOutlinedIcon sx={{ marginTop: '8px' }} />}
description={t('settings.pinned.disabled.description')}
onClick={() => handlePinningChatsToggle(true)}
>
{t('settings.pinned.enable')}
</DropdownItem>
)}
<DropdownItem
value="mcpSettings"
key="mcpSettings"
icon={<McpSettingsIcon />}
onClick={onMcpSettingsClick}
>
{t('settings.mcp.label')}
</DropdownItem>
</DropdownList>
</DropdownGroup>
{showChatTabOptions && (
<>
<Divider />
<DropdownGroup>
<DropdownList>
{isPinningChatsEnabled ? (
<DropdownItem
value="disablePinningChats"
key="disablePinningChat"
icon={<ToggleOnOutlinedIcon sx={{ marginTop: '8px' }} />}
description={t('settings.pinned.enabled.description')}
onClick={() => handlePinningChatsToggle(false)}
>
{t('settings.pinned.disable')}
</DropdownItem>
) : (
<DropdownItem
value="enablePinningChats"
key="enablePinningChats"
icon={<ToggleOffOutlinedIcon sx={{ marginTop: '8px' }} />}
description={t('settings.pinned.disabled.description')}
onClick={() => handlePinningChatsToggle(true)}
>
{t('settings.pinned.enable')}
</DropdownItem>
)}
<DropdownItem
value="mcpSettings"
key="mcpSettings"
icon={<McpSettingsIcon />}
onClick={onMcpSettingsClick}
>
{t('settings.mcp.label')}
</DropdownItem>
</DropdownList>
</DropdownGroup>
</>
)}
</ChatbotHeaderOptionsDropdown>
</ChatbotHeaderActions>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ import { ChatbotDisplayMode } from '@patternfly/chatbot';

import { FileContent } from '../types';

/**
* When switching to fullscreen (embedded), open the notebooks list or a specific session.
*
* @public
*/
export type LightspeedEmbeddedNotebooksTarget =
| 'notebooks'
| { notebookSessionId: string };

/**
* Type for LightspeedDrawerContext
*
Expand All @@ -39,9 +48,17 @@ export interface LightspeedDrawerContextType {
*/
displayMode: ChatbotDisplayMode;
/**
* Set the display mode (overlay, docked, or fullscreen/embedded)
* 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).
*/
setDisplayMode: (mode: ChatbotDisplayMode) => void;
setDisplayMode: (
mode: ChatbotDisplayMode,
conversationIdParam?: string,
embeddedNotebooks?: LightspeedEmbeddedNotebooksTarget,
) => void;
/**
* The drawer width (for docked mode)
*/
Expand Down Expand Up @@ -83,6 +100,12 @@ export interface LightspeedDrawerContextType {
* lastOpened. Returns true at most once per handoff.
*/
consumePendingOverlayThreadHandoff?: () => boolean;
/**
* Chat tab is 0, Notebooks tab is 1. Persisted across overlay/docked/fullscreen remounts
* (each display mode mounts its own `LightspeedChat` tree).
*/
shellViewTab: number;
setShellViewTab: (tab: number) => void;
}

/**
Expand Down
Loading
Loading