Scope: Product UI/UX audit + redesign plan only. No code, backend, API contract, search semantics, model, or navigation-logic changes are proposed for implementation in this pass. The search architecture (Flutter → FastAPI → QU → SearchPlan → MySQL) is treated as fixed and correct. See §26 for the protected list.
Product decision (supersedes the original single-mic recommendation): BOTH voice modes are intentionally retained. The two voice inputs — "Cloud voice" and "On-device voice" — are a deliberate product choice, not dev A/B scaffolding. The redesign keeps both fully operational and independent; it removes only the developer framing (provider/model names, "Native"/"Whisper" wording, and the transcript-provenance line) and re-presents them as two clearly labelled, product-facing input modes. Wherever this document earlier said to "collapse to one mic", read it as: keep both, restyle both, strip the debug framing. (Implemented in Phase 1.)
The backend is a well-architected, safety-first natural-language search engine. The front
end does not present it as one. The app currently opens on a technical database registry
(RallyStreamsPage — "Database Video Player Registry"), and the AI search — the actual
product — is a secondary screen reached by an app-bar button. When you do reach it, the
search experience leaks its own internals: two visible voice buttons ("Native Voice" /
"Cloud Whisper"), a cost + latency telemetry chip ("852ms · $0.0003") opening a dialog
with provider name, model, token counts, entity-resolution timings, and raw SearchQuery
JSON, and an interpreted-summary string built in schema shape
(Search Video Actions (jump) | Driver: … | Rally: …).
None of this is a backend problem. The pipeline does the right thing; the UI narrates the plumbing instead of the answer. The redesign goal is to make one search-first surface the whole product, hide every internal concept behind it, and give each of the nine result intents a purpose-built card rather than a generic one (this part is already largely done and is a genuine strength — see §3).
Three things to fix first
- Make search the front door (promote
GeneralSearchScreen, demote the streams registry to an optional "Browse" area) and give it a real first-launch hero state. - Re-present the two voice buttons as intentional product modes ("Cloud voice" / "On-device voice") — both retained — and remove all user-facing telemetry/provider/JSON surfaces and the transcript-provenance line.
- Replace the pipe-delimited interpreted summary with inspectable interpretation chips, and give clarification/empty/error states contextual, non-technical copy.
Effort is mostly SMALL–MEDIUM: the widgets and the intent-specific cards already exist; the work is re-composition, copy, a token layer, and removing dev scaffolding — not a rewrite.
- Search-first, not chat-first. The hero is a search field and a result list. Voice and natural language are input methods, not a persona to converse with.
- The answer is the product; the pipeline is invisible. Never show
SearchQuery, intent names, canonical IDs,SearchPlan, provider/model, tokens, or cost to end users. - Show the interpretation, not the schema. After a search, the user should see what was understood as removable chips ("Max Freeman", "Jump", "Rally Ireland"), never a pipe-delimited field dump.
- A zero-result is a valid, well-designed outcome — not an error screen.
- Editorial motorsport tone, utility discipline. Strong imagery and bold titles, but compact metadata and restrained color — a search product, not a racing game.
- Clarification is a feature, presented as a helpful question, because the backend already prefers clarifying over guessing wrong.
- Intent-specific result rendering already exists. The screen dispatches nine intents to seven purpose-built widgets (rally card, participation card, finisher leaderboard, video action card, video card, uploader leaderboard, driver-wins leaderboard). This is the hardest part of §7 and it is already done.
- Deterministic refinement chips with inherited-vs-refinement styling and per-chip removal
(
ActiveContextChipsBar) — a strong, non-chatbot follow-up model. - Breadcrumb history trail already present (
Rally Aluksne › Videos › Jumpspattern). - Voice-as-editable-transcript (no auto-submit) is correctly implemented and must be kept.
- Clarification candidates already carry a
subtitlediscriminator field — the data for §8's "Donegal Rally · 2026 · Ireland" pattern is already there.
- Wrong front door.
main.dart→RallyStreamsPage, a technical registry exposing Stream ID, Video ID,videoType='sendObs', clip windows, share/download counters, on-demand URLs. Search is buried behind an app-bar button. - Voice modes framed as dev scaffolding. Two mics labelled "Native Voice" / "Cloud Whisper" with a "transcript (edited)" provenance line read as an internal STT comparison. (Both modes are a valid product choice — see the Product decision note; the issue is the developer framing, which Phase 1 replaces with product labels.)
- Telemetry leakage. Latency+cost chip and a dialog printing provider, model, tokens, cost, "Entity Resolution Time", referent context, and raw query JSON.
- Schema-shaped interpretation. The "interpreted summary" bar reads as backend fields.
- Chrome-heavy layout. Search bar + STT provenance + chips bar + clarification + interpreted bar + result-count bar + results + follow-ups + pagination = up to eight stacked horizontal bands competing before a single result is seen.
- Inconsistent visual system. Card radii vary (16/14/12), elevations (0/2/3), and the three leaderboards use loud full-bleed gradient headers (blue / amber / green) that read as three different apps. Colors are hardcoded hex throughout rather than tokenized.
- No first-launch state.
initStateimmediately runs a search for all rallies, so the user never sees an inviting empty hero with example queries.
Current (happy path): App opens on stream registry → user hunts for and taps "General Search" → lands on a screen already showing all rallies → types query → sees interpreted schema string + telemetry → results. Cognitive load front-loaded; product identity unclear.
Target: App opens on Search home (hero field + a few example queries + recent searches) → user types or taps mic → context-aware loading → results with a one-line plain summary and removable interpretation chips → refine via chips or a follow-up → drill in. The streams registry becomes an optional Browse tab for power users.
Current: No distinct home. GeneralSearchScreen opens mid-results with a cramped input
Row: [TextField][Native mic][Cloud mic][Search]. On a phone this is four wide controls on
one line — the field is squeezed and the layout risks overflow.
Issues: field is not the hero; two mics; redundant explicit Search button competes with
Enter-to-submit; sparkle (auto_awesome) prefix icon signals "AI assistant"; language
dropdown (18 locales) sits in the app bar adding weight; no examples, no recents.
Proposed hierarchy (first launch / empty):
- Minimal header: "Rally Search" (no sparkle).
- Hero search field — full width, tall (56dp), rounded, single trailing mic, search icon as prefix, generous placeholder: "Search rallies, drivers, or moments".
- "Try:" — 3–4 example query chips (see §5 examples), not a wall.
- Recent searches (once any exist) — plain tappable list, clock icon, clearable.
- Language moved out of the app bar into a settings affordance (or auto-detect); keep it out of the hero.
Example queries (curated, max 4):
- Rallies in Ireland in 2025
- Show Max Freeman's rallies
- Jump highlights from Rally Alūksne
- Who won Rally Donegal?
┌──────────────────────────────────────┐
│ Rally Search │
│ │
│ ┌────────────────────────────────┐ │
│ │ 🔍 Search rallies, drivers… 🎤│ │
│ └────────────────────────────────┘ │
│ │
│ Try │
│ ▸ Rallies in Ireland in 2025 │
│ ▸ Show Max Freeman's rallies │
│ ▸ Who won Rally Donegal? │
│ │
│ Recent │
│ 🕘 Jump highlights — Rally Alūksne │
└──────────────────────────────────────┘
Current: TextField + 2 voice buttons + FilledButton "Search". Below it, an STT
provenance line ("Native transcript (edited)").
Redesign:
- One full-width hero field, two voice modes below it. The search icon is the prefix; a clear button and an inline submit arrow appear in the field only when text is present (Enter/keyboard "Search" also submits). The two voice modes ("Cloud voice" / "On-device voice") are both retained as labelled pills directly beneath the field (§16).
- Remove the STT provenance line for users. If provider comparison is still needed, gate it behind a hidden debug flag — it is not product UI.
- Sticky field. After a search, the field stays pinned at top and shows the current query, so refining is always one tap away.
- Keyboard:
textInputAction: search, autocorrect off for entity-heavy queries, submit on action key. - Focus state: subtle accent border + elevation; no heavy glow.
Current: Centered CircularProgressIndicator with status text that includes
"Searching database..." and "Understanding your search..." — the first leaks the DB layer.
Redesign:
- Context-aware, non-technical copy derived from the parsed intent once known, else a
neutral default:
Searching rallies…Finding jump highlights…Looking for Max Freeman…
- Skeleton rows matching the target card type (rally banner skeleton, leaderboard row skeleton) instead of a bare spinner, so the result shape is anticipated.
- Never surface
Resolving OpenEntity,Calling Gemini, DB, or plan steps.
Keep the intent-specific approach; unify the shell (radius, border, elevation, spacing, type scale) so seven card types read as one family. Standardize on: 16dp radius, 1px hairline border, elevation 0 (rely on border + surface), 16dp internal padding, consistent title/meta type ramp (§21). Retire the loud gradient headers on leaderboards in favor of a compact solid header bar using accent-tint, not full-saturation gradients.
Current (RallyResultCard): 160px banner image (or motorsport placeholder), stages badge,
status badge, title, location, date range. Good structure.
Assessment: Strong already. Tighten: title 18→ card-title token; ensure two-line long-name handling (present); the "ACTIVE"/status badge color logic is ad-hoc (green/blue-grey) — move to tokens. Tapping drills to top finishers (good), but the affordance is invisible — add a subtle trailing chevron or "View results ›".
Hierarchy: Primary = event name. Secondary = location, date. Tertiary = stage count, status. Action = whole-card tap → finishers.
Current (DriverParticipationCard): event name + location, finish-position badge, avatar
initial, car/number, total time. Winner state highlighted amber. Solid.
Assessment: Position badge copy is verbose (🏆 1st Place (Winner), Participated (role));
compress to P1, 2nd, DNF, or a small rosette for wins. Avatar is initial-only — fine.
Keep win-highlight but through a token, not raw Colors.amber.
Hierarchy: Primary = event + finish position. Secondary = driver, car. Tertiary = time.
Two widgets: VideoActionCard (moments: jump/drift/crash…) and VideoResultCard
(driver/stage videos). Both have thumbnail, play affordance, duration, context. Good.
Assessment: Per-action color+icon map is a nice touch but currently very saturated across 8
colors — soften to a restrained set (one accent + neutral, action name as the differentiator).
VideoActionCard action-type key uses start_line/near_miss (underscore) while the summary
localizer uses start line/near miss (space) — cosmetic mismatch worth aligning so labels
render consistently. Ensure play target ≥44dp.
Hierarchy: Primary = action type + thumbnail. Secondary = rally / stage. Tertiary = timecode / duration. Action = play.
Widgets: RallyLeaderboard, DriverWinsLeaderboard, UploaderLeaderboard.
Assessment: Information-dense and well-structured (POS / NO / DRIVER-CREW-CAR / TIME with
+diff; medal emoji top-3). This is the FotMob-density strength. Problems: three different
gradient identities and inconsistent header treatments make them feel like separate features.
totalTime renders as raw seconds (123s) — format as mm:ss.t. Medal emoji are fine but
should be consistent in size/baseline.
Unify: one leaderboard shell, one header style (solid accent-tint bar + title + count pill), shared row spec, tokenized top-3 highlight.
Current (ClarificationCard): amber card titled "Clarification Needed" + the backend
question + candidate ActionChips (Name (subtitle)).
Assessment: The generic "Clarification Needed" label is the weak point; the backend already
supplies a good question and candidates already carry subtitle. Redesign presentation only:
- Contextual title from the ambiguous entity type: "Which driver did you mean?", "Which
rally did you mean?", "Which stage did you mean?" (derive from candidate
EntityType). - Show discriminators on their own line, not in parentheses:
Donegal International Rally/2026 · Ireland. - Larger tap targets (list rows, not tight chips) when 2–5 candidates; keep chips only for many.
- Drop the amber "warning" framing; this is a helpful question, use neutral/accent surface.
Which rally did you mean?
┌───────────────────────────────┐
│ 🏁 Donegal International Rally │
│ 2026 · Ireland │
├───────────────────────────────┤
│ 🏁 Donegal Forest Rally │
│ 2024 · Ireland │
└───────────────────────────────┘
Semantics unchanged (§26): selection still reuses the pending parsed query and canonical ID, no LLM call.
Current: search_off icon, friendly message, "Active search context is preserved. Try
removing a filter…" (jargon), optional "Did you mean?" candidate chips, Reset / Show All
Rallies buttons.
Redesign — treat zero-result as a designed outcome:
- Plain title naming the miss in the user's terms: "No jump highlights found".
- Echo the active filters as removable chips right there:
[Max Freeman] [Jump] [Rally Ireland], each removable to broaden. - Offer explicit next actions, do not auto-broaden:
- "Search all Max Freeman videos"
- "Remove Rally Ireland"
- "Edit query"
- Keep "Did you mean?" candidates (already present) but style as list rows.
- Drop "Active search context is preserved" and "Reset Session" wording.
No jump highlights found
Filters: [Max Freeman ✕] [Jump ✕] [Rally Ireland ✕]
Try
▸ Search all Max Freeman videos
▸ Remove “Rally Ireland”
▸ Edit your search
Current: One generic red error_outline + message + Retry, plus voice errors via red
snackbars. No differentiation; the interpreted-summary path can show "Query parsing failed".
Redesign — four distinct states, no stack traces / HTTP codes / provider names:
- No results → §14 (not an error).
- Couldn't understand the query → "We couldn't turn that into a search. Try rephrasing — e.g. rallies in Ireland in 2025." + example chips.
- Service unavailable (network/backend) → "Search is temporarily unavailable." + [Try again]. (Generic, calm; the app already maps exceptions to friendly text — good, keep it, just split copy.)
- Voice transcription failed → inline near the field: "Couldn't transcribe that." + [Try again], not a red error snackbar.
Current: Two VoiceSearchButtons (Native + Cloud). Each animates: idle → listening
(red pulse) → processing (spinner) → error. Transcript lands editable in the field; no
auto-submit. State handling is solid.
Redesign (preserve the flow, fix the surface) — BOTH modes retained:
- Two intentional voice modes, presented as labelled pills: "Cloud voice" and "On-device voice". Both stay fully operational and independent (neither silently falls back to the other). Remove only the provider/model wording and the transcript-provenance line from the UI. Provider terminology ("Native", "Whisper", "OpenAI") is not shown to users.
- Idle: labelled pill with a mode icon. Recording: red pulsing pill + "Listening…"; a clear Stop and a Cancel. Transcribing: inline spinner + "Transcribing…". Result: editable text in field, cursor at end, user submits manually (unchanged).
- Voice errors inline (§15.4), not modal red.
- Keep permission and cancel-on-language-change behavior.
Idle: [ 🔍 Search… 🎤 ]
Recording: [ 🔴 Listening… ▁▂▄▆▄▂ Stop ✕ ]
Transcribing:[ ⏳ Transcribing… ]
Ready: [ show max freeman's rallies| 🎤 ] ← user taps Search
Current: Not a chatbot (good). Refinement chips + breadcrumb trail + a "Suggested Follow-ups" bar with 2–4 deterministic chips. This is the right pattern and already built.
Assessment / refinements:
- Keep the breadcrumb refinement trail (
Rally Alūksne › Videos › Jumps) — it is the strongest non-chat follow-up affordance; make it more prominent than today's 11.5px grey text. - Consolidate the two separate context strips (active-context chips bar + suggested-follow-ups bar) so refinement lives in one visual zone, reducing the stacked-band problem.
- Suggested follow-ups: keep, but cap at 3 and place them directly under the results header, not in a bottom bar competing with pagination.
Current: RallyStreamsPage (home) with app-bar routes to GeneralSearchScreen and
VideoActionSearchScreen (a third, manual dropdown-based moments search with a hardcoded
country list). Three overlapping search surfaces; the AI one is not primary.
Redesign:
- Search is home. Promote
GeneralSearchScreento the launch surface. - Introduce a minimal bottom nav or segmented shell: Search (primary) · Browse (the streams registry, relabeled and de-jargoned) — only if browse is a real user need; otherwise drop browse from the default path.
- Fold
VideoActionSearchScreeninto the NL search. Its dropdowns (action type, country, stage) duplicate what natural language + interpretation chips + the advanced-filter sheet already do. Keep the advanced-filter sheet as the "manual controls" escape hatch; retire the separate screen (or keep it only as an internal tool). - Language selector → settings, not app bar.
- Input row overflow (P0-ish): four wide controls in one
Rowon the search screen crowd the field on narrow screens. Collapsing to one field + inline mic fixes reach and overflow. - Touch targets: several icon-only controls (chip
✕, telemetry chip, pagination icons) sit at ~14–20dp; enforce ≥44dp hit area. - Long names: rally/person names already use
ellipsis/maxLinesin most cards — good; verify in chips and clarification rows. - Keyboard overlap: ensure results scroll clear of the keyboard; sticky field must not be hidden.
- Bottom safe area: pagination footer already uses
SafeArea— keep. - Chip wrapping: context chips are in a horizontal scroller (fine) but discoverability of off-screen chips is low; consider wrap on the empty/clarification surfaces.
- One-handed reach: primary actions (mic, submit) reachable at bottom/thumb zone — currently top-anchored; acceptable for search, but keep the mic reachable.
- Icon-only buttons lack semantic labels: mic, telemetry, chip-remove, pagination — add
Semantics/tooltip(tooltips exist on some, not all). - Contrast: grey-on-grey metadata (
Colors.grey.shade600,theme.hintColoron tinted fills) risks <4.5:1; verify and tokenize secondary/tertiary text. - Color-only meaning: action-type and status rely on color; pair with label/icon (mostly done) and ensure it survives color-blind palettes.
- Loading/error announcements: wrap state changes with live-region semantics so screen readers announce "Searching…", "No results", "Search unavailable".
- Selected chip state: inherited-vs-refinement is a subtle color shift; add a semantic selected/label so it is not color-only.
- Text scaling: dense 11–12.5px labels should respect
textScaleFactor; avoid fixed tiny sizes where possible.
Lightweight, token-driven. Introduce a single AppTheme / token file; stop hardcoding hex.
Typography (motorsport-editorial + utility):
| Role | Size / weight | Use |
|---|---|---|
| Display / search hero | 22–24 / 700 | home title, hero |
| Section title | 16 / 700 | leaderboard headers, "Try" |
| Card title | 16–18 / 700 | rally/event/stage names |
| Body | 14 / 500 | primary metadata |
| Metadata | 12–13 / 500 | location, date, role |
| Label / overline | 11 / 600, tracked | table headers, chips, badges |
Consider one distinctive display face for titles (bold, condensed-ish) with Roboto for body.
Spacing scale: 4 · 8 · 12 · 16 · 24 · 32 (already roughly used; formalize).
Radius: 8 (chips/badges/controls) · 12 (inner media) · 16 (cards/sheets). Retire 14.
Elevation: flat by default — elevation 0 + 1px hairline border + surface tint. Reserve a single soft shadow for the sticky search field and modals only. Remove card elevation 2/3.
Color roles (dark-first, restrained accent):
| Role | Note |
|---|---|
| background | near-black (dark) / near-white neutral (light) |
| surface / surface-variant | cards, sheets, bars |
| primary text / secondary / tertiary | 3-step ramp, contrast-checked |
| accent | ONE motorsport accent (current blue #1E88E5 is fine, or a bolder rally hue) |
| success / warning / destructive | status only, not decoration |
| top-3 / winner highlight | one gold token, not raw amber everywhere |
Icon style: the existing rounded Material set (*_rounded) — keep, consistently.
Chip style: 8dp radius, hairline border, filled tint for active/refinement, neutral for
inherited; ✕ with 44dp hit area.
Button style: filled accent for primary submit; text/tonal for secondary; consistent 12dp radius and padding.
Result-card style: the unified shell in §8.
Loading skeleton style: neutral shimmer blocks matching each card's silhouette.
| Component | Current purpose | Decision | Why |
|---|---|---|---|
GeneralSearchScreen |
AI NL/voice search | REFACTOR | Promote to home; split first-launch vs results; remove telemetry/dual-mic; single context zone |
RallyStreamsPage |
Technical stream registry (current home) | REFACTOR / DEMOTE | Not search-first; de-jargon and move to optional "Browse", or drop from default path |
VideoActionSearchScreen |
Manual dropdown moments search | REMOVE / MERGE | Duplicates NL search + advanced filters; hardcoded country list |
VoiceSearchButton |
Voice capture w/ state anim | KEEP (restyle) | Solid state machine; keep BOTH instances, restyle as labelled pills |
| Dual-voice presentation | Two voice modes shown as dev A/B controls | RESTYLE (both retained) | Keep both modes; product labels ("Cloud voice" / "On-device voice"); drop provider wording |
| STT provenance line | "Native/Cloud transcript (edited)" | REMOVE | Dev artifact leaked to users |
Telemetry chip + _showTelemetryDialog |
Provider/cost/latency/JSON | REMOVE (from user UI) | Violates "hide internals"; gate behind debug flag if needed |
| Interpreted-summary bar (pipe string) | Show interpretation | REFACTOR → interpretation chips | Schema-shaped; replace with removable chips |
ActiveContextChipsBar |
Refinement/inherited chips + breadcrumb | KEEP (restyle) | Core non-chat follow-up model; merge with follow-ups zone |
SuggestedFollowUpsBar |
Deterministic follow-ups | KEEP (restyle) | Good; relocate near results header, cap at 3 |
ClarificationCard |
Disambiguation | RESTYLE | Contextual title, discriminator lines, larger targets, drop warning framing |
RallyResultCard |
Rally result | KEEP (restyle) | Strong; tokenize, add drill affordance |
DriverParticipationCard |
Driver participation | KEEP (restyle) | Compress position copy, tokenize |
VideoActionCard |
Moment clip | KEEP (restyle) | Soften action palette; align action-type keys |
VideoResultCard |
Driver/stage video | KEEP (restyle) | Tokenize |
RallyLeaderboard / DriverWinsLeaderboard / UploaderLeaderboard |
Rankings | RESTYLE (unify) | One shell/header; format times; tokenize gold |
| Empty-state block (inline in screen) | No-results | REFACTOR → component | Filter-echo chips + explicit actions; de-jargon |
| Error-state block (inline) | Errors | REFACTOR | Split into 4 typed states |
| Loading block (spinner + status) | Loading | REFACTOR | Context copy + skeletons; drop "database" |
| Pagination footers | Paging | KEEP (restyle) | Fine; consider load-more on mobile |
| Language dropdown in app bar | Locale select | RESTYLE / MOVE | To settings; not in hero |
AdvancedFiltersSheet |
Manual filters | KEEP | Good power-user escape hatch |
| — | Design tokens / AppTheme |
NEW | Centralize color/type/spacing/radius |
| — | First-launch hero (examples + recents) | NEW | No home state exists today |
| — | Card/leaderboard skeletons | NEW | Replace bare spinner |
SEARCH / HOME (first launch)
- Minimal header ("Rally Search")
- Hero search field (single mic inline)
- "Try" — ≤4 example chips
- Recent searches (when present)
SEARCH RESULTS
- Sticky search field (shows current query)
- Interpretation chips (removable) + breadcrumb (one zone)
- Plain one-line summary ("12 rallies in Ireland, 2025")
- Result list (intent-specific cards)
- Up to 3 suggested follow-ups (under header)
- Pagination / load-more
RALLY RESULTS — banner · name · location · date · stages/status · drill affordance.
DRIVER RESULTS — event + finish position (compact) · driver/car · time.
VIDEOS / MOMENTS — thumbnail · action type · rally/stage · timecode · play.
CLASSIFICATION / RANKINGS — unified leaderboard shell · rank · driver/crew/car · time/wins/uploads.
CLARIFICATION — contextual question · candidate rows with discriminator lines.
NO RESULTS — plain title · filter-echo chips · explicit broaden actions · did-you-mean rows.
ERROR — one of four typed states (understand / unavailable / voice / no-result) · retry.
VOICE — inline idle/recording/transcribing states in the field · manual submit.
P0 — blocks the product being understood as search
- Make search the front door; demote the streams registry. (MEDIUM)
- Remove telemetry chip + telemetry dialog from user UI. (SMALL)
- Re-present both voice modes as product-facing pills; remove STT provenance line. (SMALL)
- Fix input-row crowding/overflow on mobile (full-width field; voice modes below). (SMALL)
P1 — major UX gains
- Replace pipe-summary with removable interpretation chips. (MEDIUM)
- First-launch hero with examples + recents. (MEDIUM)
- Contextual clarification (title + discriminator lines, bigger targets). (SMALL–MEDIUM)
- Redesigned empty state (filter-echo + explicit actions). (SMALL)
- Four typed error states + inline voice errors. (SMALL–MEDIUM)
- Context-aware loading + skeletons. (MEDIUM)
- Introduce design tokens /
AppTheme. (MEDIUM)
P2 — polish
- Unify leaderboard shells; format times; tokenize gold. (MEDIUM)
- Unify card radii/elevation/spacing; soften action palette. (SMALL–MEDIUM)
- Merge context-chips + follow-ups into one zone; cap follow-ups at 3. (SMALL)
- Compress verbose position/copy strings. (SMALL)
- Accessibility pass (labels, contrast, live regions, hit areas). (MEDIUM)
P3 — optional
- Bottom-nav shell (Search / Browse). (MEDIUM)
- Distinctive display typeface. (SMALL)
- Load-more instead of numbered pagination on mobile. (SMALL)
- Rich imagery/editorial treatment on rally banners. (MEDIUM)
(Effort tags: SMALL/MEDIUM/LARGE. Priority order favors high-impact SMALL/MEDIUM.)
Phase 1 — Search shell + design system (IMPLEMENTED)
Promote search to home; no auto-search on launch; full-width hero field with inline
clear/submit; both voice modes retained and re-presented as labelled pills ("Cloud voice" /
"On-device voice") with provider/debug framing removed; remove telemetry chip + dialog and the
STT provenance line; add lightweight design tokens (lib/theme/app_theme.dart); first-launch
hero with example queries (recent searches deferred). Streams registry retained as a secondary
"Browse" area reachable from the search app bar.
Phase 2 — Result surface + clarification + states (IMPLEMENTED)
Combined the audit's original Phases 2 and 3. Shipped: shared RallyCardShell (16dp radius,
hairline border, flat elevation) applied to the rally / participation / video / video-action
cards; shared LeaderboardScaffold replacing the three divergent gradient headers with one
restrained accent-tinted header (finishers / driver-wins / uploaders); formatRaceTime for
readable times (UI only); softened, icon-led action palette via RallyActionVisual (accent /
warning / destructive, with start_line/start line key normalization); tokenized
winner/top-3 gold (kRallyGold); rally "View results ›" drill affordance. Removed the
schema/pipe interpreted-summary band — interpretation is now the removable
ActiveContextChipsBar. Redesigned clarification (ClarificationCard: contextual type-derived
title, tappable ≥44dp discriminator rows, neutral treatment). Redesigned no-results (plain
title, removable filter chips, explicit broaden actions, clean "Did you mean" — jargon removed).
Split errors into typed states (_SearchErrorKind: service-unavailable vs
couldn't-understand). Context-aware loading copy + a lightweight ResultsSkeleton (pure
Flutter, no new package). Follow-ups capped at 3. Accessibility: semantics/live-regions on
states, ≥44dp targets, removable chips announce what they remove. Voice unchanged — both
modes retained.
Phase 3 — Visual consistency + mobile finishing (IMPLEMENTED)
Added a coherent type ramp (AppText: screen/section/card title, body, metadata, label) and
migrated the primary journey's prominent text (hero, no-results, error, section labels, rally &
participation card titles) onto it, removing those magic font sizes. Compacted the participation
finish-position copy to P1 · Winner / P2 / DNF / Participated (meaning preserved; tests
updated intentionally). Balanced the two voice pills (full-width, centered, consistent 48dp
height). Added an explicit tap semantic to the rally card ("View results for …"). Fixed a
real ~90px app-bar overflow at 320px by compacting the language selector's closed state to the
language code via selectedItemBuilder (the open menu keeps full native names). Added a
narrow-width (320px) no-overflow regression test. Pagination kept (numbered prev/next). Voice
semantics and both modes unchanged.
Deferred (genuinely optional): distinctive display typeface; richer rally imagery pipeline; load-more on mobile; bottom-nav shell; full token migration of leaderboard row internals; a broader contrast/text-scaling audit of surfaces outside the core journey.
Each phase is independently shippable and touches only presentation.
Protected — presentation-only redesign must not alter:
- Backend architecture (Flutter → FastAPI → QU → Router → OpenEntity → SearchPlan → Repository → MySQL).
- Query semantics — OR-within-dimension / AND-across-dimensions; the 9-intent model;
SearchQueryfield contract. - Clarification semantics — selection reuses the pending parsed query and replaces only the ambiguous entity dimension; no LLM call on selection; filters/referents/generation preserved.
- Canonical ID behavior — canonical entity/event IDs survive follow-ups and clarification.
- No-auto-submit voice — transcript is always editable; the user submits manually.
- Deterministic router/OpenEntity and the safety ordering (correct-confident > clarification > safe no-match > wrong-confident).
- API contracts / endpoints (
/v1/conversation/search,/v1/voice/transcribe, session/ requestId handling).
Modern search utility with a motorsport-editorial skin. Dark-first, near-black surfaces with a single restrained accent (keep the current blue or move to a bolder rally hue used sparingly). Bold, slightly condensed titles; compact, technical metadata (FotMob-grade density in leaderboards); strong rally imagery where available with a clean placeholder otherwise. Flat surfaces, hairline borders, one soft shadow reserved for the sticky search field and modals. Restraint over decoration: retire multi-gradient headers and the 8-color action palette; let type, spacing, and one accent carry the identity. The result should feel like Google-search simplicity + FotMob density + Linear-level polish — never a chatbot, a dashboard, or a racing game.
┌───────────────────────────────┐ Search-first.
│ 🔍 Search rallies, drivers 🎤│ One field. One mic.
├───────────────────────────────┤ Interpretation as chips.
│ [Max Freeman ✕][Jump ✕] │ Internals invisible.
│ 12 jump highlights │ Editorial, restrained.
│ + intent-specific result cards│
└───────────────────────────────┘
The cheesy rally personality now extends to every connectivity state via the
17-state messaging matrix (lib/services/offline/offline_messaging.dart,
rendered by lib/widgets/offline_banner.dart). Each state is playful headline →
literal explanation → obvious action, and offline results are never styled
as an error.
- Offline results: a subtle secondary-container banner ("Still racing — even without signal 🏁") above the normal result cards, with last-updated age when the snapshot is stale.
- Clarification offline: reuses the standard
ClarificationCard(same chips as online) — the offline resolver clarifies, never guesses. - No snapshot yet: "We haven't packed the service notes yet" + a Sync now action (when online).
- Low bandwidth: local result shown within the fallback budget; a late online answer is offered explicitly ("HQ answered — show latest"), never swapped silently.
- Video: discovery cards render from local metadata; playback is gated with "Found the clip — but the stream's off-stage".
- Voice: cloud-voice-offline and on-device-voice-unavailable each get their own headline + fallback action; the transcript stays editable and is never auto-submitted.
These are additive: the online experience is visually unchanged (no offline chrome when reachable).