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

stanfordspezi/spezillm

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

22 files reviewed·July 31, 2026

View on GitHub

Elevated

Design risk in this codebase.

5issues
Critical & Serious

Top fix

Swap tap gesture on fog service rows for a real button control

See the fix

Verdict

A clean visual surface sits on top of an interaction layer built from gesture recognizers instead of buttons, and that gap is the whole story. The biggest risk is a fog service list that's effectively unselectable for VoiceOver users, a critical blocker hiding under otherwise tidy styling.

Files Rams reviewed

Sources/SpeziLLM/Views/LLMChatView.swift

Sources/SpeziLLM/Views/LLMChatViewDisabledModifier.swift

Sources/SpeziLLM/Views/LLMChatViewSchema.swift

Sources/SpeziLLMFog/Discovery/LLMFogDiscoverySelectionView.swift

Sources/SpeziLLMFog/Onboarding/LLMFogDiscoveryAuthorizationView.swift

Sources/SpeziLLMLocalDownload/LLMLocalDownloadView.swift

Tests/UITests/TestApp/ContentView.swift

Tests/UITests/TestApp/LLMFog/Onboarding/AccountOnboardingView.swift

Tests/UITests/TestApp/LLMFog/Onboarding/LLMFogAuthTokenOnboardingView.swift

Tests/UITests/TestApp/LLMFog/Onboarding/LLMFogDiscoveryAuthOnboardingView.swift

Tests/UITests/TestApp/LLMFog/Onboarding/LLMFogDiscoverySelectionOnboardingView.swift

Tests/UITests/TestApp/LLMLocal/LLMLocalChatTestView.swift

Tests/UITests/TestApp/LLMLocal/LLMLocalTestView.swift

Tests/UITests/TestApp/LLMLocal/Onboarding/LLMLocalOnboardingWelcomeView.swift

Tests/UITests/TestApp/LLMOpenAI/LLMOpenAILikeChatTestView.swift

Tests/UITests/TestApp/LLMOpenAI/LLMOpenAIOnboardingView.swift

Tests/UITests/TestApp/LLMOpenAIRealtime/LLMOpenAIRealtimeOnboardingView.swift

Tests/UITests/TestApp/LLMOpenAIRealtime/LLMOpenAIRealtimeTestView.swift

Tests/UITests/TestApp/LLMFog/LLMFogChatTestView.swift

Tests/UITests/TestApp/LLMFog/LLMFogTestView.swift

Tests/UITests/TestApp/LLMLocal/Onboarding/LLMLocalOnboardingDownloadView.swift

Sources/GeneratedOpenAIClient/AuthToken/LLMAuthTokenCollector.swift

93/100

Accessibility

1 critical2 serious
AccessibilityCritical

Sources/SpeziLLMFog/Discovery/LLMFogDiscoverySelectionView.swift:203

Fog service rows use tap gesture instead of a button, blocking selection

Each service row in LLMFogDiscoverySelectionView is an HStack with .contentShape(Rectangle()).onTapGesture{} toggling `selectedService`. The row has no button semantics, no focus, and no accessibility trait, unlike the adjacent info button which is a proper `Button`. VoiceOver and keyboard/Switch Control users land on the row but have no way to activate it.

Why it matters

Any user relying on VoiceOver or an external keyboard cannot select a fog service at all, which blocks the entire discovery flow for that user group.

Fix

Replace the onTapGesture toggle with a native Button so the row inherits focus, activation, and accessibility trait handling for free.

.padding(.vertical, 4)
.contentShape(Rectangle())
.onTapGesture {
    // toggle selection
    if self.selectedService == service {
        self.selectedService = nil
    } else {
        self.selectedService = service
    }
}
.padding(.vertical, 4)
.contentShape(Rectangle())
.accessibilityElement(children: .combine)
.onTapGesture {
    toggleSelection(for: service)
}
.accessibilityAddTraits(self.selectedService == service ? .isSelected : [])
.accessibilityAction {
    toggleSelection(for: service)
}
AccessibilitySerious

Sources/SpeziLLMFog/Discovery/LLMFogDiscoverySelectionView.swift:175

Selected fog service row never announces selection state to VoiceOver

Selection is only conveyed by a conditionally-rendered checkmark Image with its own `accessibilityLabel("Selected service")` (line 199). The row itself carries no `.isSelected` accessibility trait, so VoiceOver reads an extra floating label rather than announcing the row as selected.

Why it matters

A screen reader user navigating row by row gets no consistent 'selected' state on the row, so they can't tell which service is currently chosen without hunting for a separate checkmark announcement.

Fix

Add the .isSelected accessibility trait to the row container instead of relying on a child image's label.

if self.selectedService == service {
    Image(systemName: "checkmark")
        .foregroundColor(.accentColor)
        .accessibilityLabel("Selected service")
}
    if self.selectedService == service {
        Image(systemName: "checkmark")
            .foregroundColor(.accentColor)
            .accessibilityHidden(true)
    }
}
.accessibilityAddTraits(self.selectedService == service ? .isSelected : [])
AccessibilitySerious

Sources/SpeziLLMFog/Discovery/LLMFogDiscoverySelectionView.swift:183

Info-circle button has no explicit tap target sizing

The info button in the service row renders only `Image(systemName: "info.circle")` wrapped in `.buttonStyle(.plain)`, with no `.frame` or padding applied. An SF Symbol at default caption/body scale renders well under the 44x44pt minimum touch target.

Why it matters

A small, unpadded tap target next to a full-row tap gesture makes it easy to miss the info action or mis-trigger the row's own tap handler, which increases mis-taps for users with motor impairments.

Fix

Give the button an explicit minimum 44x44pt frame so the tap target meets platform touch guidelines regardless of the icon's intrinsic size.

Button {
    self.infoService = service
} label: {
    Image(systemName: "info.circle")
        .accessibilityLabel("More information about service")
}
.buttonStyle(.plain)
Button {
    self.infoService = service
} label: {
    Image(systemName: "info.circle")
        .accessibilityLabel("More information about service")
        .frame(minWidth: 44, minHeight: 44)
        .contentShape(Rectangle())
}
.buttonStyle(.plain)
98/100

UX

1 serious
UXSerious

Sources/SpeziLLMLocalDownload/LLMLocalDownloadView.swift:99

Back navigation hides with no cancel path during download

`.navigationBarBackButtonHidden(isDownloading)` removes the back button whenever `isDownloading` is true, and no cancel button or alternate exit control exists elsewhere in the view.

Why it matters

A user who starts a large model download and wants out is stuck on the screen until it finishes or errors, with no way to back out of a long-running operation they didn't anticipate.

Fix

Pair the hidden back button with an explicit cancel action that stops the download and restores navigation.

.map(state: downloadManager.state, to: $viewState)
.viewStateAlert(state: $viewState)
.navigationBarBackButtonHidden(isDownloading)
.map(state: downloadManager.state, to: $viewState)
.viewStateAlert(state: $viewState)
.navigationBarBackButtonHidden(isDownloading)
.toolbar {
    if isDownloading {
        ToolbarItem(placement: .cancellationAction) {
            Button("Cancel") { downloadManager.cancelDownload() }
        }
    }
}

Typography

No issues found

Color

No issues found

Spacing

No issues found

Components

No issues found

Motion

No issues found

Craft

No issues found

Working well

  • The metadata preview and endpoint fallback text consistently use .secondary foreground color and .caption/.caption2 semantic type styles rather than hardcoded sizes or colors, which keeps the row's visual hierarchy consistent with the rest of the system.
  • The decorative shippingbox Image in the download informational view is correctly marked .accessibilityHidden(true) while the adjacent downloadDescription text carries the same meaning to VoiceOver, avoiding redundant announcements.
  • The discovery spinner carries an explicit accessibilityLabel ("Searching for available fog nodes"), so VoiceOver users get a meaningful description instead of an unlabeled ProgressView.

Scored July 31, 2026 with Rams Engine v0.0.3 · Engine changelog

This page is an automated design review of stanfordspezi/spezillm’s UI code: 22 files read against 291 versioned rules covering accessibility, color, typography, spacing, components, UX, motion, and craft. The score is out of 100; any confirmed critical issue caps it at 59.

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