wire search mode, single-select pills, remove Standard pill

Mode pills are now single-select (tap to activate, tap again to
deactivate) matching the web frontend behavior. Removed the
"Standard" pill — it's the implicit default when no mode is active.

Search mode sends webSearch: true in the ChatRequest body, which
triggers the backend's agentive web search (openrouter:web_search
or Google Search grounding). Sources and query are already parsed
by StreamConsumer and rendered via SourcesBlock/SourceChip.

Active pills now highlighted with accent color + accent border
instead of generic surface alpha, making the active state
visually obvious.
This commit is contained in:
Bruno Deanoz 2026-06-22 00:10:14 +02:00
parent a16dfffd5c
commit 30d3f2807d
3 changed files with 19 additions and 14 deletions

View file

@ -142,14 +142,10 @@ fun EmptyHero(userName: String, onPick: (String) -> Unit, modifier: Modifier = M
}
@Composable
fun ModePillsRow(selected: Set<Int>, onToggle: (Int) -> Unit, modifier: Modifier = Modifier) {
fun ModePillsRow(activeMode: Int?, onToggle: (Int) -> Unit, modifier: Modifier = Modifier) {
val cs = MaterialTheme.colorScheme
val isDark = isSystemInDarkTheme()
val glassBorder = remember(isDark) {
if (isDark) Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.14f), Color.White.copy(alpha = 0.04f)))
else Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.50f), Color.Black.copy(alpha = 0.06f)))
}
val accent = LocalKaizenAccent.current
Row(
modifier
@ -158,25 +154,30 @@ fun ModePillsRow(selected: Set<Int>, onToggle: (Int) -> Unit, modifier: Modifier
.padding(horizontal = 14.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
chatModes.forEach { mode ->
chatModes.filter { it.labelRes != R.string.mode_standard }.forEach { mode ->
val label = stringResource(mode.labelRes)
val active = mode.labelRes in selected
val active = mode.labelRes == activeMode
val backgroundFill = if (active) {
if (isDark) cs.surface.copy(alpha = 0.8f) else cs.surface.copy(alpha = 0.88f)
accent.primary.copy(alpha = if (isDark) 0.18f else 0.14f)
} else {
if (isDark) cs.surface.copy(alpha = 0.35f) else cs.surface.copy(alpha = 0.55f)
}
val borderColor = if (active) {
accent.primary.copy(alpha = 0.4f)
} else {
if (isDark) Color.White.copy(alpha = 0.08f) else Color.Black.copy(alpha = 0.06f)
}
Row(
Modifier
.clip(KaizenShapes.pill)
.background(backgroundFill)
.border(1.2.dp, glassBorder, KaizenShapes.pill)
.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) cs.onBackground else cs.onSurfaceVariant, modifier = Modifier.size(16.dp))
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)
}

View file

@ -97,7 +97,7 @@ fun ChatScreen(
var input by remember { mutableStateOf("") }
var isStreaming by remember { mutableStateOf(false) }
var nextId by remember { mutableStateOf(0L) }
var selectedModes by remember { mutableStateOf(setOf(R.string.mode_standard)) }
var activeMode by remember { mutableStateOf<Int?>(null) }
// Navigation State
var currentScreen by remember { mutableStateOf(AppScreen.Chat) }
@ -339,6 +339,7 @@ fun ChatScreen(
KaizenApi.chat(
cfg.baseUrl, cfg.token, cfg.model, history, cfg.speed,
attachments = uploadedAtts.takeIf { it.isNotEmpty() },
webSearch = activeMode == R.string.mode_search,
).collect { state ->
val idx = messages.indexOfLast { it.id == assistantId }
if (idx < 0) return@collect
@ -621,9 +622,9 @@ fun ChatScreen(
) {
Spacer(Modifier.height(24.dp))
ModePillsRow(
selected = selectedModes,
activeMode = activeMode,
onToggle = { mode ->
selectedModes = if (mode in selectedModes) selectedModes - mode else selectedModes + mode
activeMode = if (activeMode == mode) null else mode
haptics.tick()
},
)

View file

@ -35,6 +35,7 @@ import java.util.concurrent.TimeUnit
val reasoningEffort: String? = null,
val preferThroughput: Boolean? = null,
val attachments: List<Attachment>? = null,
val webSearch: Boolean? = null,
)
/** One entry from GET /api/v1/models — the picker's row. `provider == "vertex"` drives the hoster badge. */
@ -440,6 +441,7 @@ object KaizenApi {
messages: List<WireMessage>,
speed: ChatSpeed = ChatSpeed.Normal,
attachments: List<Attachment>? = null,
webSearch: Boolean = false,
): Flow<StreamState> = flow {
val payload = json.encodeToString(
ChatRequest(
@ -448,6 +450,7 @@ object KaizenApi {
reasoningEffort = speed.reasoningEffort,
preferThroughput = if (speed.preferThroughput) true else null,
attachments = attachments?.takeIf { it.isNotEmpty() },
webSearch = if (webSearch) true else null,
),
).toRequestBody(jsonMedia)
val req = Request.Builder()