feat: redesign mode pills and dropdowns
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.
This commit is contained in:
parent
fcac983201
commit
497f0a82f7
1 changed files with 137 additions and 71 deletions
|
|
@ -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 {
|
||||
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,
|
||||
) {
|
||||
ReasoningPreset.entries.forEach { preset ->
|
||||
Row(
|
||||
Modifier
|
||||
.clip(KaizenShapes.pill)
|
||||
.background(inactiveBg)
|
||||
.border(1.2.dp, inactiveBorder, KaizenShapes.pill)
|
||||
.clickable { showReasoningMenu = true }
|
||||
.padding(horizontal = 13.dp, vertical = 8.dp),
|
||||
.fillMaxWidth()
|
||||
.clickable { onReasoningChange(preset); showReasoningMenu = false }
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
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,
|
||||
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),
|
||||
)
|
||||
},
|
||||
onClick = { onReasoningChange(preset); showReasoningMenu = false },
|
||||
)
|
||||
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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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(backgroundFill)
|
||||
.border(1.2.dp, borderColor, KaizenShapes.pill)
|
||||
.clickable { onToggle(mode.labelRes) }
|
||||
.padding(horizontal = 13.dp, vertical = 8.dp),
|
||||
.background(bg)
|
||||
.border(0.8.dp, borderBrush, KaizenShapes.pill)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 12.dp, vertical = 7.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)
|
||||
}
|
||||
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(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue