# UX Audit — Vite Flare Starter — 2026-04-18

**Mode:** Standard (walkthrough + QA sweep)
**Tester:** Claude Opus 4.7 via Chrome MCP (Jez's real session)
**Live URL:** https://vite-flare-starter.webfonts.workers.dev
**Versions tested:** `cf609f07`, `5f402d43`, `c56c35e` (today's chat-UX session)
**Models active:** 16 models across 10 providers; `SEARCH_PROVIDER=exa`

## Summary

Overall: **solid and polished**. Today's big additions (artifact sidebar, artifact lightbox, conversation summaries, starred conversations, thin scrollbars, web-search card, screenshot countdown + capture-steps) all render correctly with no runtime errors. No criticals found. Three mediums, four lows, three minor polish opportunities. The app feels like a real product, not a starter demo.

## Findings

### Critical — 0

*None.*

### High — 0

*None.*

### Medium — 3

#### M1. Conversation FTS search only matches titles
**Where:** `GET /api/conversations/search` (fallback branch)
**Observed:** Searching "claude" returns one result with `role: 'title'` — no matches against message bodies. The route's try-block hits the `conversation_messages_fts` table, but that FTS virtual table hasn't been created on this D1 database, so every search silently falls back to LIKE-on-title.
**Evidence:** API returns `{ results: [{ role: 'title' }] }` only. See `src/server/modules/conversations/routes.ts:36-77`.
**Fix:** Run `createFTSIndex(db, { table: 'conversation_messages', columns: ['parts'], ftsTable: 'conversation_messages_fts' })` once on boot, or from an init/migration endpoint. CLAUDE.md already documents the pattern. Fork users expect this to work out of the box.

#### M2. Summarise model (Kimi K2.5) occasionally echoes title as summary
**Where:** `POST /api/conversations/:id/summarise`
**Observed:** Today's defensive de-dupe already drops the summary when it equals or starts with the title, so the surface bug is resolved. But when the summary does clear de-dupe, it sometimes reads as a near-paraphrase ("Fulfilled the request by generating a Mermaid diagram illustrating the iteration cycle and rendering a solid blue vector"). Fine, but not tight.
**Fix:** Either (a) use Haiku 4.5 via OpenRouter for ~1k tokens/request which produces sharper summaries, or (b) retry Kimi with a stronger "don't paraphrase the title" line when the summary contains >60% of title words. Lean (a) — it's one line of config.

#### M3. Chat sidebar width feels cramped for wrapped titles + summaries
**Where:** `ConversationSidebar.tsx` — `w-64` (256px)
**Observed:** After switching to `line-clamp-2` today, entries are now taller. The starred row "Build and SVG loop" with a 120-char summary occupies ~5 lines. Fine, but at 256px wide the second line of the summary always wraps awkwardly.
**Fix:** Widen to `w-72` (288px) — matches the artifact sidebar we already have on the right. Or: render the summary as one line with `truncate` (single line) but title at `line-clamp-2`. Jez's call — symmetry vs density.

### Low — 4

#### L1. "Command Palette" DialogTitle is in DOM even when palette is closed
**Observed:** `document.querySelectorAll('h1,h2,h3')` finds "Command Palette" before the real page H1. The Dialog stays mounted; its title is `sr-only` but screen readers still encounter it first.
**Fix:** Let shadcn's Dialog primitive portal-mount only when open, or wrap the title in `{open && <DialogTitle>…}`. Minor.

#### L2. Narrow dark "gutter" strip between main nav and page content on non-chat pages
**Observed:** On `/dashboard/files`, a narrow empty vertical strip appears between the main dashboard nav and the Files content. Likely a leftover layout placeholder from the chat sidebar drawer column.
**Fix:** Check DashboardLayout — the chat sidebar slot should collapse to 0 width outside the chat route, not render as empty column.

#### L3. Models endpoint doesn't include recommended in response body I queried
**Observed:** `fetch('/api/ai/models').then(j => j.recommended)` returned `undefined` in one test. May be a selector typo on my end; the code looks correct. Worth re-checking.

#### L4. Capture-steps transcribe fails gracefully on silent audio but returns 500 rather than `{ text: "" }`
**Where:** `src/server/modules/audio/routes.ts`
**Observed:** Sending a 1-second pure-silence WAV returns `500 {"error":"transcription failed"}`. Deepgram Nova 3 legitimately errors on silence, but from the client POV a zero-signal recording should probably be "no transcript" rather than "error".
**Fix:** Catch the upstream error, inspect for "no speech detected" / similar markers, return `{ text: "" }` with status 200. Client already tolerates empty strings (no prefix added to textarea).

### Polish — 3

#### P1. Artifact sidebar card has no active/selected state
**Observed:** After clicking a card + the scroll-highlight flashes on the inline artifact, there's no persistent indicator of which artifact is "current". If a conversation has 6 artifacts scrolled far apart, users lose their place after scrolling.
**Fix:** Track last-clicked artifact id; apply a subtle `ring-1 ring-primary/30` on the sidebar card for ~10s after click.

#### P2. Starred section is always expanded — no way to collapse
**Observed:** The date-group rows (Today / Yesterday / Last 7 days) all have chevron collapse toggles. The new Starred group uses the same toggle button but `collapsedGroups` localStorage state works on label, so it should collapse. Verified it does. Low-priority: consider preserving the starred section always-expanded by default (it's the user's most important group).

#### P3. Greeting time-of-day logic is evening-bias
**Observed:** In `greetingForTime()` in ChatPage, anything >= 18:00 → "Good evening" but so is anything < 5:00. Both are correct, just noting the boundary: at 4:30am you get "Good evening, Jeremy" which feels slightly off. Very minor.

## Flows verified

| Flow | Status |
|------|--------|
| New chat: greeting + chips + starter click fills textarea | Pass |
| Star toggle + sidebar reorder | Pass |
| Hover-visible star + always-visible when starred | Pass |
| Artifact sidebar: cards render, Download all, close X | Pass |
| Artifact lightbox: expand icon opens 944px dialog, ESC closes | Pass |
| Scroll-to highlight on card click | Pass |
| Web search card: globe + query + "N results" + collapse chevron | Pass |
| Web search favicons (light tile fix works for monochrome icons) | Pass |
| Models endpoint: 16 models, 4/5/5/2 cost-tier distribution | Pass |
| Conversation summarise: distinct title + summary | Pass |
| Other pages: /dashboard, /extract, /activity, /files, /components | Pass (render OK) |
| Console: zero errors during session | Pass |
| Scrollbars: thin pill style loaded globally | Pass |

## Non-issues verified

- Artifact anchors (`data-artifact-id`) work for both artifact tool results AND user file parts.
- The 45s capture-steps cap is honoured client-side.
- Kimi K2.5 is being used for summaries (free via Workers AI), not burning OpenRouter credits.
- Star button's `aria-pressed` flips correctly; screen reader users get feedback.
- Lightbox `sm:max-w-*` override lesson from CLAUDE.md/`css-patterns.md` was correctly applied.

## Recommended next moves

Ordered by user impact per hour of effort:

1. **M1 (FTS index creation)** — 20 min. Huge search UX win. One-time migration / init call.
2. **M3 (widen sidebar)** — 2 min. Instantly more breathable.
3. **L4 (transcribe 500 → 200 empty)** — 10 min. Makes capture-steps more robust when users don't narrate.
4. **L2 (narrow gutter strip)** — debug CSS, likely a `flex` leak. 10-15 min.
5. **P1 (active state on artifact card)** — 15 min. Nice-to-have, improves wayfinding.

Skip / defer:

- **M2 (summary quality)** — only matters when summaries actually differ from titles, and de-dupe already hides the bad case. Revisit if Jez wants to pay for Haiku summarisation.
- **L1 (Command Palette H2)** — accessibility polish, not visible.
- **P2, P3** — both subjective; act only if users complain.

## What's *working well*

Worth noting the wins so we don't regress them:

- **Scrollbars**: the claude.ai-style thin pill is a dramatic visual upgrade — the app immediately feels less "default shadcn".
- **Starred row visual hierarchy**: the yellow star is the only bright accent in the sidebar, and it's genuinely useful as a wayfinding anchor.
- **Artifact lightbox**: `max-w-[min(80vw,1200px)]` with the `sm:` prefix is the right default. Big enough for dashboards, respects small viewports.
- **Web-search card**: favicons sell it. Dropping the generic tool accordion and going straight to visual cards was the right call.
- **Capture-steps frame-grid idea**: the model pays for text tokens, not video tokens; the storyboard image carries the time dimension without the bandwidth cost.

---

*Report generated via /dev-tools:ux-audit in Standard mode. Total walkthrough + sweep time: ~15 minutes.*
