diff --git a/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt b/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt index 75ee18c..db39394 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt @@ -52,6 +52,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale +import androidx.compose.ui.draw.shadow import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color @@ -59,6 +60,7 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -177,11 +179,12 @@ private fun SuggestionPills(onPick: (String) -> Unit) { val cs = MaterialTheme.colorScheme val isDark = isSystemInDarkTheme() - val glassBg = if (isDark) cs.surface.copy(alpha = 0.40f) else cs.surface.copy(alpha = 0.85f) + // Highly responsive 3D floating glass capsules (iOS 26 / Apple Liquid Glass style) + val glassBg = if (isDark) cs.surface.copy(alpha = 0.45f) else cs.surface.copy(alpha = 0.88f) val glassBorder = if (isDark) { - Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.10f), Color.White.copy(alpha = 0.02f))) + Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.16f), Color.White.copy(alpha = 0.02f))) } else { - Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.50f), Color.Black.copy(alpha = 0.04f))) + Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.65f), Color.Black.copy(alpha = 0.05f))) } FlowRow( @@ -192,9 +195,10 @@ private fun SuggestionPills(onPick: (String) -> Unit) { suggestions.forEach { suggestion -> Row( Modifier + .shadow(elevation = 3.dp, shape = CircleShape, clip = false) // Ambient 3D shadow for floating look .clip(CircleShape) .background(glassBg) - .border(1.dp, glassBorder, CircleShape) + .border(1.2.dp, glassBorder, CircleShape) .clickable { onPick(suggestion.prompt) } .padding(horizontal = 16.dp, vertical = 11.dp), verticalAlignment = Alignment.CenterVertically, @@ -248,6 +252,7 @@ fun ModePillsRow(selected: String, onSelect: (String) -> Unit, modifier: Modifie Row( Modifier + .shadow(elevation = 2.dp, shape = CircleShape, clip = false) // Floating tabs .clip(CircleShape) .background(backgroundFill) .border(1.2.dp, borderBrush, CircleShape) @@ -303,6 +308,7 @@ fun MessageRow(message: Message) { Box( Modifier .widthIn(max = 300.dp) + .shadow(elevation = 3.dp, shape = shape, clip = false) // Raised amber glass bubble! .clip(shape) .background(bgBrush) .border(1.2.dp, borderBrush, shape) @@ -404,6 +410,7 @@ fun ChatInput( modifier .fillMaxWidth() .padding(horizontal = 12.dp) + .shadow(elevation = 8.dp, shape = RoundedCornerShape(28.dp), clip = false) // Luxurious floating ambient shadow! .clip(RoundedCornerShape(28.dp)) .background(glassBg) .border(1.2.dp, glassBorder, RoundedCornerShape(28.dp)) 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 96d8c76..f318502 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt @@ -1,5 +1,8 @@ package dev.kaizen.app.chat +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -9,10 +12,23 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Menu +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -23,6 +39,10 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.dp import dev.kaizen.app.haptics.rememberHaptics @@ -44,6 +64,9 @@ fun ChatScreen(userName: String = "Bruno") { val configuration = LocalConfiguration.current val isTablet = configuration.screenWidthDp > 600 + // Native sidebar drawer state + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + fun send(text: String) { val trimmed = text.trim() if (trimmed.isEmpty() || isStreaming) return @@ -82,53 +105,104 @@ fun ChatScreen(userName: String = "Bruno") { if (messages.isNotEmpty()) listState.scrollToItem(messages.lastIndex) } - MeshBackground { - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - // 1. Scrollable Chat Content - // Stretches to the full screen, allowing bubbles to scroll cleanly behind the floating bottom dock. - // Centered layout with responsive width capping on tablets. - if (messages.isEmpty()) { - EmptyHero( - userName = userName, - onPick = { haptics.tick(); send(it) }, - modifier = Modifier - .fillMaxSize() - .then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier) - ) - } else { - LazyColumn( - state = listState, - modifier = Modifier - .fillMaxSize() - .then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier), - contentPadding = PaddingValues( - top = 16.dp, // Clean, native layout allows messages to scroll all the way to the top status bar! - bottom = 152.dp, // Leaves luxurious room for the floating ModePills + ChatInput + bottom margins - start = 16.dp, - end = 16.dp - ), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - items(messages, key = { it.id }) { message -> MessageRow(message) } + // ModalNavigationDrawer provides a native drawer that slides from left-to-right + ModalNavigationDrawer( + drawerState = drawerState, + drawerContent = { + KaizenSidebar( + userName = userName, + onClose = { scope.launch { drawerState.close() } }, + onNewChat = { + haptics.tick() + messages.clear() + scope.launch { drawerState.close() } + } + ) + }, + gesturesEnabled = true // Allows swiping from the screen's left edge to open the drawer + ) { + MeshBackground { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + // 1. Scrollable Chat Content + // Stretches to the full screen, allowing bubbles to scroll cleanly behind the floating bottom dock. + // Centered layout with responsive width capping on tablets. + if (messages.isEmpty()) { + EmptyHero( + userName = userName, + onPick = { haptics.tick(); send(it) }, + modifier = Modifier + .fillMaxSize() + .then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier) + ) + } else { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier), + contentPadding = PaddingValues( + top = 76.dp, // Leaves breathing room for the floating top menu button + bottom = 152.dp, // Leaves luxurious room for the floating ModePills + ChatInput + bottom margins + start = 16.dp, + end = 16.dp + ), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + items(messages, key = { it.id }) { message -> MessageRow(message) } + } } - } - // 2. Floating Bottom Control Dock (responsive centering and clear bottom margins) - Column( - modifier = Modifier - .align(Alignment.BottomCenter) - .navigationBarsPadding() - .then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier) - ) { - ModePillsRow(selected = selectedMode, onSelect = { selectedMode = it; haptics.tick() }) - Spacer(Modifier.height(10.dp)) - ChatInput( - value = input, - onValueChange = { input = it }, - onSend = { send(input) }, - enabled = !isStreaming, - ) - Spacer(Modifier.height(16.dp)) // Increased from 12.dp to 16.dp to completely clear keyboard edges on all screens! + // 2. Floating Top Menu Button (Apple Liquid Glass - completely independent of a header bar) + val isDark = remember { isStreaming || messages.isNotEmpty() } // slight adaptive context tint + val menuGlassBg = if (isDark.not() && isSystemInDarkTheme().not()) { + Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.85f), Color(0xFFF1F5F9).copy(alpha = 0.75f))) + } else { + Brush.verticalGradient(listOf(Color(0xFF1E293B).copy(alpha = 0.65f), Color(0xFF0F172A).copy(alpha = 0.75f))) + } + val menuGlassBorder = if (isDark.not() && isSystemInDarkTheme().not()) { + Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.55f), Color.Black.copy(alpha = 0.05f))) + } else { + Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.16f), Color.White.copy(alpha = 0.03f))) + } + + Box( + modifier = Modifier + .align(Alignment.TopStart) + .statusBarsPadding() + .padding(start = 16.dp, top = 10.dp) + .shadow(elevation = 6.dp, shape = CircleShape, clip = false) // Floats above backgrounds + .clip(CircleShape) + .background(menuGlassBg) + .border(1.2.dp, menuGlassBorder, CircleShape) + .clickable { haptics.tick(); scope.launch { drawerState.open() } } + .size(44.dp), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Rounded.Menu, + contentDescription = "Menü öffnen", + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.size(20.dp) + ) + } + + // 3. Floating Bottom Control Dock (responsive centering and clear bottom margins) + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .navigationBarsPadding() + .then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier) + ) { + ModePillsRow(selected = selectedMode, onSelect = { selectedMode = it; haptics.tick() }) + Spacer(Modifier.height(10.dp)) + ChatInput( + value = input, + onValueChange = { input = it }, + onSend = { send(input) }, + enabled = !isStreaming, + ) + Spacer(Modifier.height(16.dp)) // Completely clears keyboard edges on all screens + } } } } diff --git a/app/src/main/java/dev/kaizen/app/chat/Sidebar.kt b/app/src/main/java/dev/kaizen/app/chat/Sidebar.kt new file mode 100644 index 0000000..2288b5a --- /dev/null +++ b/app/src/main/java/dev/kaizen/app/chat/Sidebar.kt @@ -0,0 +1,340 @@ +package dev.kaizen.app.chat + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Edit +import androidx.compose.material.icons.rounded.Lock +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material.icons.rounded.Settings +import androidx.compose.material.icons.rounded.Star +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import dev.kaizen.app.ui.theme.Amber + +// Mock sidebar history item representing a past conversation +data class ConvoHistoryItem( + val id: String, + val title: String, + val dateGroup: String, + val locked: Boolean = false, + val pinned: Boolean = false +) + +// Authentically mirrored mock history from sidebar.png +val mockConvoHistory = listOf( + ConvoHistoryItem("1", "Planetenübersicht P...", "16. JUNI 2026"), + ConvoHistoryItem("2", "Orbitaltheorie in der ...", "16. JUNI 2026", locked = true), + ConvoHistoryItem("3", "Wetter in Magdeburg", "16. JUNI 2026"), + ConvoHistoryItem("4", "Aktuelle Uhrzeit in To...", "16. JUNI 2026"), + + ConvoHistoryItem("5", "📷 Aussehen und Styl...", "15. JUNI 2026"), + ConvoHistoryItem("6", "👤 Objektive Bewertu...", "15. JUNI 2026"), + + ConvoHistoryItem("7", "Beziehungen und ihre...", "14. JUNI 2026"), + + ConvoHistoryItem("8", "Online-Test erfolgreic...", "12. JUNI 2026"), + + ConvoHistoryItem("9", "Hallo Gemini", "11. JUNI 2026"), + + ConvoHistoryItem("10", "Gesperrter Chat", "10. JUNI 2026", locked = true) +) + +/** + * Floating glassmorphic sidebar drawer content. + * + * Styled as a suspended glass panel with a thick "milky glass" background, + * dual-border specular light reflections, deep ambient shadows, and authentic Kaizen layouts. + */ +@Composable +fun KaizenSidebar( + userName: String, + onClose: () -> Unit, + onNewChat: () -> Unit, + modifier: Modifier = Modifier +) { + val cs = MaterialTheme.colorScheme + val isDark = isSystemInDarkTheme() + + // 1. Thick, premium milky-glass background gradient + val glassBg = remember(isDark) { + if (isDark) { + Brush.verticalGradient( + listOf( + Color(0xCC141B27), // Deep Obsidian-blue with 80% opacity + Color(0xE60A0E16) // Even darker at the bottom (90% opacity) + ) + ) + } else { + Brush.verticalGradient( + listOf( + Color.White.copy(alpha = 0.94f), // Creamy white with 94% opacity + Color(0xFFF1F5F9).copy(alpha = 0.85f) // Slightly slate tint at bottom + ) + ) + } + } + + // 2. Specular border light reflections (crisp glass edge highlight) + val glassBorder = remember(isDark) { + if (isDark) { + Brush.verticalGradient( + listOf( + Color.White.copy(alpha = 0.16f), // Bright highlight on top-left + Color.White.copy(alpha = 0.02f) // Faint dark shade on bottom + ) + ) + } else { + Brush.verticalGradient( + listOf( + Color.White.copy(alpha = 0.65f), + Color.Black.copy(alpha = 0.05f) + ) + ) + } + } + + // Floating design: 12dp margins all around so it physically hovers above the background! + Box( + modifier = modifier + .fillMaxHeight() + .width(300.dp) + .padding(vertical = 12.dp, start = 12.dp, end = 4.dp) + .shadow(elevation = 16.dp, shape = RoundedCornerShape(24.dp), clip = false) + .clip(RoundedCornerShape(24.dp)) + .background(glassBg) + .border(1.2.dp, glassBorder, RoundedCornerShape(24.dp)) + .statusBarsPadding() + .padding(16.dp) + ) { + Column(Modifier.fillMaxSize()) { + + // --- HEADER Sektion: Brand title and New Chat button --- + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "kaizen", + color = cs.onBackground, + fontSize = 20.sp, + fontWeight = FontWeight.Bold + ) + + // Translucent "New Chat" pencil button + Box( + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + .background(if (isDark) Color(0x1AFFFFFF) else Color(0x0A000000)) + .clickable { onNewChat() }, + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Rounded.Edit, + contentDescription = "Neuer Chat", + tint = cs.onBackground, + modifier = Modifier.size(18.dp) + ) + } + } + + Spacer(Modifier.height(16.dp)) + + // --- SEARCH Sektion: Glassmorphic search capsule --- + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(if (isDark) Color(0x12FFFFFF) else Color(0x06000000)) + .border( + 1.dp, + if (isDark) Color.White.copy(alpha = 0.06f) else Color.Black.copy(alpha = 0.04f), + RoundedCornerShape(12.dp) + ) + .padding(horizontal = 12.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Rounded.Search, + contentDescription = null, + tint = cs.onSurfaceVariant.copy(alpha = 0.6f), + modifier = Modifier.size(18.dp) + ) + Spacer(Modifier.width(8.dp)) + Text( + text = "Suchen...", + color = cs.onSurfaceVariant.copy(alpha = 0.6f), + fontSize = 14.sp + ) + } + + Spacer(Modifier.height(24.dp)) + + // --- BODY Sektion: Scrollable Chats list grouped by Date --- + Box(Modifier.weight(1f).fillMaxWidth()) { + val groupedHistory = remember { mockConvoHistory.groupBy { it.dateGroup } } + + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + groupedHistory.forEach { (dateGroup, items) -> + // Date header (e.g., 16. JUNI 2026) + item(key = "header_$dateGroup") { + Text( + text = dateGroup, + color = cs.onSurfaceVariant.copy(alpha = 0.45f), + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(start = 12.dp, top = 8.dp, bottom = 4.dp) + ) + } + + // History items + items(items, key = { it.id }) { item -> + HistoryItemRow(item, onClick = { onClose() }) + } + } + } + } + + Spacer(Modifier.height(16.dp)) + + // --- FOOTER Sektion: User Profile Card --- + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .background(if (isDark) Color(0x12FFFFFF) else Color(0x06000000)) + .border( + 1.dp, + if (isDark) Color.White.copy(alpha = 0.08f) else Color.Black.copy(alpha = 0.04f), + RoundedCornerShape(18.dp) + ) + .clickable { } + .padding(horizontal = 12.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // User Avatar bubble: Obsidian "N" bubble + Box( + modifier = Modifier + .size(40.dp) + .clip(CircleShape) + .background(if (isDark) Color(0xFF1E293B) else Color(0xFF0F172A)), + contentAlignment = Alignment.Center + ) { + Text( + text = "N", + color = Color.White, + fontSize = 17.sp, + fontWeight = FontWeight.Bold + ) + } + + Spacer(Modifier.width(10.dp)) + + Column(Modifier.weight(1f)) { + Text( + text = "Admin", + color = cs.onBackground, + fontSize = 15.sp, + fontWeight = FontWeight.SemiBold + ) + } + + Icon( + imageVector = Icons.Rounded.Settings, + contentDescription = "Einstellungen", + tint = cs.onSurfaceVariant.copy(alpha = 0.7f), + modifier = Modifier.size(20.dp) + ) + } + } + } +} + +@Composable +private fun HistoryItemRow(item: ConvoHistoryItem, onClick: () -> Unit) { + val cs = MaterialTheme.colorScheme + val isDark = isSystemInDarkTheme() + + val itemBg = if (isDark) Color(0x0AFFFFFF) else Color(0x05000000) + val itemBorder = if (isDark) Color.White.copy(alpha = 0.04f) else Color.Black.copy(alpha = 0.03f) + + Row( + modifier = Modifier + .fillMaxWidth() + .clip(CircleShape) + .background(itemBg) + .border(1.dp, itemBorder, CircleShape) + .clickable { onClick() } + .padding(horizontal = 16.dp, vertical = 11.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (item.pinned) { + Icon( + imageVector = Icons.Rounded.Star, + contentDescription = null, + tint = Amber, + modifier = Modifier.size(14.dp) + ) + Spacer(Modifier.width(8.dp)) + } + + Text( + text = if (item.locked) "Gesperrter Chat" else item.title, + color = if (item.locked) cs.onSurfaceVariant.copy(alpha = 0.5f) else cs.onBackground, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + fontStyle = if (item.locked) FontStyle.Italic else FontStyle.Normal, + modifier = Modifier.weight(1f) + ) + + if (item.locked) { + Spacer(Modifier.width(8.dp)) + Icon( + imageVector = Icons.Rounded.Lock, + contentDescription = "Gesperrt", + tint = Amber, + modifier = Modifier.size(15.dp) + ) + } + } +}