Skip to content

Commit 75e2314

Browse files
romanlutzCopilot
andauthored
FIX Redesign chat ribbon for clarity and narrow viewports (#1736)
Co-authored-by: Roman Lutz <romanlutz@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 484f0d3 commit 75e2314

15 files changed

Lines changed: 710 additions & 214 deletions

frontend/e2e/accessibility.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,17 +153,17 @@ test.describe("Visual Consistency", () => {
153153
// Wait for initial render then navigate to chat to measure the chat ribbon
154154
await expect(page.getByTitle("Chat")).toBeVisible();
155155
await page.getByTitle("Chat").click();
156-
await expect(page.getByText("PyRIT Attack")).toBeVisible();
156+
const anchor = page.getByTestId("new-attack-btn");
157+
await expect(anchor).toBeVisible();
157158

158159
// Take measurements
159-
const header = page.getByText("PyRIT Attack");
160-
const initialBox = await header.boundingBox();
160+
const initialBox = await anchor.boundingBox();
161161

162162
// Wait a moment for any delayed renders
163163
await page.waitForTimeout(500);
164164

165165
// Verify position hasn't changed
166-
const finalBox = await header.boundingBox();
166+
const finalBox = await anchor.boundingBox();
167167

168168
if (initialBox && finalBox) {
169169
expect(finalBox.x).toBe(initialBox.x);

frontend/e2e/chat.spec.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ async function activateMockTarget(page: Page) {
137137

138138
// Return to Chat view
139139
await page.getByTitle("Chat").click();
140-
await expect(page.getByText("PyRIT Attack")).toBeVisible({ timeout: 5000 });
140+
await expect(page.getByTestId("new-attack-btn")).toBeVisible({ timeout: 5000 });
141141
}
142142

143143
// ---------------------------------------------------------------------------
@@ -153,10 +153,10 @@ test.describe("Application Smoke Tests", () => {
153153
await expect(page.locator("body")).toBeVisible();
154154
});
155155

156-
test("should display PyRIT header", async ({ page }) => {
156+
test("should display chat ribbon", async ({ page }) => {
157157
await expect(page.getByTitle("Chat")).toBeVisible({ timeout: 10000 });
158158
await page.getByTitle("Chat").click();
159-
await expect(page.getByText("PyRIT Attack")).toBeVisible({ timeout: 10000 });
159+
await expect(page.getByTestId("new-attack-btn")).toBeVisible({ timeout: 10000 });
160160
});
161161

162162
test("should have New Attack button", async ({ page }) => {
@@ -201,8 +201,12 @@ test.describe("Chat Functionality", () => {
201201
});
202202

203203
test("should display target info after activation", async ({ page }) => {
204-
await expect(page.getByText("OpenAIChatTarget")).toBeVisible();
205-
await expect(page.getByText(/gpt-4o-mock/)).toBeVisible();
204+
// Scope queries to the badge so we don't also match the (hidden)
205+
// copy of the target text that Fluent's Tooltip renders into the DOM.
206+
const badge = page.getByTestId("target-badge");
207+
await expect(badge).toBeVisible();
208+
await expect(badge).toContainText("OpenAIChatTarget");
209+
await expect(badge).toContainText(/gpt-4o-mock/);
206210
});
207211

208212
test("should send a message and receive backend response", async ({ page }) => {
@@ -726,7 +730,11 @@ test.describe("Target type scenarios", () => {
726730

727731
// Navigate to chat
728732
await page.getByTitle("Chat").click();
729-
await expect(page.getByText("OpenAIImageTarget")).toBeVisible();
730-
await expect(page.getByText(/dall-e-3/)).toBeVisible();
733+
// Scope queries to the badge so we don't also match the (hidden)
734+
// copy of the target text that Fluent's Tooltip renders into the DOM.
735+
const badge = page.getByTestId("target-badge");
736+
await expect(badge).toBeVisible();
737+
await expect(badge).toContainText("OpenAIImageTarget");
738+
await expect(badge).toContainText(/dall-e-3/);
731739
});
732740
});

frontend/e2e/config.spec.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -232,11 +232,14 @@ test.describe("Target Config ↔ Chat Navigation", () => {
232232

233233
// Navigate back to chat
234234
await page.getByTitle("Chat").click();
235-
await expect(page.getByText("PyRIT Attack")).toBeVisible();
236-
237-
// Chat should show the active target type
238-
await expect(page.getByText("OpenAIChatTarget")).toBeVisible();
239-
await expect(page.getByText(/gpt-4o/)).toBeVisible();
235+
await expect(page.getByTestId("new-attack-btn")).toBeVisible();
236+
237+
// Chat should show the active target type. Scope to the badge to
238+
// avoid matching the (hidden) tooltip copy of the same text.
239+
const badge = page.getByTestId("target-badge");
240+
await expect(badge).toBeVisible();
241+
await expect(badge).toContainText("OpenAIChatTarget");
242+
await expect(badge).toContainText(/gpt-4o/);
240243
});
241244

242245
test("should enable chat input after a target is set", async ({ page }) => {

frontend/e2e/converters.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ async function activateMockTarget(page: Page) {
416416
await setActiveBtn.click();
417417

418418
await page.getByTitle("Chat", { exact: true }).click();
419-
await expect(page.getByText("PyRIT Attack")).toBeVisible({ timeout: 5000 });
419+
await expect(page.getByTestId("new-attack-btn")).toBeVisible({ timeout: 5000 });
420420
}
421421

422422
/** Open converter panel and select a converter by name. */

frontend/e2e/errors.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ async function activateMockTarget(page: Page) {
175175
await expect(setActiveBtn).toBeVisible({ timeout: 5000 });
176176
await setActiveBtn.click();
177177
await page.getByTitle("Chat").click();
178-
await expect(page.getByText("PyRIT Attack")).toBeVisible({ timeout: 5000 });
178+
await expect(page.getByTestId("new-attack-btn")).toBeVisible({ timeout: 5000 });
179179
}
180180

181181
/** Send a message and wait for the response. */

frontend/e2e/flows.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ async function activateTarget(page: Page, targetType: string): Promise<void> {
161161
const row = page.locator("tr", { has: page.getByText(targetType, { exact: true }) }).first();
162162
await row.getByRole("button", { name: /set active/i }).click();
163163
await page.getByTitle("Chat").click();
164-
await expect(page.getByText("PyRIT Attack")).toBeVisible({ timeout: 5_000 });
164+
await expect(page.getByTestId("new-attack-btn")).toBeVisible({ timeout: 5_000 });
165165
}
166166

167167
/** Navigate to an attack by opening the History view and clicking its row. */

frontend/src/components/Chat/ChatWindow.styles.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,27 @@ export const useChatWindowStyles = makeStyles({
3333
gap: tokens.spacingHorizontalS,
3434
color: tokens.colorNeutralForeground2,
3535
fontSize: tokens.fontSizeBase300,
36-
},
37-
targetInfo: {
38-
display: 'flex',
39-
alignItems: 'center',
40-
gap: tokens.spacingHorizontalXS,
36+
flex: '1 1 auto',
37+
minWidth: 0,
38+
overflow: 'hidden',
4139
},
4240
noTarget: {
4341
color: tokens.colorNeutralForeground3,
4442
fontStyle: 'italic',
43+
flexShrink: 0,
4544
},
4645
ribbonActions: {
4746
display: 'flex',
4847
alignItems: 'center',
4948
gap: tokens.spacingHorizontalS,
49+
flexShrink: 0,
50+
},
51+
newAttackButton: {
52+
flexShrink: 0,
53+
},
54+
newAttackLabel: {
55+
'@media (max-width: 600px)': {
56+
display: 'none',
57+
},
5058
},
5159
})

frontend/src/components/Chat/ChatWindow.test.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -288,8 +288,11 @@ describe("ChatWindow Integration", () => {
288288
</TestWrapper>
289289
);
290290

291-
expect(screen.getByText("PyRIT Attack")).toBeInTheDocument();
292-
expect(screen.getByText("New Attack")).toBeInTheDocument();
291+
// The ribbon no longer shows the "PyRIT Attack" prefix; the target
292+
// badge stands on its own as the leftmost element.
293+
expect(screen.queryByText("PyRIT Attack")).not.toBeInTheDocument();
294+
expect(screen.getByTestId("target-badge")).toBeInTheDocument();
295+
expect(screen.getByRole("button", { name: /new attack/i })).toBeInTheDocument();
293296
expect(screen.getByRole("textbox")).toBeInTheDocument();
294297
});
295298

@@ -321,8 +324,13 @@ describe("ChatWindow Integration", () => {
321324
</TestWrapper>
322325
);
323326

324-
expect(screen.getByText(/OpenAIChatTarget/)).toBeInTheDocument();
325-
expect(screen.getByText(/gpt-4/)).toBeInTheDocument();
327+
// The target badge is the leftmost element. Its visible label
328+
// includes the type and model. The same strings also appear in the
329+
// tooltip body, so we query the badge specifically.
330+
const badge = screen.getByTestId("target-badge");
331+
expect(badge).toHaveTextContent(/OpenAIChatTarget/);
332+
expect(badge).toHaveTextContent(/gpt-4/);
333+
expect(badge).toHaveAttribute("aria-label", expect.stringContaining(mockTarget.target_registry_name));
326334
});
327335

328336
it("should show no-target message when target is null", () => {
@@ -389,8 +397,9 @@ describe("ChatWindow Integration", () => {
389397
</TestWrapper>
390398
);
391399

392-
expect(screen.getByText(/OpenAIChatTarget/)).toBeInTheDocument();
393-
expect(screen.queryByText(/gpt/)).not.toBeInTheDocument();
400+
const badge = screen.getByTestId("target-badge");
401+
expect(badge).toHaveTextContent(/OpenAIChatTarget/);
402+
expect(badge).not.toHaveTextContent(/gpt/);
394403
});
395404

396405
// -----------------------------------------------------------------------

frontend/src/components/Chat/ChatWindow.tsx

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import { useState, useRef, useEffect, useCallback } from 'react'
22
import {
33
Button,
44
Text,
5-
Badge,
65
Tooltip,
76
} from '@fluentui/react-components'
87
import { AddRegular, PanelRightRegular } from '@fluentui/react-icons'
98
import MessageList from './MessageList'
109
import ChatInputArea from './ChatInputArea'
1110
import ConversationPanel from './ConversationPanel'
1211
import ConverterPanel from './ConverterPanel'
12+
import TargetBadge from './TargetBadge'
1313
import type { PieceConversion } from './converterTypes'
1414
import { PIECE_TYPE_TO_DATA_TYPE, basenameFromValue, buildMediaUrl, dataTypeToAttachmentKind, isPathDataType } from './converterTypes'
1515
import LabelsBar from '../Labels/LabelsBar'
@@ -553,17 +553,8 @@ export default function ChatWindow({
553553
<div className={styles.chatArea}>
554554
<div className={styles.ribbon}>
555555
<div className={styles.conversationInfo}>
556-
<Text>PyRIT Attack</Text>
557556
{activeTarget ? (
558-
<div className={styles.targetInfo}>
559-
<Text size={200}></Text>
560-
<Tooltip content={activeTarget.target_registry_name} relationship="label">
561-
<Badge appearance="outline" size="medium">
562-
{activeTarget.target_type}
563-
{activeTarget.model_name ? ` (${activeTarget.model_name})` : ''}
564-
</Badge>
565-
</Tooltip>
566-
</div>
557+
<TargetBadge target={activeTarget} />
567558
) : (
568559
<Text size={200} className={styles.noTarget}>
569560
No target selected
@@ -584,15 +575,19 @@ export default function ChatWindow({
584575
aria-label="Toggle conversations panel"
585576
/>
586577
</Tooltip>
587-
<Button
588-
appearance="primary"
589-
icon={<AddRegular />}
590-
onClick={() => { setIsPanelOpen(false); onNewAttack() }}
591-
disabled={!attackResultId}
592-
data-testid="new-attack-btn"
593-
>
594-
New Attack
595-
</Button>
578+
<Tooltip content="New Attack" relationship="label">
579+
<Button
580+
appearance="primary"
581+
icon={<AddRegular />}
582+
onClick={() => { setIsPanelOpen(false); onNewAttack() }}
583+
disabled={!attackResultId}
584+
data-testid="new-attack-btn"
585+
aria-label="New Attack"
586+
className={styles.newAttackButton}
587+
>
588+
<span className={styles.newAttackLabel}>New Attack</span>
589+
</Button>
590+
</Tooltip>
596591
</div>
597592
</div>
598593
<MessageList
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { makeStyles, tokens } from '@fluentui/react-components'
2+
3+
export const useTargetBadgeStyles = makeStyles({
4+
badge: {
5+
display: 'inline-flex',
6+
alignItems: 'center',
7+
gap: tokens.spacingHorizontalXS,
8+
padding: `2px ${tokens.spacingHorizontalS}`,
9+
borderRadius: tokens.borderRadiusMedium,
10+
border: `1px solid ${tokens.colorNeutralStroke1}`,
11+
backgroundColor: tokens.colorNeutralBackground1,
12+
cursor: 'help',
13+
minWidth: 0,
14+
maxWidth: '100%',
15+
},
16+
badgeText: {
17+
overflow: 'hidden',
18+
textOverflow: 'ellipsis',
19+
whiteSpace: 'nowrap',
20+
minWidth: 0,
21+
},
22+
// Applied to the Fluent Tooltip's `content` slot (the actual surface
23+
// that renders the white/dark popover with the arrow). Fluent caps
24+
// surface max-width at 240px by default, which truncates anything
25+
// wider than a short label. We override here so the surface grows
26+
// with its content, capped only by the viewport.
27+
tooltipSurface: {
28+
maxWidth: 'min(800px, calc(100vw - 64px))',
29+
width: 'max-content',
30+
minWidth: '420px',
31+
},
32+
tooltipBody: {
33+
display: 'flex',
34+
flexDirection: 'column',
35+
gap: tokens.spacingVerticalS,
36+
width: '100%',
37+
boxSizing: 'border-box',
38+
},
39+
tooltipHeader: {
40+
display: 'flex',
41+
flexDirection: 'column',
42+
gap: tokens.spacingVerticalXXS,
43+
},
44+
tooltipSection: {
45+
display: 'flex',
46+
flexDirection: 'column',
47+
gap: tokens.spacingVerticalXXS,
48+
minWidth: 0,
49+
},
50+
sectionLabel: {
51+
fontSize: tokens.fontSizeBase100,
52+
fontWeight: tokens.fontWeightSemibold,
53+
color: tokens.colorNeutralForeground3,
54+
textTransform: 'uppercase',
55+
letterSpacing: '0.05em',
56+
},
57+
endpointText: {
58+
fontFamily: tokens.fontFamilyMonospace,
59+
fontSize: tokens.fontSizeBase200,
60+
overflowWrap: 'anywhere',
61+
},
62+
flagsRow: {
63+
display: 'flex',
64+
flexWrap: 'wrap',
65+
gap: tokens.spacingHorizontalXS,
66+
},
67+
paramsBlock: {
68+
margin: 0,
69+
padding: tokens.spacingHorizontalXS,
70+
backgroundColor: tokens.colorNeutralBackground2,
71+
borderRadius: tokens.borderRadiusSmall,
72+
fontFamily: tokens.fontFamilyMonospace,
73+
fontSize: tokens.fontSizeBase200,
74+
whiteSpace: 'pre-wrap',
75+
wordBreak: 'normal',
76+
overflowWrap: 'anywhere',
77+
maxHeight: '200px',
78+
maxWidth: '100%',
79+
overflowY: 'auto',
80+
overflowX: 'auto',
81+
boxSizing: 'border-box',
82+
},
83+
})

0 commit comments

Comments
 (0)