flowiseai/flowise
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·July 9, 2026
More findings
Verdict
Solid MUI structure keeps getting undercut by icon-only controls with no accessible names and dead-end error states. The sidebar toggle's missing accessible name is the clearest sign that accessibility is an afterthought, not a practice.
Files Rams reviewed
packages/agentflow/src/features/canvas/components/AgentflowHeader.tsx
packages/agentflow/src/features/canvas/components/ConnectionLine.tsx
packages/agentflow/src/features/canvas/components/NodeIcon.tsx
packages/agentflow/src/features/canvas/components/NodeInputHandle.tsx
packages/agentflow/src/features/canvas/components/NodeModelConfigs.tsx
packages/agentflow/src/features/canvas/components/NodeOutputHandles.tsx
packages/agentflow/src/features/canvas/components/NodeStatusIndicator.tsx
packages/agentflow/src/features/canvas/components/NodeToolIcons.tsx
packages/agentflow/src/features/canvas/components/NodeToolbarActions.tsx
packages/agentflow/src/features/canvas/components/ValidationFeedback.tsx
packages/observe/src/features/executions/components/ChatMessageBubble.tsx
packages/observe/src/features/executions/components/ExecutionDetail.tsx
packages/observe/src/features/executions/components/ExecutionTreeSidebar.tsx
packages/observe/src/features/executions/components/ExecutionsListTable.tsx
packages/observe/src/features/executions/components/ExecutionsViewer.tsx
packages/observe/src/features/executions/components/FulfilledConditionsBlock.tsx
packages/observe/src/features/executions/components/HitlPanel.tsx
packages/observe/src/features/executions/components/NodeContentRenderer.tsx
packages/observe/src/features/executions/components/NodeExecutionDetail.tsx
packages/observe/src/features/executions/components/RawJsonPanel.tsx
packages/observe/src/features/executions/components/ToolAccordionList.tsx
packages/observe/src/features/executions/components/UsedToolChips.tsx
packages/ui/src/views/assistants/custom/CustomAssistantLayout.jsx
packages/ui/src/views/assistants/openai/OpenAIAssistantLayout.jsx
packages/ui/src/index.jsx
packages/ui/src/layout/MainLayout/Header/WorkspaceSwitcher/index.jsx
packages/ui/src/layout/MainLayout/Header/index.jsx
packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavCollapse/index.jsx
packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.jsx
packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavItem/index.jsx
UX
Empty state gives no way to add the first assistant
When there are no custom assistants, the empty state renders the `AssistantEmptySVG` image and a plain `<div>No Custom Assistants Added Yet</div>` with no action. - No button, no link back to `addNew` - User must scroll back up to the header's "Add" button to act
Why it matters
New users land on a dead end with no next step visible in the same view, which increases onboarding friction right at the moment they most need direction.
Fix
Give the empty state its own call-to-action wired to the same addNew handler as the header button.
<div>No Custom Assistants Added Yet</div>
</Stack> <div>No Custom Assistants Added Yet</div>
<StyledPermissionButton permissionId={'assistants:create'} variant='contained' sx={{ mt: 2, borderRadius: 2 }} onClick={addNew} startIcon={<IconPlus />}>
Add New Custom Assistant
</StyledPermissionButton>
</Stack>Error dialog offers only Logout, no way to retry the switch
The "Workspace Switch Error" dialog shows the error message in a Typography, then a single DialogActions button reading "Logout". The dialog also sets `disableEscapeKeyDown` and `disableBackdropClick`, so this is the only exit.
Why it matters
A transient network error during a workspace switch now forces every user out of the app entirely. Users who hit a recoverable failure lose their session instead of just retrying the action.
Fix
Give error dialogs a retry path alongside any destructive fallback action.
<DialogActions>
<Button onClick={handleLogout} variant='contained' color='primary'>
Logout
</Button>
</DialogActions><DialogActions>
<Button onClick={() => setShowErrorDialog(false)} variant='outlined'>
Try again
</Button>
<Button onClick={handleLogout} variant='contained' color='primary'>
Logout
</Button>
</DialogActions>Sidebar crashes with no fallback when no 'primary' group exists
`renderPrimaryItems()` calls `item.children.find((child) => child.id === 'primary')` and immediately reads `.children` off the result with no null check. If a caller passes a menu config without a `primary` group, this throws and the entire sidebar fails to render.
Why it matters
A single malformed or edited menu config takes down navigation for every user on the page, with no error boundary or empty state to recover from.
Fix
Guard the lookup and return an empty array when no primary group is found.
const renderPrimaryItems = () => {
const primaryGroup = item.children.find((child) => child.id === 'primary')
return primaryGroup.children
}const renderPrimaryItems = () => {
const primaryGroup = item.children.find((child) => child.id === 'primary')
return primaryGroup?.children ?? []
}Accessibility
Sidebar toggle avatar has no accessible name for screen readers
The menu toggle is an `Avatar` wrapped in `ButtonBase`, showing only `<IconMenu2 />` with `onClick={handleLeftDrawerToggle}`. No `aria-label` names the action.
Why it matters
Screen reader users hear only "button" with no indication it opens the sidebar, so a primary navigation control becomes unusable without sight.
Fix
Give icon-only interactive controls an aria-label describing the action.
onClick={handleLeftDrawerToggle}
color='inherit'
>
<IconMenu2 stroke={1.5} size='1.3rem' />onClick={handleLeftDrawerToggle}
color='inherit'
aria-label='Toggle sidebar'
>
<IconMenu2 stroke={1.5} size='1.3rem' aria-hidden='true' />Expandable menu button never announces open or closed state
The `ListItemButton` toggles a `Collapse` panel via `handleClick` and swaps chevron icons (`IconChevronUp`/`IconChevronDown`) to show state, but the button itself carries no `aria-expanded` or `aria-controls`.
Why it matters
Screen reader users get no indication the control expands a submenu or whether it's currently open, since the state is conveyed only through a visual icon swap.
Fix
Add aria-expanded bound to the open state and aria-controls pointing at the submenu list.
<ListItemButton
sx={{
borderRadius: `${customization.borderRadius}px`,
mb: 0.5,
alignItems: 'flex-start',
backgroundColor: level > 1 ? 'transparent !important' : 'inherit',
py: level > 1 ? 1 : 1.25,
pl: `${level * 24}px`
}}
selected={selected === menu.id}
onClick={handleClick}
><ListItemButton
sx={{
borderRadius: `${customization.borderRadius}px`,
mb: 0.5,
alignItems: 'flex-start',
backgroundColor: level > 1 ? 'transparent !important' : 'inherit',
py: level > 1 ? 1 : 1.25,
pl: `${level * 24}px`
}}
selected={selected === menu.id}
onClick={handleClick}
aria-expanded={open}
aria-controls={`nav-collapse-${menu.id}`}
>Want this on your repo?
Rams reviews your next PR automatically and posts inline fix suggestions.
Review my public repo for freeComponents
Button radius and height hardcoded inline instead of using a shared style
Both "Load" and "Add" buttons repeat `sx={{ borderRadius: 2, height: 40 }}` inline rather than pulling from a shared button variant or theme token.
Why it matters
Every future button on this page (or a sibling assistant layout) has to remember and re-type the same two magic numbers, so any future sizing change means hunting down every copy instead of updating one place.
Fix
Extract repeated inline sx overrides into a shared constant or theme-level button variant.
<PermissionButton
permissionId={'assistants:create'}
variant='outlined'
onClick={loadExisting}
startIcon={<IconFileUpload />}
sx={{ borderRadius: 2, height: 40 }}
>
Load
</PermissionButton><PermissionButton
permissionId={'assistants:create'}
variant='outlined'
onClick={loadExisting}
startIcon={<IconFileUpload />}
sx={headerButtonSx}
>
Load
</PermissionButton>Typography
Color
Spacing
Motion
Craft
Working well
- Splitting menu rendering into `renderPrimaryItems()` and `renderNonPrimaryGroups()` with a shared `shouldDisplayMenu` permission/display check keeps the visibility logic in one place instead of scattering conditional checks across the JSX. That's the right instinct: one source of truth for 'should this render' means permission changes only need one edit.
- The header conditionally renders WorkspaceSwitcher, OrgWorkspaceBreadcrumbs, and the Upgrade button based on `isEnterpriseLicensed`, `isCloud`, and admin status rather than hiding them with CSS. This keeps the DOM lean per deployment context instead of shipping unused interactive elements to every user.
- The header correctly pairs an outlined "Load" button with a contained "Add" button. This is the right hierarchy: the filled button carries more visual weight for the more common creation path, while the outline keeps the secondary import flow available without competing for attention.
- The loading skeleton uses `Skeleton variant='rounded' height={160}` in the same 3-column grid as the real content, so the layout doesn't jump when data arrives. This is the right pattern: skeletons should mirror the final layout's shape, not just show a generic spinner.
Score your own repo with Rams.
Free on public repos. Rams reviews every pull request for accessibility, color, type, spacing, components, motion, UX, and craft, and posts inline fixes.