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(), ) }