Skip to content
Open
11 changes: 11 additions & 0 deletions workspaces/lightspeed/.changeset/witty-eyes-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@red-hat-developer-hub/backstage-plugin-lightspeed': minor
---

Implemented fullscreen chat UX updates including:

- Collapsible history panel with new expand/collapse icons
- Redesigned message bar with inline model selector and attachment menu
- New collapsed history strip with quick new chat functionality
- Updated header with Lightspeed logo
- Improved conversation list with hover-only options menu
7 changes: 4 additions & 3 deletions workspaces/lightspeed/e2e-tests/lightspeed.ui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ test.describe('Lightspeed UI', () => {
function validationTestCase(path: string, name: string) {
test(`should validate file: ${name}`, async ({ browser }, testInfo) => {
const fileExtension = `.${name.split('.').pop()}`;
await uploadFiles(sharedPage, [path]);
await uploadFiles(sharedPage, [path], translations);

if (supportedFileTypes.includes(fileExtension)) {
await uploadAndAssertDuplicate(
Expand Down Expand Up @@ -221,7 +221,7 @@ test.describe('Lightspeed UI', () => {
test(`Multiple file upload`, async () => {
const file1 = `e2e-tests/fixtures/uploads/${locale}.upload1.json`;
const file2 = `e2e-tests/fixtures/uploads/${locale}.upload2.json`;
await uploadFiles(sharedPage, [file1, file2]);
await uploadFiles(sharedPage, [file1, file2], translations);

const heading = sharedPage.getByRole('heading', {
name: `Danger alert: ${translations['chatbox.fileUpload.failed']}`,
Expand All @@ -235,7 +235,8 @@ test.describe('Lightspeed UI', () => {

await assertVisibilityState('visible', heading, text, closeBtn);

await closeBtn.click();
// Use evaluate to click via JavaScript to bypass the iframe overlay
await closeBtn.evaluate((el: HTMLElement) => el.click());

await assertVisibilityState('hidden', heading, text, closeBtn);
});
Expand Down
34 changes: 24 additions & 10 deletions workspaces/lightspeed/e2e-tests/pages/LightspeedPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,18 @@ export async function selectDisplayMode(
}

export async function openChatHistoryDrawer(page: Page, t: LightspeedMessages) {
await page.getByRole('button', { name: t['aria.chatHistoryMenu'] }).click();
const chatHistoryMenuButton = page.getByRole('button', {
name: t['aria.chatHistoryMenu'],
});
const expandHistoryButton = page.getByRole('button', {
name: t['tooltip.expandHistoryPanel'],
});

if (await chatHistoryMenuButton.isVisible()) {
await chatHistoryMenuButton.click();
} else if (await expandHistoryButton.isVisible()) {
await expandHistoryButton.click();
}
}

export async function closeChatHistoryDrawer(
Expand All @@ -74,9 +85,12 @@ export async function expectChatbotControlsVisible(
t: LightspeedMessages,
) {
await expect(page.locator('.pf-chatbot__header')).toBeVisible();
await expect(
page.getByRole('button', { name: t['aria.chatHistoryMenu'] }),
).toBeVisible();
const chatHistoryMenuButton = page.getByRole('button', {
name: t['aria.chatHistoryMenu'],
});
if (await chatHistoryMenuButton.isVisible().catch(() => false)) {
await expect(chatHistoryMenuButton).toBeVisible();
}
await expect(
page.getByRole('button', { name: t['aria.settings.label'] }),
).toBeVisible();
Expand Down Expand Up @@ -344,12 +358,12 @@ export async function verifyMcpSettingsPanel(
}
}

await expect(page.getByLabel('Chatbot', { exact: true }))
.toMatchAriaSnapshot(`
- button "${t['aria.chatHistoryMenu']}"
- button "${t['aria.chatbotSelector']}"
- button "${t['aria.settings.label']}"
`);
await expect(
page.getByRole('button', { name: t['aria.chatbotSelector'] }),
).toBeVisible();
await expect(
page.getByRole('button', { name: t['aria.settings.label'] }),
).toBeVisible();

await closeMcpSettingsPanel(page, t);
await expectMcpServersSettingsHeading(page, false, t);
Expand Down
65 changes: 48 additions & 17 deletions workspaces/lightspeed/e2e-tests/utils/fileUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,29 +30,50 @@ export async function triggerFileChooser(
return fileChooser;
}

export async function uploadFiles(page: Page, filePath: string[]) {
// button name stays the same, only tooltip is translated
const attachButton = page.getByRole('button', { name: 'Attach' });
await expect(attachButton).toBeVisible();

const fileChooser = await triggerFileChooser(page, attachButton);
await fileChooser.setFiles(filePath);
export async function uploadFiles(
page: Page,
filePath: string[],
translations: LightspeedMessages,
) {
// The attach button is now a dropdown toggle with a PlusIcon
// aria-label uses 'tooltip.attach' translation
const plusButton = page.getByRole('button', {
name: translations['tooltip.attach'],
});
await expect(plusButton).toBeVisible();

// Use the hidden file input directly - this bypasses the dropdown menu
// The input has the multiple attribute so it can accept multiple files
const fileInput = page.locator('input[data-testid="attachment-input"]');

// Clear the input first to ensure change event fires even for the same file
// This is necessary because browsers don't fire 'change' if the same file is selected again
await fileInput.evaluate((el: HTMLInputElement) => {
el.value = '';
});

await fileInput.setInputFiles(filePath);
}

export async function uploadAndAssertDuplicate(
page: Page,
filePath: string,
fileName: string,
translations: LightspeedMessages,
testInfo: TestInfo,
_testInfo: TestInfo,
) {
await validateSuccessfulUpload(page, fileName, translations, testInfo);
await uploadFiles(page, [filePath]);
// First, verify the initial upload was successful by checking the file button is visible
await expect(page.getByRole('button', { name: fileName })).toBeVisible();

// Upload the same file again to trigger duplicate detection
await uploadFiles(page, [filePath], translations);

// Assert the duplicate file error alert appears
await expect(
page.getByRole('heading', {
name: translations['chatbox.fileUpload.failed'],
}),
).toBeVisible();
).toBeVisible({ timeout: 10000 });
await expect(
page.getByText(translations['file.upload.error.alreadyExists']),
).toBeVisible();
Expand Down Expand Up @@ -88,7 +109,11 @@ export async function validateSuccessfulUpload(
.getByRole('button', { name: translations['modal.close'] }),
).toBeVisible();

await page.getByRole('button', { name: translations['modal.edit'] }).click();
// Use evaluate to click buttons via JavaScript to bypass the iframe overlay
const editButton = page.getByRole('button', {
name: translations['modal.edit'],
});
await editButton.evaluate((el: HTMLElement) => el.click());
await runAccessibilityTests(page, testInfo);

await expect(
Expand All @@ -98,11 +123,15 @@ export async function validateSuccessfulUpload(
page.getByRole('button', { name: translations['modal.cancel'] }),
).toBeVisible();

await page.getByRole('button', { name: translations['modal.save'] }).click();
await page
const saveButton = page.getByRole('button', {
name: translations['modal.save'],
});
await saveButton.evaluate((el: HTMLElement) => el.click());

const closeButton = page
.getByRole('contentinfo')
.locator(`role=button[name="${translations['modal.close']}"]`)
.click();
.getByRole('button', { name: translations['modal.close'] });
await closeButton.evaluate((el: HTMLElement) => el.click());
}

export async function validateFailedUpload(
Expand All @@ -117,7 +146,9 @@ export async function validateFailedUpload(
await expect(alertHeader).toBeVisible();
await expect(alertText).toBeVisible();

await page.getByRole('button', { name: 'Close Danger alert' }).click();
// Use evaluate to click the button via JavaScript to bypass the iframe overlay
const closeButton = page.getByRole('button', { name: 'Close Danger alert' });
await closeButton.evaluate((el: HTMLElement) => el.click());
await expect(alertHeader).toBeHidden();
await expect(alertText).toBeHidden();
}
Expand Down
38 changes: 33 additions & 5 deletions workspaces/lightspeed/e2e-tests/utils/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,20 @@ export async function assertChatDialogInitialState(
await expect(page.getByLabel('Chatbot', { exact: true })).toContainText(
translations['chatbox.header.title'],
);
await expect(
page.getByRole('button', { name: translations['aria.chatHistoryMenu'] }),
).toBeVisible();

const chatHistoryMenuButton = page.getByRole('button', {
name: translations['aria.chatHistoryMenu'],
});
const closeDrawerButton = page.getByRole('button', {
name: translations['aria.closeDrawerPanel'],
});

if (await chatHistoryMenuButton.isVisible().catch(() => false)) {
await expect(chatHistoryMenuButton).toBeVisible();
} else {
await expect(closeDrawerButton).toBeVisible();
}

await assertDrawerState(page, 'open', translations);

await expect(page.getByLabel(translations['conversation.category.recent']))
Expand Down Expand Up @@ -53,10 +64,27 @@ export async function openChatDrawer(
page: Page,
translations: LightspeedMessages,
) {
const toggleButton = page.getByRole('button', {
const chatHistoryMenuButton = page.getByRole('button', {
name: translations['aria.chatHistoryMenu'],
});
await toggleButton.click();
const expandHistoryButton = page.getByRole('button', {
name: translations['tooltip.expandHistoryPanel'],
});

// Try the hamburger menu first (overlay/docked mode)
if (await chatHistoryMenuButton.isVisible().catch(() => false)) {
await chatHistoryMenuButton.click();
} else {
// In fullscreen mode, use the expand button from CollapsedHistoryStrip
await expect(expandHistoryButton).toBeVisible({ timeout: 5000 });
await expandHistoryButton.click();
}

// Wait for the drawer to open
const closeButton = page.getByRole('button', {
name: translations['aria.closeDrawerPanel'],
});
await expect(closeButton).toBeVisible({ timeout: 5000 });
}

export async function assertDrawerState(
Expand Down
2 changes: 1 addition & 1 deletion workspaces/lightspeed/plugins/lightspeed/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
"@mui/icons-material": "^6.1.8",
"@mui/material": "^5.12.2",
"@mui/styles": "5.18.0",
"@patternfly/chatbot": "6.5.0",
"@patternfly/chatbot": "6.6.0-prerelease.6",
"@patternfly/react-core": "6.4.1",
"@patternfly/react-icons": "^6.3.1",
"@patternfly/react-table": "^6.4.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,9 @@ export const lightspeedTranslationRef: TranslationRef<
readonly 'tooltip.send': string;
readonly 'tooltip.microphone.active': string;
readonly 'tooltip.microphone.inactive': string;
readonly 'tooltip.expandHistoryPanel': string;
readonly 'tooltip.collapseHistoryPanel': string;
readonly 'tooltip.quickNewChat': string;
readonly 'button.newChat': string;
readonly 'tooltip.chatHistoryMenu': string;
readonly 'tooltip.responseRecorded': string;
Expand All @@ -327,6 +330,10 @@ export const lightspeedTranslationRef: TranslationRef<
readonly 'tooltip.close': string;
readonly 'tooltip.fab.open': string;
readonly 'tooltip.fab.close': string;
readonly 'attach.menu.title': string;
readonly 'attach.menu.description': string;
readonly 'history.section.pinned': string;
readonly 'history.section.recent': string;
readonly 'modal.title.preview': string;
readonly 'modal.title.edit': string;
readonly 'icon.lightspeed.alt': string;
Expand Down
Loading
Loading