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 a7052bb..784e68e 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt @@ -737,6 +737,7 @@ fun ChatInput( pendingFiles: List = emptyList(), onAddClick: () -> Unit = {}, onRemoveFile: (String) -> Unit = {}, + pillsContent: @Composable (() -> Unit)? = null, modifier: Modifier = Modifier, ) { val cs = MaterialTheme.colorScheme @@ -746,76 +747,112 @@ fun ChatInput( val hasContent = value.isNotBlank() || pendingFiles.any { it.uploaded != null } val canSend = enabled && hasContent && pendingFiles.none { it.uploading } - val iconBg = if (isDark) Color.White.copy(alpha = 0.07f) else Color.Black.copy(alpha = 0.05f) + var showPills by remember { mutableStateOf(false) } GlassSurface( modifier = modifier .fillMaxWidth() .padding(horizontal = 16.dp), - shape = KaizenShapes.pill, - tintAlpha = if (isDark) 0.82f else 0.88f, + shape = KaizenShapes.lg, + tintAlpha = if (isDark) 0.84f else 0.90f, shadowStack = KaizenShadows.level3, - cornerRadius = 32.dp, + cornerRadius = 28.dp, ) { - Column(Modifier.padding(horizontal = 14.dp, vertical = 14.dp)) { + Column(Modifier.padding(vertical = 10.dp)) { if (isRecording) { - // Recording state — live waveform replaces the text field - VoiceWaveform(amplitude = recordingAmplitude) - Spacer(Modifier.height(10.dp)) - Row(verticalAlignment = Alignment.CenterVertically) { - Spacer(Modifier.weight(1f)) - Box( - Modifier.size(42.dp).clip(KaizenShapes.circle) - .background(KaizenSemanticColors.error) - .clickable(onClick = onMicClick), - contentAlignment = Alignment.Center, - ) { - Icon(KaizenIcons.Square, stringResource(R.string.chat_stop), tint = Color.White, modifier = Modifier.size(16.dp)) + Column(Modifier.padding(horizontal = 14.dp)) { + VoiceWaveform(amplitude = recordingAmplitude) + Spacer(Modifier.height(10.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + Spacer(Modifier.weight(1f)) + Box( + Modifier.size(42.dp).clip(KaizenShapes.circle) + .background(KaizenSemanticColors.error) + .clickable(onClick = onMicClick), + contentAlignment = Alignment.Center, + ) { + Icon(KaizenIcons.Square, stringResource(R.string.chat_stop), tint = Color.White, modifier = Modifier.size(16.dp)) + } + Spacer(Modifier.weight(1f)) } - Spacer(Modifier.weight(1f)) } } else { + // Expandable pills row + if (pillsContent != null) { + androidx.compose.animation.AnimatedVisibility( + visible = showPills, + enter = androidx.compose.animation.expandVertically() + androidx.compose.animation.fadeIn(), + exit = androidx.compose.animation.shrinkVertically() + androidx.compose.animation.fadeOut(), + ) { + Column { + pillsContent() + Spacer(Modifier.height(6.dp)) + Box( + Modifier.fillMaxWidth().padding(horizontal = 14.dp).height(0.5.dp) + .background(cs.onSurfaceVariant.copy(alpha = 0.10f)) + ) + Spacer(Modifier.height(8.dp)) + } + } + } + if (pendingFiles.isNotEmpty()) { Row( - Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()).padding(bottom = 10.dp), + Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()).padding(start = 14.dp, end = 14.dp, bottom = 8.dp), horizontalArrangement = Arrangement.spacedBy(6.dp), ) { pendingFiles.forEach { pf -> PendingFileChip(pf, onRemove = { onRemoveFile(pf.id) }) } } } - Box(Modifier.fillMaxWidth().heightIn(min = 48.dp), contentAlignment = Alignment.CenterStart) { - if (value.isEmpty()) Text( - stringResource(R.string.chat_input_placeholder), - color = cs.onSurfaceVariant.copy(alpha = 0.5f), - fontSize = 16.sp, - fontWeight = FontWeight.W300, - ) - BasicTextField( - value = value, onValueChange = onValueChange, - textStyle = TextStyle(color = cs.onBackground, fontSize = 16.sp, lineHeight = 24.sp, fontWeight = FontWeight.W300), - cursorBrush = SolidColor(accent.primary), maxLines = 6, modifier = Modifier.fillMaxWidth(), - ) - } - - Spacer(Modifier.height(10.dp)) - - Row(verticalAlignment = Alignment.CenterVertically) { + // Single-row input: Plus | TextField | Mic | Send/Call + Row( + Modifier.fillMaxWidth().padding(horizontal = 10.dp), + verticalAlignment = Alignment.Bottom, + ) { Box( - Modifier.size(42.dp).clip(KaizenShapes.circle).background(iconBg).clickable(onClick = onAddClick), + Modifier.size(38.dp).clip(KaizenShapes.circle).clickable(onClick = onAddClick), contentAlignment = Alignment.Center, ) { - Icon(KaizenIcons.Plus, stringResource(R.string.chat_attachment), tint = cs.onSurfaceVariant, modifier = Modifier.size(22.dp)) - } - Spacer(Modifier.width(8.dp)) - Box( - Modifier.size(42.dp).clip(KaizenShapes.circle).background(iconBg).clickable(onClick = onMicClick), - contentAlignment = Alignment.Center, - ) { - Icon(KaizenIcons.Mic, stringResource(R.string.chat_attachment), tint = cs.onSurfaceVariant, modifier = Modifier.size(22.dp)) + Icon(KaizenIcons.Plus, stringResource(R.string.chat_attachment), tint = cs.onSurfaceVariant, modifier = Modifier.size(21.dp)) } - Spacer(Modifier.weight(1f)) + Box( + Modifier.weight(1f).heightIn(min = 38.dp).padding(horizontal = 4.dp), + contentAlignment = Alignment.CenterStart, + ) { + if (value.isEmpty()) Text( + stringResource(R.string.chat_input_placeholder), + color = cs.onSurfaceVariant.copy(alpha = 0.45f), + fontSize = 16.sp, + fontWeight = FontWeight.W300, + ) + BasicTextField( + value = value, onValueChange = onValueChange, + textStyle = TextStyle(color = cs.onBackground, fontSize = 16.sp, lineHeight = 24.sp, fontWeight = FontWeight.W300), + cursorBrush = SolidColor(accent.primary), maxLines = 4, modifier = Modifier.fillMaxWidth(), + ) + } + + if (pillsContent != null) { + Box( + Modifier.size(38.dp).clip(KaizenShapes.circle).clickable { showPills = !showPills }, + contentAlignment = Alignment.Center, + ) { + Icon( + KaizenIcons.SlidersHorizontal, null, + tint = if (showPills) accent.primary else cs.onSurfaceVariant.copy(alpha = 0.55f), + modifier = Modifier.size(19.dp), + ) + } + } + + Box( + Modifier.size(38.dp).clip(KaizenShapes.circle).clickable(onClick = onMicClick), + contentAlignment = Alignment.Center, + ) { + Icon(KaizenIcons.Mic, stringResource(R.string.chat_mic_permission), tint = cs.onSurfaceVariant, modifier = Modifier.size(21.dp)) + } if (isStreaming) { StopButton(onClick = onStop) 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 58d2a00..0855a90 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt @@ -863,7 +863,7 @@ fun ChatScreen( .then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier), contentPadding = PaddingValues( top = 100.dp, - bottom = 260.dp, + bottom = 140.dp, start = 16.dp, end = 16.dp ), @@ -1035,6 +1035,18 @@ fun ChatScreen( Spacer(Modifier.weight(1f)) val ctxLen = models.find { it.id == displayModel }?.context_length ?: 1_000_000 TokenBadge(used = usedTokens, contextLength = ctxLen) + Spacer(Modifier.width(8.dp)) + Box( + Modifier.size(36.dp).clip(KaizenShapes.circle) + .clickable { haptics.tick(); newChat() }, + contentAlignment = Alignment.Center, + ) { + Icon( + KaizenIcons.PenLine, null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(17.dp), + ) + } } } @@ -1089,22 +1101,7 @@ fun ChatScreen( ) .then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier) ) { - Spacer(Modifier.height(if (imeVisible) 6.dp else 24.dp)) - ModePillsRow( - activeMode = activeMode, - onToggle = { mode -> - activeMode = if (activeMode == mode) null else mode - haptics.tick() - }, - reasoningPreset = reasoningPreset, - onReasoningChange = { reasoningPreset = it; haptics.tick() }, - samplingPrefs = samplingPrefs, - onSamplingChange = { samplingPrefs = it }, - currentModelId = session.config?.model ?: "", - webSearchProvider = webSearchProvider, - onWebSearchProviderChange = { webSearchProvider = it; haptics.tick() }, - ) - Spacer(Modifier.height(if (imeVisible) 6.dp else 10.dp)) + Spacer(Modifier.height(if (imeVisible) 4.dp else 12.dp)) ChatInput( value = input, onValueChange = { input = it }, @@ -1118,6 +1115,22 @@ fun ChatScreen( pendingFiles = pendingFiles, onAddClick = { showAttachMenu = !showAttachMenu }, onRemoveFile = { id -> pendingFiles.removeAll { it.id == id } }, + pillsContent = { + ModePillsRow( + activeMode = activeMode, + onToggle = { mode -> + activeMode = if (activeMode == mode) null else mode + haptics.tick() + }, + reasoningPreset = reasoningPreset, + onReasoningChange = { reasoningPreset = it; haptics.tick() }, + samplingPrefs = samplingPrefs, + onSamplingChange = { samplingPrefs = it }, + currentModelId = session.config?.model ?: "", + webSearchProvider = webSearchProvider, + onWebSearchProviderChange = { webSearchProvider = it; haptics.tick() }, + ) + }, ) Spacer(Modifier.height(if (imeVisible) 4.dp else 16.dp)) }