From 497f0a82f756faa5dd252c0c052839a68f315acc Mon Sep 17 00:00:00 2001 From: Bruno Deanoz Date: Tue, 23 Jun 2026 01:07:57 +0200 Subject: [PATCH] feat: redesign mode pills and dropdowns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../dev/kaizen/app/chat/ChatComponents.kt | 208 ++++++++++++------ 1 file changed, 137 insertions(+), 71 deletions(-) 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 1372558..dfa2258 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt @@ -159,82 +159,83 @@ fun ModePillsRow( var showReasoningMenu by remember { mutableStateOf(false) } var showSamplingPopup by remember { mutableStateOf(false) } - val inactiveBg = if (isDark) cs.surface.copy(alpha = 0.35f) else cs.surface.copy(alpha = 0.55f) - val inactiveBorder = if (isDark) Color.White.copy(alpha = 0.08f) else Color.Black.copy(alpha = 0.06f) - Row( modifier .fillMaxWidth() .horizontalScroll(rememberScrollState()) .padding(horizontal = 14.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), ) { - // Reasoning pill (dropdown) + // Reasoning pill Box { - Row( - Modifier - .clip(KaizenShapes.pill) - .background(inactiveBg) - .border(1.2.dp, inactiveBorder, KaizenShapes.pill) - .clickable { showReasoningMenu = true } - .padding(horizontal = 13.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, + val isDefault = reasoningPreset == ReasoningPreset.Standard + ModePill( + icon = KaizenIcons.Brain, + label = stringResource(reasoningPreset.label), + active = !isDefault, + showChevron = true, + onClick = { showReasoningMenu = true }, + ) + DropdownMenu( + expanded = showReasoningMenu, + onDismissRequest = { showReasoningMenu = false }, + containerColor = if (isDark) Color(0xFF1A2030) else Color(0xFFF8F9FB), + shadowElevation = 16.dp, + shape = KaizenShapes.md, ) { - Icon(KaizenIcons.Brain, null, tint = cs.onSurfaceVariant, modifier = Modifier.size(16.dp)) - Spacer(Modifier.width(6.dp)) - Text(stringResource(reasoningPreset.label), color = cs.onSurfaceVariant, fontSize = 13.sp, fontWeight = FontWeight.Medium) - Spacer(Modifier.width(4.dp)) - Icon(KaizenIcons.ChevronDown, null, tint = cs.onSurfaceVariant.copy(alpha = 0.5f), modifier = Modifier.size(14.dp)) - } - DropdownMenu(expanded = showReasoningMenu, onDismissRequest = { showReasoningMenu = false }) { ReasoningPreset.entries.forEach { preset -> - DropdownMenuItem( - text = { - Text( - stringResource(preset.label), - fontSize = 14.sp, - color = if (preset == reasoningPreset) accent.primary else cs.onBackground, - fontWeight = if (preset == reasoningPreset) FontWeight.SemiBold else FontWeight.Normal, - ) - }, - onClick = { onReasoningChange(preset); showReasoningMenu = false }, - ) + Row( + Modifier + .fillMaxWidth() + .clickable { onReasoningChange(preset); showReasoningMenu = false } + .padding(horizontal = 16.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + stringResource(preset.label), + fontSize = 13.5.sp, + color = if (preset == reasoningPreset) accent.primary else cs.onBackground.copy(alpha = 0.85f), + fontWeight = if (preset == reasoningPreset) FontWeight.SemiBold else FontWeight.Normal, + modifier = Modifier.weight(1f), + ) + if (preset == reasoningPreset) { + Icon(KaizenIcons.Check, null, tint = accent.primary, modifier = Modifier.size(15.dp)) + } + } } } } - // Sampling pill (popover) + // Sampling pill Box { val anyEnabled = samplingPrefs.temperature.enabled || samplingPrefs.topP.enabled || samplingPrefs.topK.enabled - Row( - Modifier - .clip(KaizenShapes.pill) - .background(if (anyEnabled) accent.primary.copy(alpha = if (isDark) 0.18f else 0.14f) else inactiveBg) - .border(1.2.dp, if (anyEnabled) accent.primary.copy(alpha = 0.4f) else inactiveBorder, KaizenShapes.pill) - .clickable { showSamplingPopup = !showSamplingPopup } - .padding(horizontal = 13.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, + ModePill( + icon = KaizenIcons.SlidersHorizontal, + label = stringResource(R.string.mode_sampling), + active = anyEnabled, + showChevron = true, + onClick = { showSamplingPopup = !showSamplingPopup }, + ) + DropdownMenu( + expanded = showSamplingPopup, + onDismissRequest = { showSamplingPopup = false }, + containerColor = if (isDark) Color(0xFF1A2030) else Color(0xFFF8F9FB), + shadowElevation = 16.dp, + shape = KaizenShapes.md, ) { - Icon(KaizenIcons.SlidersHorizontal, null, tint = if (anyEnabled) accent.primary else cs.onSurfaceVariant, modifier = Modifier.size(16.dp)) - Spacer(Modifier.width(6.dp)) - Text(stringResource(R.string.mode_sampling), color = if (anyEnabled) cs.onBackground else cs.onSurfaceVariant, fontSize = 13.sp, fontWeight = FontWeight.Medium) - Spacer(Modifier.width(4.dp)) - Icon(KaizenIcons.ChevronDown, null, tint = cs.onSurfaceVariant.copy(alpha = 0.5f), modifier = Modifier.size(14.dp)) - } - DropdownMenu(expanded = showSamplingPopup, onDismissRequest = { showSamplingPopup = false }) { - Column(Modifier.padding(horizontal = 16.dp, vertical = 8.dp).width(240.dp)) { + Column(Modifier.padding(horizontal = 16.dp, vertical = 12.dp).width(260.dp)) { SamplingRow( label = stringResource(R.string.sampling_temperature), param = samplingPrefs.temperature, min = 0f, max = 2f, steps = 200, onUpdate = { onSamplingChange(samplingPrefs.copy(temperature = it)) }, ) - Spacer(Modifier.height(12.dp)) + Spacer(Modifier.height(14.dp)) SamplingRow( label = stringResource(R.string.sampling_top_p), param = samplingPrefs.topP, min = 0f, max = 1f, steps = 100, onUpdate = { onSamplingChange(samplingPrefs.copy(topP = it)) }, ) - Spacer(Modifier.height(12.dp)) + Spacer(Modifier.height(14.dp)) SamplingRow( label = stringResource(R.string.sampling_top_k), param = samplingPrefs.topK, min = 1f, max = 100f, steps = 99, @@ -246,24 +247,62 @@ fun ModePillsRow( // Mode pills (Bild, Suche, Video, Audio) chatModes.filter { it.labelRes != R.string.mode_standard && it.labelRes != R.string.mode_sampling }.forEach { mode -> - val label = stringResource(mode.labelRes) val active = mode.labelRes == activeMode - val backgroundFill = if (active) accent.primary.copy(alpha = if (isDark) 0.18f else 0.14f) else inactiveBg - val borderColor = if (active) accent.primary.copy(alpha = 0.4f) else inactiveBorder + ModePill( + icon = mode.icon, + label = stringResource(mode.labelRes), + active = active, + onClick = { onToggle(mode.labelRes) }, + ) + } + } +} - Row( - Modifier - .clip(KaizenShapes.pill) - .background(backgroundFill) - .border(1.2.dp, borderColor, KaizenShapes.pill) - .clickable { onToggle(mode.labelRes) } - .padding(horizontal = 13.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon(mode.icon, null, tint = if (active) accent.primary else cs.onSurfaceVariant, modifier = Modifier.size(16.dp)) - Spacer(Modifier.width(6.dp)) - Text(label, color = if (active) cs.onBackground else cs.onSurfaceVariant, fontSize = 13.sp, fontWeight = if (active) FontWeight.SemiBold else FontWeight.Medium) - } +@Composable +private fun ModePill( + icon: ImageVector, + label: String, + active: Boolean, + showChevron: Boolean = false, + onClick: () -> Unit, +) { + val cs = MaterialTheme.colorScheme + val isDark = isSystemInDarkTheme() + val accent = LocalKaizenAccent.current + + val bg = when { + active -> accent.primary.copy(alpha = if (isDark) 0.15f else 0.10f) + else -> if (isDark) Color.White.copy(alpha = 0.06f) else Color.Black.copy(alpha = 0.04f) + } + val borderBrush = if (active) { + Brush.verticalGradient(listOf( + accent.primary.copy(alpha = if (isDark) 0.30f else 0.22f), + accent.primary.copy(alpha = if (isDark) 0.08f else 0.06f), + )) + } else { + Brush.verticalGradient(listOf( + if (isDark) Color.White.copy(alpha = 0.08f) else Color.White.copy(alpha = 0.40f), + if (isDark) Color.White.copy(alpha = 0.02f) else Color.Black.copy(alpha = 0.03f), + )) + } + val iconTint = if (active) accent.primary else cs.onSurfaceVariant.copy(alpha = 0.60f) + val textColor = if (active) cs.onBackground else cs.onSurfaceVariant.copy(alpha = 0.70f) + + Row( + Modifier + .clip(KaizenShapes.pill) + .background(bg) + .border(0.8.dp, borderBrush, KaizenShapes.pill) + .clickable(onClick = onClick) + .padding(horizontal = 12.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(icon, null, tint = iconTint, modifier = Modifier.size(15.dp)) + Spacer(Modifier.width(5.dp)) + Text(label, color = textColor, fontSize = 12.5.sp, fontWeight = if (active) FontWeight.SemiBold else FontWeight.Medium) + if (showChevron) { + Spacer(Modifier.width(3.dp)) + Icon(KaizenIcons.ChevronDown, null, tint = cs.onSurfaceVariant.copy(alpha = 0.35f), modifier = Modifier.size(13.dp)) } } } @@ -271,21 +310,48 @@ fun ModePillsRow( @Composable private fun SamplingRow(label: String, param: SamplingParam, min: Float, max: Float, steps: Int, onUpdate: (SamplingParam) -> Unit) { val cs = MaterialTheme.colorScheme + val isDark = isSystemInDarkTheme() val accent = LocalKaizenAccent.current + Row(verticalAlignment = Alignment.CenterVertically) { - Checkbox(checked = param.enabled, onCheckedChange = { onUpdate(param.copy(enabled = it)) }, modifier = Modifier.size(20.dp)) - Spacer(Modifier.width(8.dp)) - Text(label, color = cs.onBackground, fontSize = 13.sp, modifier = Modifier.weight(1f)) + Box( + Modifier + .size(20.dp) + .clip(KaizenShapes.xs) + .border( + 1.dp, + if (param.enabled) accent.primary else cs.onSurfaceVariant.copy(alpha = 0.25f), + KaizenShapes.xs, + ) + .then(if (param.enabled) Modifier.background(accent.primary.copy(alpha = 0.15f)) else Modifier) + .clickable { onUpdate(param.copy(enabled = !param.enabled)) }, + contentAlignment = Alignment.Center, + ) { + if (param.enabled) { + Icon(KaizenIcons.Check, null, tint = accent.primary, modifier = Modifier.size(13.dp)) + } + } + Spacer(Modifier.width(10.dp)) + Text(label, color = cs.onBackground.copy(alpha = if (param.enabled) 0.90f else 0.50f), fontSize = 13.sp, modifier = Modifier.weight(1f)) Text( if (max > 2f) param.value.toInt().toString() else "%.2f".format(param.value), - color = cs.onSurfaceVariant, fontSize = 12.sp, + color = if (param.enabled) accent.primary else cs.onSurfaceVariant.copy(alpha = 0.40f), + fontSize = 12.sp, fontWeight = FontWeight.Medium, ) } + Spacer(Modifier.height(4.dp)) Slider( value = param.value, onValueChange = { onUpdate(param.copy(value = it)) }, valueRange = min..max, steps = steps, enabled = param.enabled, - colors = SliderDefaults.colors(thumbColor = accent.primary, activeTrackColor = accent.primary), + colors = SliderDefaults.colors( + thumbColor = accent.primary, + activeTrackColor = accent.primary, + inactiveTrackColor = if (isDark) Color.White.copy(alpha = 0.08f) else Color.Black.copy(alpha = 0.06f), + disabledThumbColor = cs.onSurfaceVariant.copy(alpha = 0.20f), + disabledActiveTrackColor = cs.onSurfaceVariant.copy(alpha = 0.15f), + disabledInactiveTrackColor = if (isDark) Color.White.copy(alpha = 0.05f) else Color.Black.copy(alpha = 0.04f), + ), modifier = Modifier.fillMaxWidth(), ) }