Compare commits

..

2 commits

Author SHA1 Message Date
Bruno Deanoz
f855a26b31 fix: sidebar overlapping status bar on notch devices
statusBarsPadding() was on the inner Column (only pushed content down),
but the GlassSurface itself started at a fixed 12dp from the top edge —
on devices with tall notches (S25 Ultra) the sidebar header sat behind
the system clock/icons. Moved statusBarsPadding() to the GlassSurface
modifier so the entire glass panel starts below the status bar.
2026-06-23 01:41:07 +02:00
Bruno Deanoz
8099f463b8 docs: comprehensive CLAUDE.md update for 2026-06-23 design session
Added 12 new commit entries (sidebar redesign through ReasoningBlock
redesign), new bugs-fixed section, updated API contract (webSearchProvider),
Room schema (titleGenerated, v4), voice recording docs (auto-send +
waveform), new feature checkboxes (edit/resend, sidebar search, generate
title, duplicate, web search provider picker).
2026-06-23 01:39:44 +02:00
2 changed files with 34 additions and 6 deletions

View file

@ -22,7 +22,7 @@ Make the **native Android app** (`kaizen-app`) a valid daily-driver that talks t
- **Auth:** every request carries `Authorization: Bearer kzn_...`. Backend resolves via `requireActiveUserOrToken` (`lib/auth/guard.ts`). - **Auth:** every request carries `Authorization: Bearer kzn_...`. Backend resolves via `requireActiveUserOrToken` (`lib/auth/guard.ts`).
- **Token acquisition:** in-app login (`POST {baseUrl}/api/v1/auth/token` with `{ email, password, name }`). Consumer "Sign in" pattern — no copy-paste. - **Token acquisition:** in-app login (`POST {baseUrl}/api/v1/auth/token` with `{ email, password, name }`). Consumer "Sign in" pattern — no copy-paste.
- **Chat:** `POST {baseUrl}/api/v1/chat``{ model, messages: [{ role, content }], reasoningEffort?, preferThroughput? }`. Response is `text/plain` chunked stream (NOT SSE). Parsed by `StreamConsumer.Incremental` in `net/StreamConsumer.kt`. - **Chat:** `POST {baseUrl}/api/v1/chat``{ model, messages: [{ role, content }], reasoningEffort?, preferThroughput?, webSearch?, webSearchProvider?: "google"|"enterprise", temperature?, topP?, topK? }`. Response is `text/plain` chunked stream (NOT SSE). Parsed by `StreamConsumer.Incremental` in `net/StreamConsumer.kt`.
- **Sentinels** (`lib/chat/constants.ts`): `\x00` USAGE, `\x01` REASONING, `\x02` SOURCES, `\x03` TOOL, `\x04` QUERY. App currently strips all sentinels and renders only visible content. - **Sentinels** (`lib/chat/constants.ts`): `\x00` USAGE, `\x01` REASONING, `\x02` SOURCES, `\x03` TOOL, `\x04` QUERY. App currently strips all sentinels and renders only visible content.
- **Models:** `GET {baseUrl}/api/v1/models``{ models: [{ id, name?, provider? }] }`. - **Models:** `GET {baseUrl}/api/v1/models``{ models: [{ id, name?, provider? }] }`.
- **Conversations:** `GET /api/v1/conversations``{ conversations, hasMore }`. `POST /api/v1/conversations` → create. `GET /api/v1/conversations/[id]``{ messages }`. `POST /api/v1/conversations/[id]/messages` → persist. `POST /api/v1/conversations/[id]/title` → auto-title. - **Conversations:** `GET /api/v1/conversations``{ conversations, hasMore }`. `POST /api/v1/conversations` → create. `GET /api/v1/conversations/[id]``{ messages }`. `POST /api/v1/conversations/[id]/messages` → persist. `POST /api/v1/conversations/[id]/title` → auto-title.
@ -159,7 +159,7 @@ Architecture decision document: `OFFLINE_CACHE_ADD.md`.
**Stack:** Room 2.8.4 + KSP 2.2.10-2.0.2. `room-ktx` merged into `room-runtime` since Room 2.7 (no separate dep needed). `exportSchema = false`, `fallbackToDestructiveMigration()` — it's a cache, not a DB. On schema changes the SQLite is wiped; cache rebuilds on next server fetch. **Stack:** Room 2.8.4 + KSP 2.2.10-2.0.2. `room-ktx` merged into `room-runtime` since Room 2.7 (no separate dep needed). `exportSchema = false`, `fallbackToDestructiveMigration()` — it's a cache, not a DB. On schema changes the SQLite is wiped; cache rebuilds on next server fetch.
**Schema:** **Schema:**
- `conversations` — id, title, locked, pinned, updatedAt, cachedAt - `conversations` — id, title, locked, pinned, updatedAt, cachedAt, titleGenerated
- `messages` — id, conversationId (FK CASCADE), role, content, attachments (JSON via TypeConverter), sortOrder - `messages` — id, conversationId (FK CASCADE), role, content, attachments (JSON via TypeConverter), sortOrder
**Data flow:** **Data flow:**
@ -268,6 +268,17 @@ All committed on `main`. Compile-verified on Mac (`./gradlew :app:compileDebugKo
| `9e1f30d` | **Voice recording + TTS read-aloud** — Mic button wired to `MediaRecorder` (AAC/M4A), tap-to-record, auto-upload as audio attachment. Volume button on assistant messages → `POST /api/v1/audio-gen` → Gemini-TTS → `MediaPlayer` playback. `RECORD_AUDIO` permission | | `9e1f30d` | **Voice recording + TTS read-aloud** — Mic button wired to `MediaRecorder` (AAC/M4A), tap-to-record, auto-upload as audio attachment. Volume button on assistant messages → `POST /api/v1/audio-gen` → Gemini-TTS → `MediaPlayer` playback. `RECORD_AUDIO` permission |
| `c4884a3` | **TTS audio cache in Room**`ttsUrl` + `ttsVoice` on `MessageEntity` (Room v3), skip API call on replay with same voice. Voice change invalidates cache | | `c4884a3` | **TTS audio cache in Room**`ttsUrl` + `ttsVoice` on `MessageEntity` (Room v3), skip API call on replay with same voice. Voice change invalidates cache |
| `1645be8` | **Server-synced TTS voice** — reads `ttsVoice` from `GET /api/v1/me`, uses it for all TTS calls. Voice set in web settings applies in app automatically | | `1645be8` | **Server-synced TTS voice** — reads `ttsVoice` from `GET /api/v1/me`, uses it for all TTS calls. Voice set in web settings applies in app automatically |
| `558b1be` | **Sidebar redesign** — glasier look (tintAlpha 0.80/0.86), functional search (live title filter), "Titel generieren" (Sparkles, `POST .../title` with `{ manual: true }`), "Duplizieren" (copies messages with new IDs + parentId chain), `titleGenerated` on ConversationSummary/Entity (Room v4), accent gradient borders on active items + user card |
| `10ece6d` | **Rounder chat items** — KaizenShapes.md (16dp) instead of sm (12dp), 4dp gap, card-style glass borders on inactive items, horizontal padding |
| `497f0a8` | **Mode pills redesign** — extracted shared `ModePill` composable, thinner gradient borders (0.8dp), compact padding (12×7dp), styled reasoning dropdown (check icon on selected), compact sampling panel (220dp width, 28dp slider height, 16dp custom checkbox) |
| `20a71ad` | **Voice recording auto-send** — tap mic → record → tap stop → auto-upload → auto-send → stream response (no manual send). Recording UI: live 32-bar waveform visualization (amplitude from `MediaRecorder.getMaxAmplitude()` polled every 60ms), centered red stop button |
| `d289466` | **Live voice waveform** — scrolling bar chart reacts to microphone amplitude in real-time, bars use accent color, louder = taller + more opaque |
| `fb6bc98` | **Web search provider picker** — for Vertex models: Google vs Enterprise Web Search dropdown pill, `webSearchProvider` sent in chat request |
| `1899f2c` | **Voice message display** — audio-only user messages render as compact "🎤 Sprachnachricht" pill instead of ugly filename chip |
| `44ff82a` | **Voice message ordering fix** — caches user + assistant messages in Room with correct `sortOrder` after server save (was missing, caused wrong order on reload) |
| `8f72afd` | **Edit and resend user messages** — SquarePen icon on user messages, removes message + all after it, puts text back in input field |
| `b2df7e9` | **Modern message action icons** — ClipboardCopy (copy), SquarePen (edit), RotateCcw (regenerate), 36dp touch target, 20dp icons |
| `421b29e` | **ReasoningBlock + SourceCards redesign** — accent-tinted background + gradient borders, KaizenShapes.md (16dp radius), domain instead of full URL in source cards |
**Earlier UI/feel work (Phase 0 prototype → feel-first):** **Earlier UI/feel work (Phase 0 prototype → feel-first):**
- Liquid glass styling, floating panels, glassmorphic sidebar - Liquid glass styling, floating panels, glassmorphic sidebar
@ -313,6 +324,19 @@ All committed on `main`. Compile-verified on Mac (`./gradlew :app:compileDebugKo
- **LoginScreen used `Modifier.shadow`** — violated design system pillar 4 (multi-layer shadows). Fix: migrated to `kaizenShadow` (level1 for fields, level2 for button). - **LoginScreen used `Modifier.shadow`** — violated design system pillar 4 (multi-layer shadows). Fix: migrated to `kaizenShadow` (level1 for fields, level2 for button).
- **Migration 0030 crash**`pnpm db:generate` bundled the entire org schema (0026-0029) + `tts_voice` into one migration, causing duplicate CREATE TABLE / ADD COLUMN failures. Fix: reduced 0030 to only `ALTER TABLE users ADD COLUMN IF NOT EXISTS tts_voice text`. - **Migration 0030 crash**`pnpm db:generate` bundled the entire org schema (0026-0029) + `tts_voice` into one migration, causing duplicate CREATE TABLE / ADD COLUMN failures. Fix: reduced 0030 to only `ALTER TABLE users ADD COLUMN IF NOT EXISTS tts_voice text`.
### Bugs fixed + features (2026-06-23 — design + sidebar + voice session)
- **Sidebar too opaque / bland** — tintAlpha was 0.88/0.92 (barely any blob bleed-through). Fix: lowered to 0.80/0.86, added gradient borders on active items + user card + search bar.
- **Sidebar search was placeholder-only** — just showed "Suchen…" text. Fix: functional `BasicTextField` with live case-insensitive title filter + X clear button.
- **Missing sidebar features** — no "Titel generieren" or "Duplizieren". Fix: added `SidebarAction.GenerateTitle` (calls `POST .../title` with `{ manual: true }`, only shown when `titleGenerated == false`) and `SidebarAction.Duplicate` (fetches messages, creates new conversation with copied messages + parentId chain).
- **Pills barely visible in dark + light mode** — inactive background alpha was too low (0.06/0.04), border was invisible (white-on-light). Fix: raised all alpha values, borders now use visible colors in both modes.
- **Sampling panel too large** — overlay covered most of the chat. Fix: width 260→220dp, slider height capped at 28dp, spacing reduced, checkbox 20→16dp.
- **Voice messages shown as ugly file chip**`voice_1782170416740.m4a` filename displayed. Fix: audio-only user messages render as "🎤 Sprachnachricht" pill.
- **Voice messages wrong order after reload** — auto-send code saved to server but not to Room cache. Fix: added Room caching with correct `sortOrder`.
- **Message spacing too tight** — 16dp between messages. Fix: increased to 24dp.
- **No edit/resend for user messages** — only copy + delete were available. Fix: SquarePen button removes message + successors, puts text back in input.
- **`webSearchProvider` not sent by app** — Vertex's Google vs Enterprise search toggle was missing. Fix: added `webSearchProvider` to `ChatRequest`, dropdown pill in mode pills row (only visible for `vertex:` models with search active).
## What's NOT done yet (app, priority order) ## What's NOT done yet (app, priority order)
### 1. Remaining visual/UX gaps (daily-drive blockers) ### 1. Remaining visual/UX gaps (daily-drive blockers)
@ -347,8 +371,12 @@ These are what separate "prototype" from "daily-driver":
- [x] **Attachments** — image/file upload (multi-select for gallery + files), attachment preview in messages, display in loaded conversations, camera capture, attachment picker menu (camera/gallery/file) - [x] **Attachments** — image/file upload (multi-select for gallery + files), attachment preview in messages, display in loaded conversations, camera capture, attachment picker menu (camera/gallery/file)
- [x] **Reasoning/thinking display**`ReasoningBlock` (collapsible), parses `\x01` sentinel - [x] **Reasoning/thinking display**`ReasoningBlock` (collapsible), parses `\x01` sentinel
- [x] **Web search sources**`SourcesBlock` (collapsible, clickable links via ACTION_VIEW), parses `\x02`/`\x04` sentinels. `webSearch: true` in ChatRequest triggers agentive search - [x] **Web search sources**`SourcesBlock` (collapsible, clickable links via ACTION_VIEW), parses `\x02`/`\x04` sentinels. `webSearch: true` in ChatRequest triggers agentive search. Vertex: Google vs Enterprise Web Search picker (`webSearchProvider` in request body, dropdown pill only for `vertex:` models)
- [x] **Tool call display**`ToolEventsBlock`, parses `\x03` sentinel, status icons per step - [x] **Tool call display**`ToolEventsBlock`, parses `\x03` sentinel, status icons per step
- [x] **Edit + resend user messages** — SquarePen button removes message + all successors from UI + server, puts text back in input field for editing
- [x] **Sidebar search** — live case-insensitive title filter with clear button
- [x] **Generate title from sidebar** — Sparkles icon in 3-dot menu (only when `titleGenerated == false`), calls `POST .../title` with `{ manual: true }`
- [x] **Duplicate conversation** — Copy icon in sidebar 3-dot menu, creates new conversation with all messages copied (new IDs + parentId chain)
- [ ] **Native passkey login** (WebAuthn/FIDO2 on Android) - [ ] **Native passkey login** (WebAuthn/FIDO2 on Android)
- [ ] **Usage/cost display** - [ ] **Usage/cost display**
- [ ] **User context / KI-Profil** - [ ] **User context / KI-Profil**
@ -356,7 +384,7 @@ These are what separate "prototype" from "daily-driver":
### 5. Voice & Call Mode ### 5. Voice & Call Mode
**v1 — Pipeline (Weg A, uses existing backend):** **v1 — Pipeline (Weg A, uses existing backend):**
- [x] **Voice recording + send** — Android `MediaRecorder` (AAC/M4A, 44.1kHz/128kbps), tap-to-record mic button (red stop-square while recording), auto-upload via `/api/v1/upload`, sent as audio attachment via `/api/v1/chat`. Vertex understands audio natively; OpenRouter passes it as `input_audio`. Persisted in DB as normal attachment - [x] **Voice recording + auto-send** — Android `MediaRecorder` (AAC/M4A, 44.1kHz/128kbps), tap mic → live 32-bar waveform visualization (amplitude from `getMaxAmplitude()` polled 60ms, scrolling bars in accent color) → tap stop → auto-upload + auto-send (no manual send needed). Voice-only user messages render as compact "🎤 Sprachnachricht" pill. Room caching with correct `sortOrder` after server save
- [x] **TTS read-aloud** — Volume button on every assistant message. `POST /api/v1/audio-gen { prompt, kind: "speech", voice }` → Gemini-TTS → `MediaPlayer` streams the audio URL. Tap again to stop. Accent-colored icon during playback - [x] **TTS read-aloud** — Volume button on every assistant message. `POST /api/v1/audio-gen { prompt, kind: "speech", voice }` → Gemini-TTS → `MediaPlayer` streams the audio URL. Tap again to stop. Accent-colored icon during playback
- [x] **TTS cache**`ttsUrl` + `ttsVoice` fields on `MessageEntity` (Room v3). First play generates + caches; subsequent plays with same voice skip the API call. Voice change auto-invalidates - [x] **TTS cache**`ttsUrl` + `ttsVoice` fields on `MessageEntity` (Room v3). First play generates + caches; subsequent plays with same voice skip the API call. Voice change auto-invalidates
- [x] **Server-synced voice**`tts_voice` column on `users` table (migration 0030). Read from `GET /api/v1/me`, written via `PATCH /api/v1/me`. Web's `setCallVoice()` syncs to server. App reads on login/resume. Default "Kore" - [x] **Server-synced voice**`tts_voice` column on `users` table (migration 0030). Read from `GET /api/v1/me`, written via `PATCH /api/v1/me`. Web's `setCallVoice()` syncs to server. App reads on login/resume. Default "Kore"

View file

@ -119,8 +119,9 @@ fun KaizenSidebar(
GlassSurface( GlassSurface(
modifier = modifier modifier = modifier
.fillMaxHeight() .fillMaxHeight()
.statusBarsPadding()
.width(300.dp) .width(300.dp)
.padding(start = 12.dp, top = 12.dp, end = 4.dp, bottom = 12.dp), .padding(start = 12.dp, top = 8.dp, end = 4.dp, bottom = 12.dp),
shape = KaizenShapes.lg, shape = KaizenShapes.lg,
tintAlpha = if (isDark) 0.80f else 0.86f, tintAlpha = if (isDark) 0.80f else 0.86f,
shadowStack = KaizenShadows.level3, shadowStack = KaizenShadows.level3,
@ -129,7 +130,6 @@ fun KaizenSidebar(
Column( Column(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.statusBarsPadding()
.padding(16.dp) .padding(16.dp)
) { ) {
// ── Header ── // ── Header ──