diff --git a/AUTHENTIK_IDENTITY_PLAN.md b/AUTHENTIK_IDENTITY_PLAN.md new file mode 100644 index 0000000..fa9f725 --- /dev/null +++ b/AUTHENTIK_IDENTITY_PLAN.md @@ -0,0 +1,219 @@ +# Kaizen & Authentik β€” Enterprise Identity & Access Management (IAM) Design Document + +This design document outlines the architectural blueprint, security concepts, and native Android/Backend integration patterns to elevate **Kaizen** to a world-class, enterprise-grade AI assistant platform (comparable to ChatGPT Enterprise, Gemini Advanced, and Microsoft Copilot). + +By leveraging **Authentik** as our central Identity Provider (IdP), we eliminate custom user-management development, guarantee enterprise-grade compliance (ISO 27001, SOC2, GDPR), and deliver a premium, high-performance, and friction-free user experience. + +--- + +## πŸ›οΈ 1. System Architecture + +To achieve extreme scalability, security, and low-latency, we employ a decoupled, token-based, cloud-native architecture. + +``` + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Authentik (IdP) β”‚ + β”‚ - Master User Database β”‚ + β”‚ - MFA, Passkeys, OAuth2 β”‚ + β”‚ - Exposes JWKS (Keys) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ 1. Authenticate β”‚ 3. Fetch public keys (JWKS) β”‚ + β”‚ - OIDC / PKCE (Browser) β”‚ (Cached in memory, no DB lag) β”‚ + β”‚ - OR Native ROPC (Direct API) β”‚ β”‚ + β–Ό β–Ό β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ Kaizen App β”‚ 2. API Request β”‚ Kaizen Backend β”‚ 4. Read/Write β”‚ PostgreSQL β”‚ +β”‚ (Android) β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€>β”‚ (Next.js API) β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€>β”‚ Database β”‚ +β”‚ β”‚ (Bearer JWT) β”‚ β”‚ (User messages) β”‚ (Master/Repl) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Architectural Pillars: +1. **Single Source of Truth (SSOT):** Authentik manages all identities, credentials, MFA tokens, and group memberships. No passwords are saved or processed in the Kaizen Database. +2. **Offline JWT Verification (JWKS):** The Kaizen Backend does not query Authentik on every API request. Instead, it downloads Authentik's public keys (`/.well-known/jwks.json`) once, caches them, and validates incoming JWT signatures locally in microseconds. +3. **Database Scalability:** Since authentication is decoupled, the PostgreSQL database can be optimized with master-replica replication to handle high-frequency chat traffic seamlessly. + +--- + +## πŸ”‘ 2. Identity Protocols & Flows (Zero-Friction UX) + +To deliver a premium consumer feel while retaining strict enterprise security compliance, the system supports a dual-path authentication pipeline. + +### Flow A: Direct Native Login (Resource Owner Password Credentials - ROPC) +*Best for: Daily personal use, family accounts, and initial setup where zero UI redirection is desired.* + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” POST /application/o/token/ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Kaizen App β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€>β”‚ Authentik (IdP) β”‚ +β”‚ (Glassmorphic) β”‚<───────────────────────────────────────────────────── β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ Returns Access & Refresh Token β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` +1. **Native UI:** The user enters their E-Mail and password inside Kaizen's premium glassmorphic native fields. +2. **Direct Secure API:** The app makes a secure background HTTPS call directly to Authentik’s OAuth token endpoint using the `password` grant type. +3. **Immediate Session:** Authentik returns a standard OIDC JSON payload (Access Token, Refresh Token, ID Token). The user transitions seamlessly into the chat with **zero browser redirects**. + +--- + +### Flow B: Standard Authorization Code Flow with PKCE (RFC 7636) +*Best for: Corporate SSO (Microsoft Entra ID, Google Workspace, Okta), Multi-Factor Authentication (MFA), and passwordless Passkey registration.* + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” 1. Open Custom Tab (SAML/OIDC) β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Kaizen App β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€>β”‚ Authentik β”‚ +β”‚ (Native) β”‚<──────────────────────────────────────────────── Login Page β”‚ +β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ 4. Redirect: kaizen://auth/callback β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β–² + β”‚ β”‚ + β”‚ 2. POST /token + PKCE Verifier β”‚ 3. Authenticates + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` +1. **SSO Triggers:** The user clicks a branded button like **"Anmelden mit Microsoft"** or **"Anmelden mit Google"**. +2. **Secure Custom Tab:** The app opens an Android Custom Tab (not a vulnerable WebView) pointing to the Authentik flow. +3. **Corporate Handshake:** Authentik handles the redirect to Microsoft/Google, processes conditional access policies, checks MFA, and redirects back to the app via a custom scheme (`kaizen://auth/callback?code=XYZ`). +4. **Token Exchange:** The app exchanges the authorization code alongside the PKCE cryptographic verifier for security tokens. + +--- + +### Flow C: Native Hardware-Backed Passkeys (FIDO2) +*Best for: High-end device users seeking instant passwordless convenience.* + +1. **Android Credential Manager:** Kaizen implements Android's native Credential Manager API. +2. **Biometric Handshake:** When signing in, the OS slides up a native fingerprint/face unlock sheet. +3. **Cryptographic Validation:** The user touches the sensor. The secure enclave signs a challenge and Authentik validates it. +4. **No Redirection, No Passwords, Sub-Second Speed.** + +--- + +## ⏳ 3. Token Lifecycle & Persistent Sessions (Silent Refresh) + +Enterprise security mandates that API tokens must expire quickly (typically after 15 minutes). To prevent this security measure from destroying the user experience, Kaizen implements **Silent Background Refreshing**. + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” 1. Send API Call (Expired Token) β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€>β”‚ β”‚ +β”‚ β”‚<───────────────────────────────────────────────────── β”‚ +β”‚ β”‚ 2. Response: HTTP 401 β”‚ β”‚ +β”‚ Kaizen App β”‚ β”‚ Kaizen Backend β”‚ +β”‚ (Android) β”‚ 3. POST /token (Using Refresh Token) β”‚ β”‚ +β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚<────────────────────────────────────────────┐ β”‚ β”‚ β”‚ +β”‚ β”‚ 4. Store New Tokens & Retry Request β”‚ β”‚ β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β–Ό β–Ό β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Authentik β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +1. **Token Storage:** Upon successful login, the **Access Token** and **Refresh Token** are saved in `SecureStore.kt` using `EncryptedSharedPreferences` (AES-256-GCM, hardware-bound to the StrongBox / Secure Enclave). +2. **The 401 Interceptor:** OkHttp's Interceptor mechanism intercepts any outgoing API request. +3. **Auto-Refresh:** If an API call fails with `HTTP 401 Unauthorized`, the interceptor pauses the request pipeline, makes a synchronous background call to Authentik using the langlebige `Refresh Token`, stores the new tokens, and retries the original request. +4. **Invisible to User:** The user's chat stream continues without interruption. They remain logged in for weeks or months (based on the Refresh Token validity configured in Authentik) without ever seeing a login screen. + +--- + +## πŸ‘₯ 4. Hybrid User Management Architecture (Authentication vs. Authorization) + +To deliver a world-class enterprise system with zero-friction, Kaizen decouples **Authentication & Lifecycle** (Identity, managed by Authentik) from **Authorization & Resource Allocation** (Application-specific, managed inside Kaizen). + +``` + 1. Authentication (MFA, Passwords, Passkeys) + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Authentik (IdP) │──────────► Overwrites/Provides Email & SSO status + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό Just-In-Time (JIT) Provisioning + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Kaizen PostgreSQL │◄───────── Creates a local 'user' row on first OIDC callback + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό Administrative Control + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Kaizen Admin UI │──────────► Manages app-specific settings (/admin/users): + β”‚ (/admin/users) β”‚ - Model Whitelisting (users.restrictions) + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - Monthly Spent Budgets (USD) + - Built-in / MCP Tool whitelists +``` + +### Decoupling Rules: +1. **Authentik (Identity & Credential Master):** + * Handles user creation, password policies, invitation links ("Magic Links"), MFA, and device registration (Passkeys). + * Kaizen **never** stores or handles raw passwords when using Authentik OIDC or ROPC. +2. **Kaizen Admin Dashboard (App-Specific Authorization):** + * Uses Kaizen's native `/admin/users` panel to configure PostgreSQL `users.restrictions` (Zod-validated JSONB schema). + * **Model Whitelists:** Restrict a user to cheaper/faster models (e.g., Gemini 2.5 Flash only) or open powerful models (e.g., Gemini 1.5 Pro). + * **Spending Limits (Budgets):** Enforce tight monthly, weekly, or lifetime spending limits (tested dynamically during the `/api/chat` preflight leg). + * **Feature & Tool Whitelisting:** Enforce access control on built-in tools (`fetch_url`, `evaluate_math`, `get_weather`), MCP servers, document analysis (`analyze_document`), and generation engines. +3. **Just-In-Time (JIT) Provisioning:** + * When a user successfully authenticates via Authentik OIDC for the first time, Auth.js on the backend automatically provisions a corresponding SQL record in the `users` table with default values, enabling a flawless onboarding experience. + +--- + +## 🎨 5. Premium Enterprise Login Screen UI/UX + +We transform the login screen into a luxury visual experience inspired by Gemini and Microsoft Copilot. + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β”‚ [Kaizen] β”‚ +β”‚ β”‚ +β”‚ (KaizenOrb) β”‚ +β”‚ *Breaths* β”‚ +β”‚ β”‚ +β”‚ Willkommen zurΓΌck β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ 🟦 Mit Microsoft anmelden β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ ⬜ Mit Google anmelden β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ - oder per E-Mail anmelden - β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ E-Mail β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Passwort β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Anmelden β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Visual Enhancements: +1. **SSO-First Layout:** The primary options are seidenmatte glassmorphic buttons for Microsoft Entra ID and Google Workspace, equipped with high-fidelity haptic clicks on touch. +2. **Responsive KaizenOrb States:** + * **Idle:** Gentle breathing glow in standard Amber/Aurora. + * **Domain Discovery (E-Mail entered):** The light ring around the Orb begins rotating elegantly as it checks the company's SSO endpoint. + * **Redirecting:** A pulsing blue light when the Custom Tab or Passkey dialog is initiated. + * **Success:** A single bright emerald flash paired with a short, precise tactile vibration, followed by a smooth screen transition. +3. **Workspace Switcher:** Once logged in, the sidebar displays a beautiful company logo/initials avatar, allowing users to switch between multiple logged-in accounts (e.g., Personal, Corporate) with standard enterprise isolation. + +--- + +## πŸ› οΈ 6. Implementation Roadmap + +We can implement this architectural blueprint systematically over three clear phases: + +### Phase 1: Backend OIDC / JWKS Setup +* Configure Authentik OAuth2 provider for Kaizen. +* Add JWT parsing & offline validation middleware to Next.js Backend (`kaizen/`). +* Extract user scopes and group mappings (e.g. `groups: ["kaizen-admins"]` -> `role = SUPER_ADMIN`). + +### Phase 2: Android Native ROPC & Silent Refresh +* Refactor `SecureStore.kt` to handle standard JWT payloads (`accessToken`, `refreshToken`, `expiresAt`). +* Implement the OkHttp Interceptor in `KaizenApi` for silent background refreshing. +* Connect the existing glassmorphic email/password text fields directly to Authentik's `/application/o/token/` API endpoint. + +### Phase 3: Premium SSO & Passkeys +* Implement OpenID AppAuth/Custom Tabs for standard SSO redirects. +* Integrate Android Credential Manager API for native hardware-backed FIDO2 Passkeys. +* Refactor `LoginScreen.kt` to show the beautiful SSO-First layout with animated `KaizenOrb` states. diff --git a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt index 95d8d38..e7e3032 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt @@ -239,7 +239,9 @@ fun ChatScreen( } } - fun startLiveCall() { + val liveAvailable = models.any { it.id.startsWith("vertex:") && it.id.contains("live") } + + fun startLiveCallInner() { val cfg = session.config ?: return liveCallActive = true @@ -253,21 +255,104 @@ fun ChatScreen( kotlinx.coroutines.delay(50) attempts++ } - liveService?.startCall( + + val svc = liveService ?: return@launch + + // Create conversation if needed (same pattern as text chat) + val cid = conversationId ?: run { + val newId = KaizenApi.createConversation(cfg.baseUrl, cfg.token) + if (newId != null) { + conversationId = newId + chat.conversationRepo.insertOptimistic(newId, "Live Call") + } + newId + } + + svc.onTurnEnd = { turn -> + scope.launch { + val convId = conversationId ?: return@launch + + // Add messages to local UI + if (turn.userText.isNotEmpty()) { + val userMsg = Message(nextId++, Role.User, turn.userText, wireId = turn.userMessageId ?: java.util.UUID.randomUUID().toString()) + messages.add(userMsg) + } + if (turn.assistantText.isNotEmpty()) { + val aMsg = Message(nextId++, Role.Assistant, turn.assistantText, wireId = turn.assistantMessageId ?: java.util.UUID.randomUUID().toString()) + messages.add(aMsg) + chatModel = cfg.model + usedTokens = turn.usage.inputTokens + turn.usage.outputTokens + } + + // Cache in Room (server already persisted via WS-Server) + val msgCount = messages.size + val entitiesToCache = mutableListOf() + if (turn.userMessageId != null && turn.userText.isNotEmpty()) { + entitiesToCache.add(dev.kaizen.app.db.MessageEntity( + id = turn.userMessageId, conversationId = convId, + role = "user", content = turn.userText, + attachments = emptyList(), sortOrder = msgCount - 2, + )) + } + if (turn.assistantMessageId != null && turn.assistantText.isNotEmpty()) { + entitiesToCache.add(dev.kaizen.app.db.MessageEntity( + id = turn.assistantMessageId, conversationId = convId, + role = "assistant", content = turn.assistantText, + attachments = emptyList(), sortOrder = msgCount - 1, + )) + } + if (entitiesToCache.isNotEmpty()) { + chat.messageRepo.cacheMessages(convId, entitiesToCache) + } + } + } + + svc.onError = { code, message -> + scope.launch { + val errorText = message ?: code + Toast.makeText(context, errorText, Toast.LENGTH_SHORT).show() + } + } + + svc.startCall( baseUrl = cfg.baseUrl, token = cfg.token, model = cfg.model, voice = ttsVoice, - conversationId = conversationId, + conversationId = cid, ) } } + val liveCallPermission = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> + if (granted) startLiveCallInner() + else Toast.makeText(context, context.getString(R.string.chat_mic_permission), Toast.LENGTH_SHORT).show() + } + + fun startLiveCall() { + if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { + liveCallPermission.launch(Manifest.permission.RECORD_AUDIO) + return + } + startLiveCallInner() + } + fun endLiveCall() { + liveService?.onTurnEnd = null + liveService?.onError = null liveService?.endCall() liveCallActive = false try { context.unbindService(liveServiceConnection) } catch (_: Exception) {} liveService = null + + // Generate title for new conversations + val cfg = session.config ?: return + val cid = conversationId ?: return + scope.launch { + val title = KaizenApi.generateTitle(cfg.baseUrl, cfg.token, cid) + if (title != null) chat.conversationRepo.updateTitle(cid, title) + chat.conversationRepo.refresh(cfg.baseUrl, cfg.token) + } } var showAttachMenu by remember { mutableStateOf(false) } @@ -1274,7 +1359,7 @@ fun ChatScreen( isRecording = isRecording, recordingAmplitude = recordingAmplitude, onMicClick = { toggleRecording() }, - onCallClick = { startLiveCall() }, + onCallClick = if (liveAvailable) {{ startLiveCall() }} else {{ /* PTT TODO */ }}, pendingFiles = pendingFiles, onAddClick = { showAttachMenu = !showAttachMenu }, onRemoveFile = { id -> pendingFiles.removeAll { it.id == id } }, diff --git a/bugs/chat-ui-dark.jpg b/bugs/chat-ui-dark.jpg new file mode 100644 index 0000000..f0bbc9a Binary files /dev/null and b/bugs/chat-ui-dark.jpg differ diff --git a/bugs/eingabebug.jpg b/bugs/eingabebug.jpg new file mode 100644 index 0000000..57ab7f8 Binary files /dev/null and b/bugs/eingabebug.jpg differ diff --git a/bugs/gemini-chat.jpg b/bugs/gemini-chat.jpg new file mode 100644 index 0000000..b370473 Binary files /dev/null and b/bugs/gemini-chat.jpg differ diff --git a/bugs/message-bar-dark.jpg b/bugs/message-bar-dark.jpg new file mode 100644 index 0000000..12790ad Binary files /dev/null and b/bugs/message-bar-dark.jpg differ diff --git a/bugs/message-bar-white.jpg b/bugs/message-bar-white.jpg new file mode 100644 index 0000000..c33ae85 Binary files /dev/null and b/bugs/message-bar-white.jpg differ