status
This commit is contained in:
1 parent
c9c25f86eb
commit
40aaf4fea7
56 files changed
+5683
-179
No files matched your search
@@ -0,0 +1,528 @@
|
||||
# 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:
|
||||
|
||||
1. **Favicon glyph** (`favicon.svg`): 64×64 rounded square, dark plate `#08111f`,
|
||||
iris circle `#8ba7ff` with tilted orbit ellipse `#526cae` and warm accent dot
|
||||
`#ffb38e`. This is the compact "Hope" identity mark.
|
||||
2. **Companion app icon** (`hope_companion/resources/icon.png`, generated by
|
||||
`gen-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 `.topBarTrailing` pushes (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.swift` L44–56: a `Circle()` with
|
||||
`.blue/.green/.orange/.red` system gradients and a uniform `scaleEffect` pulse.
|
||||
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.** `ConversationStateModel` exposes smoothed
|
||||
`micLevel`/`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 `#1b3150` with bottom-right corner
|
||||
cut, agent left-aligned `#14243a` with 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.** `TaskPanelView` uses 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.json`
|
||||
has no image entries; the app ships with the default iOS placeholder.
|
||||
- **C10 — No Dynamic Type / reduced-motion handling anywhere.** No
|
||||
`@Environment(\.dynamicTypeSize)`, no `accessibilityReduceMotion` checks 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-hidden`
|
||||
with a separate `aria-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 (`TranscriptListView` has no `accessibilityLiveRegion` equivalent —
|
||||
SwiftUI: needs `.accessibilityLabel` on 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:
|
||||
|
||||
1. **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-switch` group (index.html L26–34). The header keeps at most: wall, label, chip,
|
||||
meter, one caption line.
|
||||
2. **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.
|
||||
3. **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)
|
||||
|
||||
```swift
|
||||
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%, over `rgba(6,12,24,0.4)`, with a
|
||||
1 pt border `rgba(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 `rad` and `phase` once 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`) with `think-pulse` equivalent
|
||||
(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 `danger` token at 0.1 opacity
|
||||
background; reconnecting banner uses `warm`.
|
||||
|
||||
### 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),
|
||||
text `inkSoft`, 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-in` equivalent — opacity 0→1 + y +5→0 over ~180 ms
|
||||
ease-out on insert only (L709–712). Use `.transition(.opacity.combined(with: .offset(y: 5)))`
|
||||
with `withAnimation(.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 caption2 `inkMuted` under 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 | Text] control; TextField hidden (placeholder area shows "Barge in anytime — speak or switch to text"); End button (danger outline) |
|
||||
| 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 to `iris`.
|
||||
|
||||
### 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
|
||||
|
||||
1. **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.
|
||||
2. **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).
|
||||
3. **Reduce Motion** handled per §7.
|
||||
4. **Dynamic Type:** all text uses semantic fonts (`.headline`, `.body`, `.caption`) —
|
||||
already true; verify bubble max-widths don't clip at accessibility sizes by allowing
|
||||
`maxWidth: 82%` to grow to 92% above `.accessibility1`. Wall size is fixed (decorative).
|
||||
5. **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).
|
||||
6. **Touch targets:** all interactive controls ≥44 pt (web `.btn` min-height 42 px; iOS
|
||||
standard). Composer buttons and toolbar icons already comply via SF Symbol sizing —
|
||||
verify segmented control height ≥44.
|
||||
7. **VoiceOver order:** header status → transcript (oldest→newest) → composer, matching
|
||||
visual order; the wall is hidden from VO (§6.1).
|
||||
|
||||
---
|
||||
|
||||
## 9. App Icon & Branding Assets
|
||||
|
||||
1. **AppIcon:** generate from `hope_companion/scripts/gen-icons.mjs` output
|
||||
(`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) to `AppIcon.appiconset`. Fallback if regeneration is undesirable:
|
||||
use the existing PNG directly; it has alpha and correct dimensions (verified via sips).
|
||||
2. **HopeGlyph asset:** port `favicon.svg` into 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 with `Path`.
|
||||
3. **No raster assets** for the LED wall — procedural only (§6.1).
|
||||
|
||||
---
|
||||
|
||||
## 10. Phased Implementation Plan
|
||||
|
||||
### Phase A — Brand foundation (low risk, high impact)
|
||||
- [ ] Add `HopeColor` token 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 by `ConversationStateModel.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 `.idle` per §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)
|
||||
|
||||
1. **Brand:** app icon set populated; HopeGlyph visible in conversation + settings
|
||||
headers; no system palette colors on conversation surfaces (grep for `.blue`,
|
||||
`.green`, `.orange`, `.red` in `Sources/Features` returns only token-enum internals).
|
||||
2. **LED wall:** renders 21×21 grid; dot opacity/size respond to `agentLevel` while
|
||||
speaking (verifiable in simulator via TTS) and to `micLevel` while listening
|
||||
(device-only); idle shows static dim matrix; state colors match §5.1 table exactly.
|
||||
3. **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 `.idle` or backgrounded.
|
||||
4. **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.
|
||||
5. **Composer:** always visible; Voice/Text segmented control switches mode and syncs
|
||||
with `SettingsStore.textMode`; Start/End states per §6.4 table.
|
||||
6. **Accessibility:** reduce-motion yields static wall + no pulses; status announcements
|
||||
fire on speaking/listening/failed transitions only; all interactive targets ≥44 pt.
|
||||
7. **Build:** zero warnings; simulator build/run clean (XcodeBuildMCP); existing
|
||||
connect/disconnect/reconnect matrix from PLAN.md Phase 8 still passes.
|
||||
8. **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.
|
||||
Reference in new issue
Block a user