Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e0dd1fa
feat(intelligent-assistant): implement docked and overlay display mod…
rohitratannagar Jul 30, 2026
3d4dec7
fix(intelligent-assistant): update test to preserve notebook tab acro…
rohitratannagar Jul 30, 2026
e4d419a
Delete, upload, rename modals render within the docked/overlay panel …
rohitratannagar Jul 30, 2026
809141e
fix(intelligent-assistant): fix modal header alignment, e2e login ret…
rohitratannagar Jul 30, 2026
8b2f6bf
fix(intelligent-assistant): adjust compact upload modal title font we…
rohitratannagar Jul 30, 2026
696ed36
fix(intelligent-assistant): hide notebook header actions in compact m…
rohitratannagar Aug 4, 2026
c73f9d8
refactor(intelligent-assistant): lift sidebar and upload modal state …
rohitratannagar Aug 6, 2026
d2d693f
Merge remote-tracking branch 'upstream/main' into feat/RHIDP-14656-no…
rohitratannagar Aug 6, 2026
421274b
chore: retrigger CI
rohitratannagar Aug 6, 2026
7c8ed4d
chore: retrigger CI
rohitratannagar Aug 9, 2026
92ed5f2
fix sonar issue
rohitratannagar Aug 11, 2026
8648ca3
matching the chat header from the prototype
rohitratannagar Aug 11, 2026
44ce0de
feat: use outlined AddCircleOIcon and auto-close sidebar on chat select
rohitratannagar Aug 12, 2026
81cfb15
addressing comments
rohitratannagar Aug 13, 2026
a2bd2fa
Merge upstream/main into feat/RHIDP-14656-notebook-overlay-docked-modes
rohitratannagar Aug 17, 2026
d265266
fix(intelligent-assistant): remove PF style overrides per review feed…
rohitratannagar Aug 17, 2026
5a6a826
fix(intelligent-assistant): fix prettier formatting in OverwriteConfi…
rohitratannagar Aug 17, 2026
96eb588
fix(intelligent-assistant): prevent notebook auto-delete on display m…
rohitratannagar Aug 19, 2026
5d32537
fix(intelligent-assistant): preserve notebook stream state across dis…
rohitratannagar Aug 20, 2026
f1e5e2d
fix(intelligent-assistant): fix notebook auto-delete, tab persistence…
rohitratannagar Aug 20, 2026
fdfc9c7
addressing comments
rohitratannagar Aug 24, 2026
4077164
fixing the stream issues
rohitratannagar Aug 25, 2026
ca1531e
fix(intelligent-assistant): stream notebook source citations during r…
rohitratannagar Aug 26, 2026
ca5e3f4
fix(intelligent-assistant): don't use raw match text as notebook sour…
rohitratannagar Aug 26, 2026
a6d33e2
fix(intelligent-assistant): restore toast inset and max-width styling
rohitratannagar Aug 26, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@red-hat-developer-hub/backstage-plugin-intelligent-assistant': minor
---

implement docked and overlay display modes for Notebook
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,17 @@ test.describe('Intelligent assistant conversation', () => {
await verifySidePanelConversation(sharedPage, translations);
});

test('Verify scroll controls in Conversation', async ({
browser,
}, testInfo) => {
test('Verify scroll controls in Conversation', async ({}, testInfo) => {
await mockChatHistory(sharedPage, demoChatContent);
await sharedPage.reload();
await sharedPage.locator('.pf-chatbot__messagebox').waitFor({
state: 'visible',
});
await sharedPage.waitForSelector('.pf-chatbot__message--bot', {
timeout: 10_000,
});

const message = demoChatContent[0].messages[0].content;
await sendMessage(message, sharedPage, translations, false);

const jumpTopButton = sharedPage.getByRole('button', {
name: translations['aria.scroll.up'],
Expand All @@ -154,17 +158,22 @@ test.describe('Intelligent assistant conversation', () => {
await jumpTopButton.click();
await sharedPage.waitForTimeout(500);
await expect(
sharedPage.locator('span').filter({ hasText: message }),
sharedPage
.locator('.pf-chatbot__message--user')
.filter({ hasText: message })
.first(),
).toBeVisible();

await verifySidePanelConversation(sharedPage, translations);
await expect(jumpBottomButton).toBeVisible();
await jumpBottomButton.click();

const responseMessage = sharedPage
.locator('div.pf-chatbot__message-response')
.last();
await expect(responseMessage).toHaveText(/OpenShift deployment/);
await expect(
sharedPage
.locator('div.pf-chatbot__message-response')
.filter({ hasText: /OpenShift deployment/ })
.first(),
).toBeVisible();
});

test('Filter and switch conversations', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
/*
* 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 fileInput = dialog.locator('input[type="file"]');
await fileInput.setInputFiles([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();
// Empty notebooks (no uploaded documents) are auto-deleted on close
await expect(
notebooks.createNotebookFromEmptyStateButton(),
).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();
}
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<DisplayMode, string> = {
Overlay: t['settings.displayMode.overlay'],
'Dock to window': t['settings.displayMode.docked'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -51,6 +51,11 @@ export class NotebookDeleteDialogPage {
}

async confirmDeletion(): Promise<void> {
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 });
}
}
Loading
Loading