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
339 changes: 271 additions & 68 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,25 @@
*/

import {
makeStyles,
Subtitle1,
Tab,
TabList,
Text,
tokens,
Button,
makeStyles,
Subtitle1,
Subtitle2,
Text,
tokens,
} from '@fluentui/react-components'
Comment on lines 10 to 17
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import blocks have inconsistent indentation (mix of 2 and 4 spaces) which hurts readability and is likely accidental. Please run the repo formatter (or manually normalize indentation) so the import list is consistently aligned.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f72dd03 — normalized all import indentation in App.jsx to consistent 2-space style (also alphabetically sorted Subtitle1/Subtitle2 while at it).

import {
Bot24Regular,
DataHistogram24Regular,
DocumentEdit24Regular,
Flow24Regular,
Info24Regular,
Pulse24Regular,
Settings24Regular,
Table24Regular,
Wrench24Regular,
Bot24Regular,
ChevronLeft24Regular,
ChevronRight24Regular,
DataHistogram24Regular,
DocumentEdit24Regular,
Flow24Regular,
Info24Regular,
Pulse24Regular,
Settings24Regular,
Table24Regular,
Wrench24Regular,
} from '@fluentui/react-icons'
import { useEffect, useMemo, useState } from 'react'
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'
Expand All @@ -43,31 +45,174 @@ import SettingsPage from './features/settings/SettingsPage'
import useTabPreferences from './features/settings/useTabPreferences'
import { listWorkbenchAgents } from './services/api'

const NAV_COLLAPSED_STORAGE_KEY = 'app-nav-collapsed'

const useStyles = makeStyles({
app: {
minHeight: '100vh',
backgroundColor: tokens.colorNeutralBackground3,
overflowX: 'hidden',
display: 'flex',
flexDirection: 'column',
},
header: {
backgroundColor: tokens.colorBrandBackground,
color: tokens.colorNeutralForegroundOnBrand,
padding: `${tokens.spacingVerticalL} ${tokens.spacingHorizontalXL}`,
boxShadow: tokens.shadow4,
'@media (max-width: 768px)': {
padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalL}`,
},
'@media (max-width: 480px)': {
padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalM}`,
},
},
headerInner: {
maxWidth: '1400px',
margin: '0 auto',
},
title: {
color: tokens.colorNeutralForegroundOnBrand,
overflowWrap: 'anywhere',
},
subtitle: {
color: tokens.colorNeutralForegroundOnBrand,
opacity: 0.9,
marginTop: tokens.spacingVerticalXS,
overflowWrap: 'anywhere',
},
nav: {
shell: {
display: 'flex',
flex: 1,
minHeight: 0,
},
sidebar: {
width: '280px',
flexShrink: 0,
display: 'flex',
flexDirection: 'column',
backgroundColor: tokens.colorNeutralBackground1,
borderRight: `1px solid ${tokens.colorNeutralStroke1}`,
transitionDuration: tokens.durationNormal,
transitionProperty: 'width',
transitionTimingFunction: tokens.curveEasyEase,
minHeight: 0,
'@media (max-width: 768px)': {
width: '232px',
},
},
sidebarCollapsed: {
width: '88px',
'@media (max-width: 768px)': {
width: '72px',
},
},
sidebarHeader: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: tokens.spacingHorizontalS,
padding: `${tokens.spacingVerticalL} ${tokens.spacingHorizontalM}`,
borderBottom: `1px solid ${tokens.colorNeutralStroke1}`,
padding: `0 ${tokens.spacingHorizontalXL}`,
},
sidebarTitle: {
minWidth: 0,
overflow: 'hidden',
},
collapseButton: {
minWidth: '36px',
width: '36px',
height: '36px',
padding: 0,
flexShrink: 0,
},
navList: {
display: 'flex',
flexDirection: 'column',
gap: tokens.spacingVerticalXS,
padding: tokens.spacingHorizontalS,
overflowY: 'auto',
flex: 1,
minHeight: 0,
},
navButton: {
width: '100%',
display: 'flex',
alignItems: 'center',
gap: tokens.spacingHorizontalM,
border: 'none',
borderRadius: tokens.borderRadiusLarge,
backgroundColor: 'transparent',
color: tokens.colorNeutralForeground2,
cursor: 'pointer',
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`,
textAlign: 'left',
transitionDuration: tokens.durationNormal,
transitionProperty: 'background-color, color',
transitionTimingFunction: tokens.curveEasyEase,
':hover': {
backgroundColor: tokens.colorNeutralBackground1Hover,
color: tokens.colorNeutralForeground1,
},
},
navButtonCollapsed: {
justifyContent: 'center',
padding: `${tokens.spacingVerticalS} 0`,
},
navButtonActive: {
backgroundColor: tokens.colorBrandBackground2,
color: tokens.colorBrandForeground1,
boxShadow: `inset 3px 0 0 ${tokens.colorBrandStroke1}`,
':hover': {
backgroundColor: tokens.colorBrandBackground2Hover,
color: tokens.colorBrandForeground1,
},
},
navIcon: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
},
navLabelWrap: {
minWidth: 0,
display: 'flex',
flexDirection: 'column',
gap: '2px',
},
navLabel: {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
navPath: {
color: tokens.colorNeutralForeground4,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
contentArea: {
flex: 1,
minWidth: 0,
minHeight: 0,
display: 'flex',
flexDirection: 'column',
},
content: {
width: '100%',
flex: 1,
minWidth: 0,
minHeight: 0,
padding: `${tokens.spacingVerticalL} ${tokens.spacingHorizontalXL}`,
'@media (max-width: 768px)': {
padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalL}`,
},
'@media (max-width: 480px)': {
padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalM}`,
},
},
contentInner: {
width: '100%',
maxWidth: '1400px',
margin: '0 auto',
},
Expand All @@ -77,8 +222,24 @@ export default function App() {
const styles = useStyles()
const location = useLocation()
const navigate = useNavigate()
const [isNavCollapsed, setIsNavCollapsed] = useState(false)

const [menuAgents, setMenuAgents] = useState([])
useEffect(() => {
const storedValue = window.localStorage.getItem(NAV_COLLAPSED_STORAGE_KEY)
if (storedValue !== null) {
setIsNavCollapsed(storedValue === 'true')
return
}
if (window.innerWidth <= 1024) {
setIsNavCollapsed(true)
}
}, [])

useEffect(() => {
window.localStorage.setItem(NAV_COLLAPSED_STORAGE_KEY, String(isNavCollapsed))
}, [isNavCollapsed])

useEffect(() => {
listWorkbenchAgents()
.then((data) => {
Expand Down Expand Up @@ -136,63 +297,105 @@ export default function App() {
?? allTabs.find((tab) => location.pathname.startsWith(tab.path))?.value
?? 'csvtickets'

const mainNavTabs = navTabs.filter((tab) => tab.value !== 'settings')

const renderNavButton = (tab) => {
const isActive = activeTab === tab.value
return (
<button
key={tab.value}
type="button"
className={`${styles.navButton} ${isNavCollapsed ? styles.navButtonCollapsed : ''} ${isActive ? styles.navButtonActive : ''}`}
onClick={() => navigate(tab.path)}
data-testid={tab.testId}
aria-label={tab.label}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Navigation items no longer expose an active-state attribute. Our Playwright e2e tests (and assistive tech) previously relied on tab semantics (e.g., aria-selected="true" on the active item). Consider restoring equivalent semantics by marking the active button with aria-selected (and adding appropriate role/container role like tab/tablist), or otherwise ensuring there is a stable, testable active-state attribute such as aria-current="page" (and updating tests accordingly).

Suggested change
aria-label={tab.label}
aria-label={tab.label}
aria-current={isActive ? 'page' : undefined}

Copilot uses AI. Check for mistakes.
title={isNavCollapsed ? tab.label : undefined}
>
<span className={styles.navIcon}>{tab.icon}</span>
{!isNavCollapsed && (
<span className={styles.navLabelWrap}>
<Text className={styles.navLabel} weight={isActive ? 'semibold' : 'regular'}>
{tab.label}
</Text>
<Text className={styles.navPath} size={200}>
{tab.path}
</Text>
</span>
)}
</button>
)
}

return (
<div className={styles.app}>
<header className={styles.header}>
<Subtitle1 className={styles.title}>CSV Ticket Viewer</Subtitle1>
<Text className={styles.subtitle} size={300}>
View and filter ticket data from CSV exports
</Text>
<div className={styles.headerInner}>
<Subtitle1 className={styles.title}>CSV Ticket Viewer</Subtitle1>
<Text className={styles.subtitle} size={300}>
View and filter ticket data from CSV exports
</Text>
</div>
</header>

<nav className={styles.nav}>
<TabList
selectedValue={activeTab}
onTabSelect={(_, data) => {
const selected = navTabs.find((tab) => tab.value === data.value)
if (selected) {
navigate(selected.path)
}
}}
size="large"
>
{navTabs.map((tab) => (
<Tab key={tab.value} value={tab.value} icon={tab.icon} data-testid={tab.testId}>
{tab.label}
</Tab>
))}
</TabList>
</nav>

<main className={styles.content}>
<Routes>
<Route path="/" element={<Navigate to="/csvtickets" replace />} />
<Route path="/kba-drafter" element={<KBADrafterPage />} />
<Route path="/csvtickets" element={<CSVTicketTable />} />
{USECASE_DEMO_DEFINITIONS.map((definition) => (
<Route
key={definition.id}
path={definition.route}
element={<UsecaseDemoPage definition={definition} />}
/>
))}
<Route path="/kitchensink" element={<KitchenSink />} />
<Route path="/fields" element={<FieldsDocs />} />
<Route path="/workbench" element={<WorkbenchPage />} />
{menuAgents.map((agent) => (
<Route
key={agent.id}
path={`/agent-run/${agent.id}`}
element={<AgentRunPage agent={agent} />}
<div className={styles.shell}>
<aside className={`${styles.sidebar} ${isNavCollapsed ? styles.sidebarCollapsed : ''}`}>
<div className={styles.sidebarHeader}>
{!isNavCollapsed && (
<div className={styles.sidebarTitle}>
<Subtitle2>Navigation</Subtitle2>
<Text size={200}>Main sections</Text>
</div>
)}
<Button
className={styles.collapseButton}
appearance="subtle"
icon={isNavCollapsed ? <ChevronRight24Regular /> : <ChevronLeft24Regular />}
onClick={() => setIsNavCollapsed((current) => !current)}
aria-label={isNavCollapsed ? 'Expand navigation' : 'Collapse navigation'}
title={isNavCollapsed ? 'Expand navigation' : 'Collapse navigation'}
/>
))}
<Route path="/agent" element={<AgentChat />} />
<Route path="/activity" element={<ActivityPage />} />
<Route path="/workflow" element={<WorkflowPage />} />
<Route path="/settings" element={<SettingsPage tabPrefs={tabPrefs} />} />
<Route path="*" element={<Navigate to="/csvtickets" replace />} />
</Routes>
</main>
</div>

<nav className={styles.navList} aria-label="Main navigation">
{mainNavTabs.map(renderNavButton)}
{renderNavButton(settingsTab)}
</nav>
</aside>

<div className={styles.contentArea}>
<main className={styles.content}>
<div className={styles.contentInner}>
<Routes>
<Route path="/" element={<Navigate to="/csvtickets" replace />} />
<Route path="/kba-drafter" element={<KBADrafterPage />} />
<Route path="/csvtickets" element={<CSVTicketTable />} />
{USECASE_DEMO_DEFINITIONS.map((definition) => (
<Route
key={definition.id}
path={definition.route}
element={<UsecaseDemoPage definition={definition} />}
/>
))}
<Route path="/kitchensink" element={<KitchenSink />} />
<Route path="/fields" element={<FieldsDocs />} />
<Route path="/workbench" element={<WorkbenchPage />} />
{menuAgents.map((agent) => (
<Route
key={agent.id}
path={`/agent-run/${agent.id}`}
element={<AgentRunPage agent={agent} />}
/>
))}
<Route path="/agent" element={<AgentChat />} />
<Route path="/activity" element={<ActivityPage />} />
<Route path="/workflow" element={<WorkflowPage />} />
<Route path="/settings" element={<SettingsPage tabPrefs={tabPrefs} />} />
<Route path="*" element={<Navigate to="/csvtickets" replace />} />
</Routes>
</div>
</main>
</div>
</div>
</div>
)
}
Loading