Commit graph

153 commits

Author SHA1 Message Date
Bruno Deanoz
feb326fc40 Auto-search detection + live reasoning preview
Auto-search:
- detectSearchQuery() pattern-matches common search prompts in DE+EN
  (weather, news, prices, scores, current events, etc.)
- send() enables webSearch automatically when detected, no manual
  pill needed. Manual pill still works as explicit override.
- 30+ patterns ported from backend lib/chat/detection.ts

Live reasoning preview:
- ReasoningBlock shows first meaningful line as preview label when
  collapsed (max 80 chars, ellipsis). Updates live during streaming.
- "Gedankengang" header + preview line stacked vertically.
- Preview hidden when block is expanded (full text visible).

Tests: 4 new tests for detectSearchQuery (DE patterns, EN patterns,
normal prompts not triggered, short input ignored).
2026-06-24 10:30:52 +02:00
Bruno Deanoz
77d2d8e5df Redesign SourcesBlock — horizontal chips + detail cards
Before: sources hidden behind a collapsed "Quellen (3)" toggle,
vertical stack of flat cards, requires click to see anything.

After:
- Always-visible horizontal scrollable chips with numbered colored
  circles (indigo/violet/cyan/emerald/amber/red/pink/blue cycling)
- Each chip shows the domain name, tap opens the URL
- Search query shown above sources when available
- "Details anzeigen" expands to full cards with colored left bar,
  title, domain, and snippet
- Perplexity/Gemini-inspired compact visual style
2026-06-24 10:23:24 +02:00
Bruno Deanoz
24d6b2148a Add image generation + streaming recomposition isolation
Image generation:
- KaizenApi.generateImage() calls POST /api/v1/image-gen
- Bild mode pill in ChatInput triggers image generation instead of chat
- Generated image displayed as attachment in assistant message
- Conversation created + title generated automatically

Streaming isolation (from previous uncommitted work in ChatScreen):
- liveStream StateFlow emits content during streaming
- Only the active MessageRow subscribes via collectAsState()
- Other LazyColumn items stay completely static during streaming
2026-06-24 10:18:33 +02:00
Bruno Deanoz
f746c603b0 Isolate streaming recomposition to active bubble only
During streaming, content updates now flow through a StateFlow
(liveStream) instead of mutating the SnapshotStateList on every chunk.
Only the single MessageRow with id == streamingMessageId subscribes to
the flow via collectAsState(). All other items in the LazyColumn stay
completely static during streaming.

Before: every chunk mutated messages[idx] → entire LazyColumn evaluated
After: only the live bubble recomposes, list mutation happens once at
stream end

Auto-scroll split into two LaunchedEffects: one for list size changes,
one for liveStream emissions.
2026-06-24 10:15:24 +02:00
Bruno Deanoz
40db869717 Extract ChatScreenViewModel from God Composable
Move all business logic and state (~35 variables, ~15 functions) from
the 1540-line ChatScreen @Composable into ChatScreenViewModel.

ChatScreen is now a thin rendering layer (~430 lines) that reads VM
state and wires UI events to VM methods. Android-framework-only concerns
(ActivityResultLaunchers, DrawerState, LazyListState, keyboard insets)
stay in the Composable.

Changes:
- New ChatScreenViewModel.kt: messages, streaming, conversation mgmt,
  sidebar actions, file upload, voice recording, TTS, live call, unlock
- ChatScreen.kt: pure UI, delegates all logic to vm.*
- Haptics.kt: add createHaptics(Context) factory for non-Composable use
- SessionViewModel: store nullable for test construction
- MainActivity: instantiates ChatScreenViewModel

Tests: 12 new unit tests for ChatScreenViewModel (state transitions,
newChat reset, mode toggles, edit/delete, password dialog lifecycle).
All 90 tests pass.
2026-06-24 10:09:40 +02:00
Bruno Deanoz
3b73e15376 Security hardening + performance optimization pass
Security:
- Fix JSON injection in deleteMessage (raw string → serialized DTO)
- Separate OkHttpClient for streaming (30s timeout on normal requests,
  infinite only for chat stream)
- Disable backup (EncryptedSharedPrefs without KeyStore → crash on restore)
- Block cleartext traffic explicitly
- Strip server URLs from log output

Performance:
- Share OkHttp connection pool across KaizenApi + LiveClient
- Eliminate duplicate fetchMe/fetchDefaultModel call on login
- Parallelize prefetchMissing (was sequential per conversation)
- Stream file uploads directly from ContentResolver (no full readBytes)
- Cache assistant message index during streaming (O(1) vs O(n) per chunk)
- Build chat history before adding assistant placeholder (no filter needed)

Bugs:
- Fix send() race condition (isStreaming set before async work)
- Move voice recording file read to IO dispatcher
2026-06-24 09:38:54 +02:00
Bruno Deanoz
392e582084 fix: show toast when Live Call unavailable instead of silent no-op
The call button did nothing when no live model was available (empty
lambda). Now shows a toast explaining the issue.
2026-06-24 07:12:13 +02:00
Bruno Deanoz
ff66b80966 Wire Live Call end-to-end: turn persistence, model check, permission
Turn persistence: onTurnEnd handler creates conversation if needed,
adds user + assistant messages to UI list, caches in Room. Server
already persists via WS-Server — app only needs local state sync.

Model check: CallButton only triggers Live Call when a vertex:*live*
model is in the catalog (liveAvailable). Falls back to PTT otherwise.

Permission: RECORD_AUDIO runtime request via ActivityResultContracts
before starting the call. Denied → toast, no crash.

Title generation: endLiveCall triggers generateTitle + sidebar refresh,
same pattern as text chat.
2026-06-24 03:25:32 +02:00
Bruno Deanoz
31278f2440 Add Live Call UI overlay + wiring in ChatScreen
LiveCallOverlay — fullscreen composable: KaizenOrb (amplitude-reactive,
streaming/recording variants), duration timer, status text (connecting/
listening/speaking), animated subtitle (transcript/response), end call
button (red gradient circle with PhoneOff icon), interrupt on orb tap.

ChatScreen wiring: CallButton → startLiveCall() → binds LiveCallService
→ Foreground Service with AudioPipeline + LiveClient. Overlay renders
above chat content with fade animation. endLiveCall() unbinds + stops.

ChatInput: onCallClick callback propagated from CallButton to ChatScreen.

i18n: live_connecting/listening/ready/speaking/end/reconnecting in de+en.
KaizenIcons: added PhoneOff (Lucide).
2026-06-24 03:14:08 +02:00
Bruno Deanoz
65ea3a5656 Add Live Call transport layer (P10 foundation)
LiveProtocol — type-safe Kaizen WS protocol serialization/parsing,
mirrors lib/live/protocol.ts 1:1 (setup/resume/audio/text/interrupt/end
+ all 12 server event types).

LiveClient — OkHttp WebSocket client with auto-reconnect (exponential
backoff, max 5 attempts), resume protocol (sessionId + lastSeqId),
backpressure handling, terminal error detection. 15s ping interval
keeps mobile NAT alive.

AudioPipeline — bidirectional audio: AudioRecord (16kHz PCM16 mono,
VOICE_COMMUNICATION for hardware AEC/noise suppression) → 100ms chunks
→ Base64. AudioTrack (24kHz PCM16 mono, LOW_LATENCY, MODE_STREAM) ←
server chunks. Mic-ducking during KI speech (RMS threshold gating).

LiveCallService — Foreground Service (MICROPHONE type), survives
rotation/backgrounding. Notification with duration timer + end button.
Status state machine (IDLE→CONNECTING→LISTENING→SPEAKING). Orchestrates
LiveClient + AudioPipeline, emits onTurnEnd for tree integration.
2026-06-24 03:06:48 +02:00
Bruno Deanoz
25fbb17d18 fix: token badge text wrapping on long model names
Added maxLines=1 and softWrap=false to prevent "0 / 1M" from breaking
into two lines when the top bar is compressed by a long model name.
2026-06-24 03:02:33 +02:00
Bruno Deanoz
bb53b24655 fix: TTS read-aloud downloads audio via OkHttp before playing
MediaPlayer.setDataSource(url) with HTTPS URLs can silently fail due to
certificate issues, redirect handling, or listener race conditions (listeners
were set after prepareAsync). Fix: download the WAV file via OkHttp (same
client as all other network calls) to a temp file, set listeners before
prepareAsync, play from local path. Temp file cleaned up on completion/error.
2026-06-24 01:33:23 +02:00
Bruno Deanoz
287cdb4c6b feat: branch navigation for edit/regenerate instead of destructive delete
Edit creates a sibling branch (same parentId) instead of deleting messages.
Regenerate creates a new assistant branch. BranchNavigator UI shows chevron
arrows with "1/3" counter on messages with siblings. Tree state (activeRootId,
activeChild) synced to server via PATCH. New ChatTree data structure mirrors
the web frontend's tree.ts. StoredMessage.parentId parsed from API response.
2026-06-24 01:29:02 +02:00
Bruno Deanoz
d69e2bcf68 fix: block title-leaking actions on locked conversations
Locked chats could leak their real title through sidebar search,
"Generate Title", and "Duplicate" — all three now blocked.
2026-06-23 20:23:51 +02:00
Bruno Deanoz
7aefd7ba70 fix: migrate ClickableText to LinkAnnotation, add server version check
Replace deprecated ClickableText with LinkAnnotation.Url + Text (Compose
native URI handling). Add GET /api/v1/meta call after login and on each
app resume to detect API version skew — shows dismissible amber banner.
2026-06-23 14:51:20 +02:00
Bruno Deanoz
430d5b609e style: flat sidebar user row — remove heavy card background
No more card background, border, shadow on user profile. Just a flat
row with thin separator line above, avatar 34dp, subtle icon buttons.
2026-06-23 14:31:04 +02:00
Bruno Deanoz
ecb89019dc fix: move new-chat icon left of token badge in top bar 2026-06-23 14:27:24 +02:00
Bruno Deanoz
9f7e3fb8b0 fix: center text and icons vertically in input capsule
Column gets heightIn(min=64dp) + Arrangement.Center so content
sits in the middle. Placeholder maxLines=1 to prevent overflow dots.
2026-06-23 14:24:21 +02:00
Bruno Deanoz
1d47e05dbf style: taller input (64dp), rounder corners (34dp), centered text 2026-06-23 14:20:46 +02:00
Bruno Deanoz
e683419d96 style: taller capsule input matching Gemini proportions
56dp min height, 30dp corner radius, generous inner padding.
Icons 40dp touch targets, 22dp icon size. Text 16sp centered.
High opacity (0.92/0.97) for solid feel. 16dp horizontal margin.
2026-06-23 14:18:23 +02:00
Bruno Deanoz
fb8f21dfd8 style: compact capsule input like Gemini
Tighter row: 36dp icons, 6dp padding, 15sp text, capsule shape.
Higher opacity (0.90/0.96) for solid feel against background.
All action buttons (Send/Call/Stop) matched to 36dp.
2026-06-23 14:02:37 +02:00
Bruno Deanoz
e56bef05f6 style: rounder input card, stronger shadow, higher opacity
pill shape (32dp radius), level4 shadow for stronger separation from
background, tintAlpha 0.88/0.94 (more opaque, cleaner), tighter
horizontal margin (12dp).
2026-06-23 13:59:12 +02:00
Bruno Deanoz
4958ca8429 feat: single-row input, pills inside card, new-chat button
ChatInput redesigned: single row (Plus | Text | Tune | Mic | Send).
Pills hidden by default, expandable via tune icon with slide animation
inside the same GlassSurface card. Shape lg (28dp) instead of pill.

ModePillsRow moved from separate floating row into ChatInput as
pillsContent lambda. Bottom dock scrim simplified.

New Chat button (PenLine icon) added to top bar next to TokenBadge.

LazyColumn bottom padding 260→140dp (dock is now smaller).
2026-06-23 13:57:28 +02:00
Bruno Deanoz
ad98b9f499 feat: redesign app icon + splash to match glass-lens orb
Foreground: opaque plastic orb → translucent glass-lens with subtle
amber tint, lighter speculars, thinner rims. 10 layers → 8 layers.

Backgrounds: warm cream → neutral off-white (light), cleaner obsidian
(dark). Matches app's actual background colors.

Monochrome: cleaner gradient, thinner rim.

Splash screen uses the same foreground via windowSplashScreenAnimatedIcon.
2026-06-23 13:48:25 +02:00
Bruno Deanoz
1c9491e3dc feat: text selection, modern sidebar profile, markdown spacing
Text selection: SelectionContainer wraps both user and assistant
messages — long-press to select, copy, share.

Sidebar profile: avatar 30→38dp with radial gradient, rounded lg
shape, icon buttons with subtle backgrounds, bigger text (15sp W400).

Markdown: heading spacing 18→24/20dp, list items better aligned
(8dp gap, bullet muted), blockquote bar 4dp inset + 12dp gap.
2026-06-23 13:45:23 +02:00
Bruno Deanoz
95672d3124 fix: mode pills more visible, action buttons larger, revert Brain icon
Pills: higher background alpha, stronger borders, removed muted text
opacity, bigger padding (14x8dp), icon 16dp, text 13sp W400/W500.

Action buttons: Plus/Mic/Call/Send/Stop 38dp→42dp, icons 19→22dp.

Brain icon reverted to Lucide — custom version too complex at 16dp.
2026-06-23 13:11:47 +02:00
Bruno Deanoz
3c6a8911c4 feat: custom 1.0px icons for highest-visibility touchpoints
Hand-drawn Menu, Plus, Mic, AudioLines, Brain, SlidersHorizontal,
Globe, Image with 1.0px stroke (vs Lucide's 1.5px) to match the
lighter W300 typography. Registered in KaizenIcons — all usages
update automatically, no call-site changes.
2026-06-23 13:05:50 +02:00
Bruno Deanoz
0f6e55d77a feat: upgrade NetworkImageCache — disk cache, shared client, crossfade
Shared OkHttpClient via KaizenApi.imageClient (reuses connection pool),
file-based disk cache in cacheDir/img (survives app restart), 32MB
size-based memory limit instead of 50-entry count, Crossfade animation
on image load.
2026-06-23 13:00:49 +02:00
Bruno Deanoz
38678a2add refactor: extract hardcoded colors into KaizenSemanticColors
Single source of truth for error, success, indigo, link, and dropdown
colors. Replaces ~25 scattered Color(0xFF...) literals across 10 files.
Syntax highlighting and glass-surface tints intentionally left local.
2026-06-23 12:56:09 +02:00
Bruno Deanoz
75ca8c0e7b feat: visual refresh — lighter typography, message animations, richer themes
Typography: body text W400→W300 for airier feel, headings scaled up
(H1 26sp, H2 22sp, H3 18sp), W600/W500 weight hierarchy for contrast.

Animations: animateItem() on LazyColumn messages — 350ms fade-in,
spring placement, 200ms fade-out.

Themes: blobs 20% larger + better full-screen distribution, higher
alpha on OLED (0.48 peak), light mode factor 0.65→0.78, reduced
vignette, distinct primaryDeep per theme for richer gradients.
2026-06-23 12:22:38 +02:00
Bruno Deanoz
fbead82ada fix: increase chat content padding to prevent overlap with top/bottom bars 2026-06-23 07:17:12 +02:00
Bruno Deanoz
fd684fff2e fix: scroll button direction follows scroll position
Single button instead of two mutually exclusive ones. Arrow direction
is based on proximity to bottom: near bottom → arrow up (scroll to top),
not near bottom → arrow down (scroll to end). Updates instantly as the
user scrolls. Hidden when already at the target end.

Old logic had an if/else that always showed ArrowUp first (because
!atTop was always true when scrolled down), making ArrowDown unreachable
in most scroll positions.
2026-06-23 01:44:47 +02:00
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
421b29ef5a feat: redesign ReasoningBlock + SourceCards to match design system
ReasoningBlock: accent-tinted background + gradient border (matches pill
language), KaizenShapes.md (16dp radius), larger padding, better line
height (20sp), softer icon tint. Replaces flat gray box with solid border.

SourceCards: gradient border instead of solid, KaizenShapes.md, shows
domain instead of full URL in the link line, slightly larger padding.

Both blocks now use the same visual language as the rest of the app —
gradient borders, accent tinting, squircle shapes.
2026-06-23 01:35:45 +02:00
Bruno Deanoz
b2df7e9330 feat: modern message action icons — larger, distinctive
Icons swapped to more distinctive Lucide variants:
- Copy → ClipboardCopy (clipboard with arrow, instantly recognizable)
- Edit → SquarePen (filled square with pen, modern)
- Regenerate → RotateCcw (single-arrow rotate, cleaner than RefreshCw)
- Delete/Volume stay (already good)

Size: 36dp touch target, 20dp icons (was 32/18). Tint alpha 0.50→0.55
for better visibility. Added ClipboardCopy, SquarePen, RotateCcw, PenLine
to KaizenIcons registry.
2026-06-23 01:33:59 +02:00
Bruno Deanoz
8f72afd714 feat: edit and resend user messages
Pencil icon on user messages (next to copy, before delete). Tap removes
the edited message and all messages after it from the UI + server, then
puts the text back into the input field for editing and resending.

Only shown on user messages with text content (not voice-only), and only
when not currently streaming.
2026-06-23 01:30:52 +02:00
Bruno Deanoz
44ff82a66d fix: voice messages appear in wrong order after reopening chat
Voice auto-send was saving messages to the server but not caching them
in Room with correct sortOrder. When navigating away and back, the app
loaded from Room cache (empty for voice messages) then from server,
where the tree-based ordering could differ from insertion order.

Now caches both user and assistant messages in Room with proper sortOrder
after the server save succeeds, matching the normal send() flow.
2026-06-23 01:28:53 +02:00
Bruno Deanoz
1899f2cff1 fix: voice messages show as compact pill instead of file attachment chip
Audio-only user messages (no text, only audio attachments) now render as
a clean "🎤 Sprachnachricht" pill instead of the ugly filename chip
(voice_1782170416740.m4a). Detects via attachments.all { kind == "audio" }
&& content.isBlank(). Regular file attachments are unaffected.
2026-06-23 01:24:56 +02:00
Bruno Deanoz
fb6bc981d1 feat: web search provider picker for Vertex models (Google vs Enterprise)
When search mode is active and a vertex: model is selected, a second pill
appears next to "Suche" with a dropdown to choose between Google Search
(default) and Enterprise Web Search grounding. The selection is sent as
webSearchProvider in the chat request body. Matches the web frontend's
OptionPill for webSearchProvider.

Only visible for Vertex models — OpenRouter uses agentive web_search
server-tool which has no provider toggle.
2026-06-23 01:23:03 +02:00
Bruno Deanoz
4150936b18 fix: sampling panel too large + pills barely visible
Sampling panel: width 260→220dp, padding reduced, spacing 14→6dp between
parameters, checkbox 20→16dp, slider height capped at 28dp, removed extra
spacer between label row and slider. Panel is now ~40% shorter.

Pills visibility: inactive background alpha raised (dark 0.06→0.10, light
0.04→0.07), border alpha doubled (dark 0.08→0.14, light was white-on-light
invisible, now uses black 0.10), icon tint 0.60→0.75, text 0.70→0.80.
Active state also strengthened. Pills are now clearly visible in both modes.
2026-06-23 01:17:56 +02:00
Bruno Deanoz
d289466276 feat: live voice waveform replaces static recording indicator
32-bar waveform visualization that reacts to microphone amplitude in
real-time. MediaRecorder.getMaxAmplitude() polled every 60ms, normalized
to 0-1 range, fed into a scrolling bar chart (new bars enter right, old
shift left). Bars use accent color — louder = taller + more opaque,
quiet = short + muted. Replaces the static "Aufnahme läuft…" text.
2026-06-23 01:15:51 +02:00
Bruno Deanoz
20a71adb99 feat: voice recording auto-send + recording indicator UI
Voice messages now upload and send automatically when recording stops —
no manual send button needed. The input area transforms during recording:
text field replaced by a centered pulsing red dot + "Aufnahme läuft…" label,
with a single centered red stop button below.

The old flow (record → attachment chip → manual send) treated voice like
a file attachment. New flow: tap mic → record → tap stop → auto-upload →
auto-send → stream response. Matches the expected voice message UX.
2026-06-23 01:12:48 +02:00
Bruno Deanoz
497f0a82f7 feat: redesign mode pills and dropdowns
Pills: extracted shared ModePill composable — thinner border (0.8dp gradient
instead of 1.2dp solid), subtler inactive background, smaller text (12.5sp),
compact padding (12x7 instead of 13x8). Active state uses accent gradient
border instead of solid color.

Reasoning dropdown: styled container (dark glass bg, 16dp shadow, squircle
shape), check icon on selected preset, cleaner row layout.

Sampling popover: same styled container, custom checkbox (squircle with accent
tint instead of Material Checkbox), disabled state dims labels and track,
wider panel (260dp), more spacing between parameters.
2026-06-23 01:07:57 +02:00
Bruno Deanoz
fcac983201 fix: flat message action icons — remove background box
Clean flat icons without bordered container. 32dp touch target (circle clip
for ripple), 18dp icon size, no background fill. Modern minimal look.
2026-06-23 01:04:30 +02:00
Bruno Deanoz
dea379e237 fix: increase message spacing from 16dp to 24dp 2026-06-23 01:02:47 +02:00
Bruno Deanoz
0ade063b34 feat: improve message action buttons — larger, visible, subtle background
Buttons: 30dp→34dp with squircle shape (KaizenShapes.sm), subtle tinted
background (dark: white 4%, light: black 2.5%), icons 16→17dp. Tint alpha
raised from 0.35→0.50 (copy/regenerate/volume) and 0.6→0.70 (delete) so
buttons are actually visible in both light and dark mode. Spacing 2→4dp
between buttons, top padding 4→6dp.
2026-06-23 01:02:07 +02:00
Bruno Deanoz
10ece6d72f feat: rounder chat items with card-style borders
Items use KaizenShapes.md (16dp radius) instead of sm (12dp), more spacing
(4dp gap), horizontal padding, and subtle glass borders on inactive items.
Active items get accent gradient border + shadow. Matches web's rounded-xl
card treatment.
2026-06-23 00:59:58 +02:00
Bruno Deanoz
558b1be675 feat: sidebar redesign — glasier look, functional search, generate title, duplicate
Design: lower tintAlpha (0.80/0.86) for more blob bleed-through, accent-tinted
gradient borders + level1 shadow on active items, specular borders on search bar
and user card, accent gradient avatar, separator line in context menu.

Features: live search filter, "Titel generieren" (Sparkles, manual POST with
{ manual: true }), "Duplizieren" (copies all messages with new IDs + parentId
chain), titleGenerated field on ConversationSummary/Entity (Room v4).
2026-06-23 00:56:58 +02:00
Bruno Deanoz
1645be8a97 feat: sync TTS voice from server — app reads ttsVoice from /api/v1/me
Voice preference set in web settings now automatically applies in the app.
Falls back to "Kore" if no server preference set.
2026-06-23 00:39:34 +02:00
Bruno Deanoz
c4884a3748 feat: TTS audio cache in Room — skip regeneration on replay
- MessageEntity gets ttsUrl + ttsVoice fields (Room version 2→3)
- MessageDao.getTtsUrl() checks cache before API call
- MessageDao.updateTts() saves URL + voice after generation
- readAloud() checks Room first, only calls /api/v1/audio-gen on miss
- Voice change invalidates cache automatically (query includes voice)
- Default voice "Kore" (Gemini-TTS)
2026-06-23 00:33:09 +02:00