Skip to content
Open
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
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