Rams MCP · The full engine, now in your coding agent
chatwoot on GitHub

chatwoot/chatwoot

Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.

30 files reviewed·August 1, 2026

View on GitHub

Low

Design risk in this codebase.

5issues
Serious & Moderate · top 4 shown below

Top fix

Show an error state when agent or inbox fetches fail

See the fix

Verdict

Good interaction logic wrapped in unfinished edges: solid v-model wiring undercut by silent failure states and disconnected labels. The biggest risk is invisible failure, forms that go blank or icons that convey status to no one but sighted, successful users.

Files Rams reviewed

app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentAssignmentPolicyForm.vue

app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentCapacityPolicyForm.vue

app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/ConfirmInboxDialog.vue

app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/InboxLinkDialog.vue

app/javascript/v3/views/auth/signup/components/Signup/Form.vue

app/javascript/v3/views/auth/signup/components/Signup/PasswordRequirements.vue

app/javascript/v3/views/auth/signup/components/Testimonials/Index.vue

app/javascript/v3/views/auth/signup/components/Testimonials/TestimonialCard.vue

app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/ConfirmDeletePolicyDialog.vue

app/javascript/dashboard/routes/dashboard/calls/pages/CallsIndex.vue

app/javascript/dashboard/routes/dashboard/campaigns/pages/LiveChatCampaignsPage.vue

app/javascript/dashboard/routes/dashboard/campaigns/pages/SMSCampaignsPage.vue

app/javascript/dashboard/routes/dashboard/campaigns/pages/WhatsAppCampaignsPage.vue

app/javascript/dashboard/routes/dashboard/captain/pages/AssistantsIndexPage.vue

app/javascript/dashboard/routes/dashboard/companies/pages/CompaniesIndex.vue

app/javascript/dashboard/routes/dashboard/companies/pages/CompanyDetailView.vue

app/javascript/dashboard/routes/dashboard/contacts/pages/ContactManageView.vue

app/javascript/dashboard/routes/dashboard/helpcenter/pages/HelpCenterPageRouteView.vue

app/javascript/dashboard/routes/dashboard/helpcenter/pages/PortalsArticlesEditPage.vue

app/javascript/dashboard/routes/dashboard/helpcenter/pages/PortalsArticlesIndexPage.vue

app/javascript/dashboard/routes/dashboard/helpcenter/pages/PortalsArticlesNewPage.vue

app/javascript/dashboard/routes/dashboard/helpcenter/pages/PortalsCategoriesIndexPage.vue

app/javascript/dashboard/routes/dashboard/helpcenter/pages/PortalsIndexPage.vue

app/javascript/dashboard/routes/dashboard/helpcenter/pages/PortalsLocalesIndexPage.vue

app/javascript/dashboard/routes/dashboard/helpcenter/pages/PortalsSettingsIndexPage.vue

app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentCreatePage.vue

app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentEditPage.vue

app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentAssignmentIndexPage.vue

app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentCapacityCreatePage.vue

app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/AgentCapacityEditPage.vue

96/100

Accessibility

2 serious
AccessibilitySerious

app/javascript/v3/views/auth/signup/components/Signup/PasswordRequirements.vue:58

Requirement status icon carries no accessible text for met/unmet state

The Icon at line 58 switches between 'i-lucide-circle-check-big' and 'i-lucide-circle' with color changes to convey met/unmet status, but has no aria-hidden or accompanying sr-only text stating the status. The adjacent span only renders the requirement's label text (e.g. 'must contain a number'), never the word 'met' or 'unmet'.

Why it matters

A screen reader user hears only the requirement description, never whether it's satisfied, so they can't confirm password validity without switching to sighted verification or trial-and-error submission.

Fix

Hide decorative status icons from assistive tech and add a visually-hidden text alternative that states the actual met/unmet condition.

<Icon
  class="flex-none flex-shrink-0 w-3 mt-0.5"
  :icon="item.met ? 'i-lucide-circle-check-big' : 'i-lucide-circle'"
  :class="item.met ? 'text-n-teal-10' : 'text-n-slate-10'"
/>
<span :class="item.met ? 'text-n-slate-11' : 'text-n-slate-10'">
  {{ item.label }}
</span>
<Icon
  aria-hidden="true"
  class="flex-none flex-shrink-0 w-3 mt-0.5"
  :icon="item.met ? 'i-lucide-circle-check-big' : 'i-lucide-circle'"
  :class="item.met ? 'text-n-teal-10' : 'text-n-slate-10'"
/>
<span :class="item.met ? 'text-n-slate-11' : 'text-n-slate-10'">
  <span class="sr-only">{{ item.met ? 'Met: ' : 'Not met: ' }}</span>
  {{ item.label }}
</span>
AccessibilitySerious

app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentCapacityPolicyForm.vue:165

Users section label has no programmatic link to its dropdown control

The label rendering `t(`${BASE_KEY}.FORM.USERS.LABEL`)` at line 165 is a bare <label> with no 'for' attribute, and it doesn't wrap the AddDataDropdown or DataTable it visually describes. It sits in the DOM near the control but isn't connected to it.

Why it matters

Screen reader users navigating by form control get no announced label when they land on the AddDataDropdown, so they can't tell what the control adds without extra exploration.

Fix

Associate every label with its control via a matching id/for pair or aria-labelledby so assistive tech announces the relationship.

<label class="text-sm font-medium text-n-slate-12 py-1">
  {{ t(`${BASE_KEY}.FORM.USERS.LABEL`) }}
</label>
<label id="users-section-label" class="text-sm font-medium text-n-slate-12 py-1">
  {{ t(`${BASE_KEY}.FORM.USERS.LABEL`) }}
</label>
98/100

Components

1 serious
ComponentsSerious

app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentCapacityPolicyForm.vue:11

Unused 'enabled' field bloats the policy form's default data shape

The initialData prop's default object includes an 'enabled' field alongside name, description, exclusionRules, and inboxCapacityLimits, but nothing in the component's reactive state, watchers, or submit payload reads or writes it. It's dead surface area on a prop that callers may copy verbatim when wiring new consumers.

Why it matters

Future maintainers editing this form assume 'enabled' does something because it's typed into the default shape, so debugging time goes into a field the component never touches, and the confusion compounds every time the form is extended.

Fix

Remove unused fields from a prop's default shape so the interface only advertises data the component actually consumes.

default: () => ({
  name: '',
  description: '',
  enabled: false,
  exclusionRules: {
    excludedLabels: [],
    excludeOlderThanHours: null,
  },
  inboxCapacityLimits: [],
}),
default: () => ({
  name: '',
  description: '',
  exclusionRules: {
    excludedLabels: [],
    excludeOlderThanHours: null,
  },
  inboxCapacityLimits: [],
}),

Get this score on every PR.

Rams reviews each pull request on your repo and posts inline one-click fixes — about a minute per review.

Install Rams free
98/100

UX

1 serious
UXSerious

app/javascript/dashboard/routes/dashboard/settings/assignmentPolicy/pages/components/AgentCapacityPolicyForm.vue:178

Agent and inbox fetch failures render nothing, leaving the form silently blank

InboxCapacityLimits receives :is-fetching="isInboxesLoading" and the users section only branches on showUserSection with an empty-state message; there's no error path if the underlying agent or inbox fetch fails. A failed request looks identical to a loading state that never resolves or an empty list that has no data.

Why it matters

A user configuring capacity limits sees a stalled or empty section with no explanation, so they can't tell whether to wait, retry, or report a bug, and support tickets increase for what is actually a network failure.

Fix

Add an explicit error state alongside loading and empty states so failed fetches are distinguishable and recoverable.

<InboxCapacityLimits
  v-if="showInboxLimitSection"
  v-model:inbox-capacity-limits="state.inboxCapacityLimits"
  :inbox-list="inboxList"
  :is-fetching="isInboxesLoading"
  @delete="handleDeleteInboxLimit"
  @add="handleAddInboxLimit"
  @update="handleLimitChange"
/>
<InboxCapacityLimits
  v-if="showInboxLimitSection"
  v-model:inbox-capacity-limits="state.inboxCapacityLimits"
  :inbox-list="inboxList"
  :is-fetching="isInboxesLoading"
  :has-error="isInboxesFetchError"
  :error-message="t(`${BASE_KEY}.FORM.INBOXES.FETCH_ERROR`)"
  @delete="handleDeleteInboxLimit"
  @add="handleAddInboxLimit"
  @update="handleLimitChange"
/>

Typography

No issues found

Color

No issues found

Spacing

No issues found

Motion

No issues found

Craft

No issues found

Working well

  • The excludeOlderThanMinutes computed with its get/set bridging hours-to-minutes is a clean adapter that keeps DurationInput's minute-based API decoupled from the policy's hour-based storage, so each side stays honest about its own unit without leaking conversion logic into the template.
  • detectExclusionUnit choosing DAYS vs HOURS based on whether the stored value divides evenly by 24 avoids silently flooring a 25-hour threshold down to 1 day on load, which is exactly the kind of quiet data-loss bug this kind of unit-detection guard exists to prevent.
  • The password field's focus-driven PasswordRequirements popover appears exactly when the user needs guidance and disappears on blur, keeping the requirements out of permanent layout space while still being discoverable at the moment they matter.
  • Met/unmet requirements are distinguished by both icon shape (circle-check-big vs circle) and color, so the signal doesn't rely on color alone for sighted users, even though the screen reader path still needs the text alternative noted above.

Scored August 1, 2026 with Rams Engine v0.0.3 · Engine changelog
First scored July 9, 2026: 92/100. This rescore on v0.0.3: 92/100.

This page is an automated design review of chatwoot/chatwoot’s UI code: 30 files read against 291 versioned rules covering accessibility, color, typography, spacing, components, UX, motion, and craft. The score is out of 100; confirmed criticals cap it — one at 59, two at 49, three or more at 39.

More design scores

Score your own repo.

Free on public repos, no account. The same engine that scored this page reads your UI code and mints a score page like this one.

Public repos only. The full engine reviews the UI code and mints a public score page — we email you the link too. Already-scored repos open instantly.

Or get a design review on every pull requestInstall Rams