31 KiB
Hope Voice iOS — UI/UX Audit & Redesign Specification
- Task: 20260831-105824-plan-code-ui-audit
- Agent: coder (UI/UX research pass)
- Model: litellm-lan/qwen3.8-27b
- Date: 2026-08-31
- Status: complete (research + specification only; no product code changed)
1. Scope & Method
This document audits the current iOS UI in /Users/shane/dev/voice-ios against the
canonical Hope visual language found in the reference projects, and specifies a
coherent redesign. All findings cite file paths and line numbers so an implementer can
act without this conversation's context.
Sources inspected:
| Path | What it provided |
|---|---|
hope-voice-api/web/style.css (1607 lines) |
Full design-token system (:root, lines 1–26), LED-wall avatar (lines 316–429), brand glyph (lines 125–166), message bubbles (lines 702–764), buttons/controls (line 431+), task status dots (lines 1120–1150), keyframes (think-pulse L427, message-in L709, status-pulse L1133) |
hope-voice-api/web/app.js |
LED-wall build/render implementation (lines 265–341): 21×21 grid, radial falloff, per-dot phase ripple, audio-reactive sizing, state palettes (line 389 sets data-state) |
hope-voice-api/web/index.html |
ARIA roles: led-wall is aria-hidden="true" (L61), orb-labels is aria-live="polite" (L62), transcript aria-live="polite" (L102) |
hope-voice-api/web/favicon.svg |
Square Hope glyph: 64×64 rounded rect (rx=16) dark plate #08111f, iris circle + tilted orbit ellipse + warm dot — the canonical "square Hope" mark |
hope_companion/scripts/gen-icons.mjs |
SDF-generated app icon: 1024² grid, rounded-rect plate (sdRoundedRect … 512,512, r=228) with indigo→violet diagonal gradient (43,46,122)→(124,58,237), white mark of two nodes + arc + status ring |
hope_companion/resources/icon.png |
1024×1024 RGBA output of the above generator (verified via sips: hasAlpha, RGB) |
voice-ios/Sources/** (all SwiftUI sources) |
Current implementation audited below |
voice-ios/project.yml, Resources/Assets.xcassets/AppIcon.appiconset/Contents.json |
Build config; app icon set is empty (no image assigned) |
The "square Hope element" — resolved
The square Hope visual element is the LED-wall avatar: a 320×320 px square
container (border-radius: 26px) holding a 21×21 dot matrix that blooms with audio.
Evidence: style.css L331–346 (.led-wall { width: 320px; height: 320px; … grid-template-columns: repeat(21,1fr); border-radius: 26px }),
L349–359 (glass panel backing), L361–372 (.led-dot), L376–392 (state color palette),
and app.js L265–341 (build + per-frame render). It is the single most prominent
Hope visual in the web UI and sits at the center of the voice console.
Two related but distinct marks also exist and are relevant to the iOS redesign:
- Favicon glyph (
favicon.svg): 64×64 rounded square, dark plate#08111f, iris circle#8ba7ffwith tilted orbit ellipse#526caeand warm accent dot#ffb38e. This is the compact "Hope" identity mark. - Companion app icon (
hope_companion/resources/icon.png, generated bygen-icons.mjs): 1024² rounded square (corner radius 228/1024 ≈ 22.3%) with an indigo→violet diagonal gradient and a white two-node + arc + ring mark.
The current iOS app uses none of these: OrbView.swift renders a plain 120 pt
Circle() filled with a system-color gradient (lines 45–47), and the app icon set is
empty. This is the central brand gap this spec addresses.
2. Asset Provenance
| Asset | Source | Format | Notes |
|---|---|---|---|
| LED-wall avatar | hope-voice-api/web/style.css L316–429 + app.js L265–341 |
CSS grid + JS per-frame render | Not an image asset; must be re-implemented in SwiftUI (Canvas/TimelineView) |
| Square Hope glyph (favicon) | hope-voice-api/web/favicon.svg |
SVG 64×64 | Portable to iOS as a vector asset or SF Symbol-style shape |
| Companion icon | hope_companion/resources/icon.png |
PNG 1024² RGBA, generated by scripts/gen-icons.mjs (SDF) |
Candidate base for the iOS AppIcon; generator is dependency-free Node |
| Design tokens | style.css :root L1–26 |
CSS custom properties | Full palette + easing below (§5) |
| State palettes | style.css L376–392 |
CSS data-state selectors |
idle/connecting/thinking/listening/speaking colors |
No binary image assets currently exist in voice-ios/Resources/Assets.xcassets beyond
an empty AppIcon.appiconset. The redesign should add: (a) an AppIcon derived from
the companion mark, (b) a HopeGlyph vector asset from the favicon, and (c) no raster
assets for the LED wall (procedural rendering only).
3. Current State Audit (voice-ios)
3.1 Screen inventory
| Screen | File | Navigation | Notes |
|---|---|---|---|
| Conversation (root) | Sources/Features/Conversation/ConversationView.swift |
NavigationStack, .navigationTitle("Hope Voice") |
Single screen; everything else is pushed or sheeted from the toolbar |
| Orb + status header | ConversationView.statusHeader L63–125, OrbView.swift |
— | 120 pt circle, system colors |
| Transcript list | OrbView.swift (TranscriptListView) L64–146 |
Inline in root VStack | Plain rows, role name + timestamp, no bubbles/avatars |
| Composer | ConversationView.composer L161–191 |
— | Start button / ProgressView / TextField+Send+End |
| Settings | Sources/Features/Settings/SettingsView.swift |
Pushed from toolbar gear | Form: Server URL, Voice, Model, Text mode, Connection test; voice/model pickers as sheets |
| Connection test | Sources/Features/Settings/ConnectionTestView.swift |
Pushed from Settings | Token endpoint probe |
| Tasks panel | Sources/Features/Tasks/TaskPanelView.swift |
Pushed from toolbar tray icon (badge) | Cards with status dot, steps, result/error blocks; DEBUG "Sample" button |
| Media panel | Sources/Features/Media/MediaPanelView.swift |
Pushed from toolbar music icon | Now-playing card, transport, seek, volume, pending jobs, choices, search, library, add-stream |
3.2 Navigation & hierarchy findings
- N1 — Flat toolbar with three unrelated destinations. Tasks, Media, and Settings
are all
.topBarTrailingpushes (ConversationView L193–229). There is no tab bar or grouped navigation; the web UI keeps these as side panels/sheets. Acceptable on iOS, but the icon set (tray.full,music.note,gearshape) carries no brand and the order puts Settings last while it is the most-used config surface. - N2 — No persistent identity. The web header shows the brand glyph + "Hope Voice"
- presence dot + profile chip (index.html L18–39). iOS shows only a plain nav title; no Hope mark anywhere in the UI.
- N3 — Session lifecycle is not visually distinct. Idle, connecting, waitingAgent, ready, and failed all render in the same layout with only text changes (ConversationView L148–163, L248–263). The web UI has a dedicated console-intro state (style.css L290–314) before connecting.
3.3 Component & state findings
- C1 — OrbView is not the Hope element.
OrbView.swiftL44–56: aCircle()with.blue/.green/.orange/.redsystem gradients and a uniformscaleEffectpulse. It matches none of the web LED-wall behavior (radial bloom, per-dot phase ripple, audio-reactive dot sizing) and uses colors outside the Hope palette (§5). - C2 — State palette mismatch. OrbView maps listening→green, speaking/thinking→orange
(L17–24), while the web uses cool blue for listening (
#6f8ff7), warm amber for thinking/connecting (#d99a6c), and light periwinkle for speaking (#9dc2ff) (style.css L376–392). The iOS colors contradict the brand's "cool while it listens, warm toward the center as it speaks" intent. - C3 — No audio-reactive rendering.
ConversationStateModelexposes smoothedmicLevel/agentLevel(ConversationState.swift L18–19) but nothing consumes them for visuals except a 4 pt mic meter bar (ConversationView L127–138). The web drives the entire avatar from these levels every frame (app.js L300–341). - C4 — Transcript rows are not chat bubbles.
TranscriptListView.transcriptRow(OrbView.swift L111–137) renders full-width stacked cards with a role-name caption. The web uses aligned bubbles: user right-aligned#1b3150with bottom-right corner cut, agent left-aligned#14243awith bottom-left cut, 27 px avatar dot for the agent, partials at opacity 0.58 (style.css L714–756). Partial/final distinction on iOS is only an opacity change on plain text (L117, L130). - C5 — No message entrance motion. Web rows animate in with
message-in(translateY 5px + fade, style.css L709–712); iOS rows appear instantly. - C6 — Thinking chip is plain. A capsule with SF Symbol "ellipsis"
(ConversationView L68–74) vs the web's amber pulsing dot + text (
think-pulse, style.css L409–429). - C7 — Task status dots lack motion.
TaskPanelViewuses static colored circles; the web's running dot pulses (status-pulse, style.css L1125–1135). - C8 — Composer states are abrupt. Idle shows a Start button, connecting shows a
bare
ProgressView(L169–170), ready shows a disabled TextField in voice mode with no explanation beyond placeholder text. The web's composer is always present and the mic meter sits directly above it (index.html L85–107). - C9 — Empty app icon.
Resources/Assets.xcassets/AppIcon.appiconset/Contents.jsonhas no image entries; the app ships with the default iOS placeholder. - C10 — No Dynamic Type / reduced-motion handling anywhere. No
@Environment(\.dynamicTypeSize), noaccessibilityReduceMotionchecks in any view. - C11 — Accessibility is label-only. Phase 8 added labels/values (OrbView L58–60,
toolbar buttons L211/219/227), but the LED wall equivalent on web is
aria-hiddenwith a separatearia-live="polite"status region (index.html L61–62). iOS's OrbView combines label+value on one element, which is fine, but transcript updates are not announced (TranscriptListViewhas noaccessibilityLiveRegionequivalent — SwiftUI: needs.accessibilityLabelon rows + possibly UIAccessibility notifications for new finals).
3.4 What is already good (keep)
- State machine port is faithful: EMA 0.6/0.4, thresholds 0.035/0.02, thinking chip after 2 s (ConversationState.swift L12–89) — matches app.js exactly.
- Stale-room guards, audio interruption handling, error banners (Phase 8).
- Media panel structure (now-playing → transport → seek → volume → jobs → choices → search → library → add-stream) is a sensible vertical flow for iPhone.
- Task card information design (status dot + description + role-styled steps + result/ error blocks) mirrors the web well.
4. Navigation & Hierarchy Specification
Target structure
Root: ConversationScreen (NavigationStack)
├── Header (inline, not nav-title-only)
│ ├── HopeGlyph (28×28, favicon mark) + "Hope Voice" title
│ └── Toolbar: [Tasks (badge)] [Media] [Settings]
├── PresenceStage (replaces statusHeader)
│ ├── HopeLedWall (160–200 pt square, procedural)
│ ├── StateLabel (aria-live equivalent)
│ ├── ThinkingChip / BargeInHint
│ ├── MicMeter (voice mode)
│ └── Voice·Model caption + Agent caption
├── TranscriptList (bubbles, §6.3)
└── ComposerBar (always visible; content by session state, §6.4)
Pushed: TaskPanelView, MediaPanelView, SettingsView (unchanged destinations)
Sheets: VoicePickerView, ModelPickerView (unchanged)
Changes vs current:
- Move mode toggles out of the header. The mic/text-mode toggles currently live in
the status header (ConversationView L93–105), making it tall and busy. Move them into
the ComposerBar as a segmented control (Voice | Text) — matching the web's
mode-switchgroup (index.html L26–34). The header keeps at most: wall, label, chip, meter, one caption line. - Add an intro/landing state. When
.idle, render a compact console-intro block (brand glyph large + "Hope is ready when you are" + Start button) instead of the full transcript layout — mirrors web.console-intro(style.css L290–314). This makes the first-run experience branded rather than blank. - Keep three toolbar items but reorder: Tasks, Media, Settings is fine; ensure the
tasks badge uses
--danger/red only for failed and iris-blue for active (see §6.5).
5. Visual Language & Tokens
5.1 Color tokens (from style.css L1–26, ported to SwiftUI)
enum HopeColor {
static let canvas = Color(hex: 0x08111F) // page background
static let canvasDeep = Color(hex: 0x050B14)
static let surface = Color(hex: 0x0D1929)
static let surfaceRaised = Color(hex: 0x132238)
static let surfaceSoft = Color(hex: 0x172941)
static let line = Color(hex: 0xB7CCEB, opacity: 0.13)
static let lineStrong = Color(hex: 0xB7CCEB, opacity: 0.22)
static let ink = Color(hex: 0xF4F7FB)
static let inkSoft = Color(hex: 0xC5D1E2)
static let inkMuted = Color(hex: 0x8395AD)
static let iris = Color(hex: 0x8BA7FF) // primary accent
static let irisStrong = Color(hex: 0x6F8FF7)
static let irisDeep = Color(hex: 0x3C5FC9)
static let warm = Color(hex: 0xFFB38E) // secondary accent
static let mint = Color(hex: 0x76D9B2) // success
static let danger = Color(hex: 0xFF806F) // error
static let userBubble = Color(hex: 0x1B3150)
static let agentBubble = Color(hex: 0x14243A)
static let focus = Color(hex: 0xB5C5FF)
}
LED-wall state colors (style.css L376–392):
| State | Web color | SwiftUI |
|---|---|---|
| idle | #56739E |
Color(hex: 0x56739E) |
| connecting / thinking | #D99A6C |
Color(hex: 0xD99A6C) |
| listening | #6F8FF7 |
HopeColor.irisStrong |
| speaking | #9DC2FF + drop-shadow rgba(111,143,247,0.35) @14px |
Color(hex: 0x9DC2FF) + .shadow(color: .iris.opacity(0.35), radius: 14) |
Rule: replace all system .blue/.green/.orange/.red in OrbView, mic meter, chips,
and banners with the tokens above. Semantic mapping: success=mint, warning/danger=
danger (not system red), thinking=warm, listening=irisStrong, speaking=#9DC2FF.
5.2 Shape & surface tokens
- Corner radii: panels/cards 14 pt (matches current TaskCardView), LED-wall plate 26 pt at 320 px web → scale to ~13–18 pt at 160–200 pt iOS size (keep the 26/320 ≈ 8% ratio). Bubbles 12 pt with one corner cut to 3 pt (user: bottom-right; agent: bottom-left) — style.css L732–751.
- Backing surfaces: LED-wall glass panel = radial gradient
rgba(111,143,247,0.10)center → transparent 62%, overrgba(6,12,24,0.4), with a 1 pt borderrgba(139,167,255,0.14)(style.css L349–359). - Easing:
cubic-bezier(0.22, 1, 0.36, 1)(--ease-out) for all state transitions; dot-level changes use 90 ms linear (style.css L371); wall filter transition 220 ms ease (L345).
5.3 Typography
Keep SwiftUI defaults but align scale with web: status label = .headline (web 0.83rem
semibold), captions .caption/.caption2, transcript body .body at line-height 1.55
(web L737–738). No custom font is used by the web (system stack) — do not introduce one.
6. Screen & Component Specifications
6.1 HopeLedWall (replaces OrbView)
Geometry: square container, size min(200, availableWidth - 32) pt, corner radius
≈ 8% of side. Inside: a 21×21 dot grid with 2 pt gap and 12 pt padding (web L337–342).
At 180 pt the dots are ~5 pt base size — same as web's --sz: 5px (L362).
Implementation: SwiftUI Canvas inside a TimelineView(.animation) (or
.periodic(from:by: 1/30) to cap cost; web renders every rAF but iOS should cap at
30 fps for battery). For each of the 441 dots compute, porting app.js L298–341 verbatim:
rad = sqrt(dx² + dy²) // 0 center → ~1.41 corner; web normalizes by (COLS/2)
phase = hash(r,c) * 2π // deterministic per-dot phase (app.js L290 formula)
level = speaking ? agentLevel : listening ? micLevel : 0 // from ConversationStateModel
idle: vis = 0.16 - rad*0.08 ; size = 1
signal: lvl = min(1, level*1.3)
falloff = exp(-rad² * (2.0 - lvl*1.1))
ripple = 0.62 + 0.38*sin(t*0.32 + phase)
vis = falloff * (0.35 + lvl) * ripple * (speaking ? 1 : 0.55)
size = 1 + (1 - rad) * 1.4 * lvl
no-signal: breathe = 0.5 + 0.5*sin(t*0.12 + phase)
vis = 0.14 - rad*0.06 + 0.04*breathe ; size = 1 + (1-rad)*0.3
Dot color = state palette (§5.1); opacity clamped [0.02, 1]; dot diameter = 2*size pt
base 5 pt → up to ~9 pt at center on strong signal. The wall itself gets the glass
backing (§5.2) and, in speaking state, a soft outer glow (shadow radius 14, iris 35%).
Performance notes:
- Precompute
radandphaseonce into a[Dot]array; per-frame work is only the trig/exp math on 441 entries — trivial for Canvas. - Pause the TimelineView when phase == .idle (static matrix) or scene backgrounded.
- Respect
accessibilityReduceMotion: render the static idle matrix + state color only, no ripple/breathe; keep opacity changes but drop size animation.
Accessibility: mark the wall .accessibilityHidden(true) and expose a separate
status element with .accessibilityLabel("Conversation status") +
.accessibilityValue(stateText) — exactly mirroring web's aria-hidden wall +
aria-live="polite" label region (index.html L61–62). Announce phase changes via
UIAccessibility.post(notification: .announcement, …) only on meaningful transitions
(speaking start/end), not every tick.
6.2 PresenceStage layout (replaces statusHeader)
VStack(spacing: 12) {
HopeLedWall(state:, level:) // §6.1
StateLabel(text: statusText) // .headline, inkSoft
ThinkingChip() // when isThinkingChipVisible
BargeInHint() // when textMode && ready
MicMeter(level:) // voice mode only; 4 pt capsule, iris fill
CaptionLine(voice · model) // .caption, inkMuted — one line, not two
}
- Thinking chip: amber dot (5 pt,
warm) withthink-pulseequivalent (opacity 1→0.28, scale 1→0.75, 1.2 s ease-in-out infinite) + "Thinking…" caption — replaces the current ellipsis capsule (ConversationView L68–74). - Mic meter: fill color
irisStrong(not system green), width = level × width × 2.5 (keep existing scaling, ConversationView L134). - Error banners keep their current placement but use
dangertoken at 0.1 opacity background; reconnecting banner useswarm.
6.3 TranscriptList (bubble redesign)
Port web bubble semantics (style.css L714–756):
- User row: right-aligned, max width 82%, background
userBubble(#1B3150), text #E8EFFF, corner radius 12 with bottom-right 3 pt. No avatar dot. - Agent row: left-aligned, max width 82%, background
agentBubble(#14243A), textinkSoft, 27 px→24 pt avatar dot (iris ring + iris center, style.css L714–730), corner radius 12 with bottom-left 3 pt. - Partial rows: opacity 0.58 on the whole row (L753–756) — keep current behavior but apply to bubble background too.
- System rows: centered, transparent,
inkMuted, caption size (L702–707). - Entrance animation:
message-inequivalent — opacity 0→1 + y +5→0 over ~180 ms ease-out on insert only (L709–712). Use.transition(.opacity.combined(with: .offset(y: 5)))withwithAnimation(.easeOut(duration: 0.18))in the existing scroll-to-bottom path (OrbView.swift L102–106). - Timestamps: keep per-row timestamps but style them as web does — hidden by default,
revealed on row focus/long-press (
message-row::after, L758–764). Simplest iOS: show timestamp in caption2inkMutedunder the bubble (current behavior) — acceptable; long-press-to-reveal is a later-phase nicety. - Accessibility: each row gets
.accessibilityElement(children: .combine)with label "(roleName): (text)" so VoiceOver reads one element per message; post an announcement on new final agent messages only.
6.4 ComposerBar (always visible)
Replace the current state-swapped HStack (ConversationView L161–191) with a persistent bar:
| Session state | Content |
|---|---|
| idle | Intro card above; bar shows prominent "Start conversation" button (iris fill, white text, 42 pt min height — web .btn metrics L444–453) |
| connecting / waitingAgent | Bar disabled, shows ProgressView + "Connecting…" caption |
| ready, voice mode | Segmented [Voice |
| ready, text mode | Same segmented control; TextField enabled (rounded, surfaceRaised bg); Send button (iris) disabled when empty; End button |
The Voice/Text segmented control replaces both current toggles (L95–104) and the
Settings "Text mode" toggle stays in sync via SettingsStore.textMode.
6.5 TaskPanelView adjustments
- Status dot: running = iris with
status-pulse(opacity 1→0.3, 1.4 s infinite); completed = mint; failed = danger; pending = inkMuted (style.css L1120–1131). - Step role colors already mirror web (thinking=blue→iris, tool_call=indigo, etc.) —
keep, but swap system blues for
iris/irisDeep. - Toolbar badge on the tray icon: iris-blue background for active count; reserve red
(
danger) for a failed-task indicator if one is added later. Current red-always badge (ConversationView L206) reads as "error" — change toiris.
6.6 MediaPanelView adjustments
- Keep layout; swap system colors: now-playing card bg
surfaceRaised, transport accent iris, error banner danger/0.1, seek slider tint iris. - Now-playing art placeholder: use HopeGlyph at low opacity instead of a blank box.
6.7 Settings & pickers
- Form sections are fine; add the HopeGlyph + "Hope Voice" header row at top for brand continuity (mirrors web header, index.html L18–39).
- Connection test result colors: success mint, failure danger.
7. Motion Specification
| Element | Trigger | Animation | Source |
|---|---|---|---|
| LED wall dots | every frame (≤30 fps) while active | opacity + size per §6.1 formulas; 90 ms linear feel via per-frame lerp | app.js L300–341, style.css L371 |
| Wall state color | phase change | cross-fade 220 ms ease (filter transition) | style.css L345, L376–392 |
| Speaking glow | entering .speaking | shadow radius 0→14 iris/35% over 220 ms | style.css L389–392 |
| Thinking chip dot | visible | think-pulse: opacity 1→0.28, scale 1→0.75, 1.2 s ease-in-out ∞ |
style.css L415–429 |
| Message row insert | new event | fade + translateY(5→0), 180 ms ease-out | style.css L709–712 |
| Task running dot | status running | status-pulse: opacity 1→0.3, 1.4 s ∞ |
style.css L1125–1135 |
| Buttons | tap/press | transform scale or color 180 ms --ease-out |
style.css L453 |
| State label text | phase change | no animation (instant) — keep text updates jank-free | — |
Reduce Motion: when accessibilityReduceMotion is on: wall renders static per-state
matrix (no ripple/breathe/size), thinking chip loses pulse (static dot), message rows
appear without offset (fade only or instant), task dots static. All state information
remains visible — motion is decorative.
8. Accessibility Specification
- Status announcements: separate live status element (label "Conversation status", value = state text) distinct from the hidden wall (§6.1). Announce only on phase transitions to speaking/listening/failed, throttled to ≥2 s apart.
- Transcript: combined elements per row (§6.3); announce new final agent messages;
partials are not announced (they churn too fast — matches web
aria-live="polite"on the container which coalesces). - Reduce Motion handled per §7.
- Dynamic Type: all text uses semantic fonts (
.headline,.body,.caption) — already true; verify bubble max-widths don't clip at accessibility sizes by allowingmaxWidth: 82%to grow to 92% above.accessibility1. Wall size is fixed (decorative). - Contrast: token palette on canvas (#08111F): ink #F4F7FB ≈ 16:1, inkSoft #C5D1E2 ≈ 10:1, inkMuted #8395AD ≈ 4.6:1 (passes AA for large text; captions using it are ≥ caption size — flag as a watch-item), iris #8BA7FF on canvas ≈ 6.5:1. All pass WCAG AA except muted-on-canvas at small sizes, which should be reserved for secondary metadata only (timestamps, agent name).
- Touch targets: all interactive controls ≥44 pt (web
.btnmin-height 42 px; iOS standard). Composer buttons and toolbar icons already comply via SF Symbol sizing — verify segmented control height ≥44. - VoiceOver order: header status → transcript (oldest→newest) → composer, matching visual order; the wall is hidden from VO (§6.1).
9. App Icon & Branding Assets
- AppIcon: generate from
hope_companion/scripts/gen-icons.mjsoutput (resources/icon.png, 1024²) — it already encodes the square Hope plate + mark and is dependency-free to regenerate. Add all required iOS sizes (60/58/40/29 @1x–3x, marketing 1024) toAppIcon.appiconset. Fallback if regeneration is undesirable: use the existing PNG directly; it has alpha and correct dimensions (verified via sips). - HopeGlyph asset: port
favicon.svginto the asset catalog as a vector PDF/SVG or implement as a small SwiftUI shape view (rounded rect + circle + rotated ellipse + dot) for use in the header, empty states, and media placeholder. The SVG is 6 lines — trivial to hand-port withPath. - No raster assets for the LED wall — procedural only (§6.1).
10. Phased Implementation Plan
Phase A — Brand foundation (low risk, high impact)
- Add
HopeColortoken enum +Color(hex:)helper; replace system colors in OrbView, mic meter, chips, banners, task dots, media panel. - Add AppIcon (all sizes) from companion generator output.
- Add HopeGlyph shape view; use in header row of Conversation and Settings.
- Exit: app visually reads as "Hope" at a glance; zero system-green/orange/red left in conversation surfaces.
Phase B — LED wall avatar (the core element)
- Implement
HopeLedWall(Canvas + TimelineView ≤30 fps) with the exact formulas from §6.1, driven byConversationStateModel.micLevel/agentLevel. - Glass backing + speaking glow; state palette per §5.1.
- Pause rendering when idle/backgrounded; reduce-motion fallback (§7).
- Replace OrbView usage in ConversationView; keep the old file for one release as a
#if-guarded fallback, then delete. - Exit: wall blooms with agent audio (visible in sim via text-mode TTS-driven state — note: sim mic is dead per PLAN.md Phase 2, so listening-side reactivity must be verified on device; speaking-side can be verified in simulator).
Phase C — Transcript bubbles & composer
- Bubble redesign per §6.3 (alignment, colors, corner cuts, avatar dot, entrance animation, combined VO elements).
- Persistent ComposerBar with Voice/Text segmented control per §6.4; remove toggles from header.
- Intro/landing state for
.idleper §4.3. - Exit: transcript visually matches web bubbles; mode switch is one tap in the bar.
Phase D — Polish pass
- Thinking chip pulse, task running-dot pulse, button press easing (§7).
- Toolbar badge color → iris (§6.5); media panel token swap (§6.6).
- Dynamic Type audit at
.accessibility3; contrast fixes for muted captions if needed. - Exit: side-by-side with web UI on same deployment shows consistent language; no motion under reduce-motion; VO walkthrough clean.
Sequencing rationale: A is pure token/asset work (no behavior change). B is the signature element and isolates the only new rendering code. C changes information layout (transcript/composer) but reuses existing data flow. D is small deltas. Each phase is independently reviewable and revertible.
11. Acceptance Criteria (for the redesign implementation)
- Brand: app icon set populated; HopeGlyph visible in conversation + settings
headers; no system palette colors on conversation surfaces (grep for
.blue,.green,.orange,.redinSources/Featuresreturns only token-enum internals). - LED wall: renders 21×21 grid; dot opacity/size respond to
agentLevelwhile speaking (verifiable in simulator via TTS) and tomicLevelwhile listening (device-only); idle shows static dim matrix; state colors match §5.1 table exactly. - Performance: wall animation holds 30 fps on iPhone 15-class hardware with no
main-thread stalls (Instruments template: Time Profiler + SwiftUI view body count);
TimelineView paused when
.idleor backgrounded. - Transcript: user rows right-aligned #1B3150, agent rows left-aligned #14243A with avatar dot; partials at 0.58 opacity; new rows animate in (fade+5 pt rise); VO reads one element per message.
- Composer: always visible; Voice/Text segmented control switches mode and syncs
with
SettingsStore.textMode; Start/End states per §6.4 table. - Accessibility: reduce-motion yields static wall + no pulses; status announcements fire on speaking/listening/failed transitions only; all interactive targets ≥44 pt.
- Build: zero warnings; simulator build/run clean (XcodeBuildMCP); existing connect/disconnect/reconnect matrix from PLAN.md Phase 8 still passes.
- No regressions: media reconciliation, task events, TLS pinning, persistence all unchanged (these phases touch only presentation layer).
12. Risks & Open Questions
- R1 — Sim mic limitation. Listening-side wall reactivity cannot be verified in the simulator (CoreAudio -4010, PLAN.md Phase 2). Device testing required for that half of §6.1; speaking-side is verifiable in sim via agent TTS.
- R2 — Canvas performance on older devices. 441 dots × per-frame trig at 30 fps is cheap on A15+ but should be profiled on the oldest supported device (iOS 17 floor = A14-class). Mitigation: drop to 15 fps or 17×17 grid if profiling shows headroom loss.
- R3 — Color(hex:) helper. No such helper exists in the codebase; it is a new ~10-line utility. Trivial but must be added before Phase A color swaps.
- R4 — Web parity scope. This spec deliberately does not port web-only features (keyboard shortcuts, drag-to-seek waveform) — consistent with PLAN.md's accepted gaps.
- Open question: should the LED wall size scale with device width (e.g., larger on iPad-class or landscape)? Spec assumes iPhone portrait only per UPDATE_PATH.md scope; recommend fixed 180 pt for Phase B and revisit later.