Compare commits

..

2 commits

Author SHA1 Message Date
Bruno Deanoz
fb6bc981d1 feat: web search provider picker for Vertex models (Google vs Enterprise)
When search mode is active and a vertex: model is selected, a second pill
appears next to "Suche" with a dropdown to choose between Google Search
(default) and Enterprise Web Search grounding. The selection is sent as
webSearchProvider in the chat request body. Matches the web frontend's
OptionPill for webSearchProvider.

Only visible for Vertex models — OpenRouter uses agentive web_search
server-tool which has no provider toggle.
2026-06-23 01:23:03 +02:00
Bruno Deanoz
4150936b18 fix: sampling panel too large + pills barely visible
Sampling panel: width 260→220dp, padding reduced, spacing 14→6dp between
parameters, checkbox 20→16dp, slider height capped at 28dp, removed extra
spacer between label row and slider. Panel is now ~40% shorter.

Pills visibility: inactive background alpha raised (dark 0.06→0.10, light
0.04→0.07), border alpha doubled (dark 0.08→0.14, light was white-on-light
invisible, now uses black 0.10), icon tint 0.60→0.75, text 0.70→0.80.
Active state also strengthened. Pills are now clearly visible in both modes.
2026-06-23 01:17:56 +02:00
5 changed files with 79 additions and 18 deletions

View file

@ -151,6 +151,9 @@ fun ModePillsRow(
onReasoningChange: (ReasoningPreset) -> Unit, onReasoningChange: (ReasoningPreset) -> Unit,
samplingPrefs: SamplingPrefs, samplingPrefs: SamplingPrefs,
onSamplingChange: (SamplingPrefs) -> Unit, onSamplingChange: (SamplingPrefs) -> Unit,
currentModelId: String = "",
webSearchProvider: String = "google",
onWebSearchProviderChange: (String) -> Unit = {},
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val cs = MaterialTheme.colorScheme val cs = MaterialTheme.colorScheme
@ -159,6 +162,8 @@ fun ModePillsRow(
var showReasoningMenu by remember { mutableStateOf(false) } var showReasoningMenu by remember { mutableStateOf(false) }
var showSamplingPopup by remember { mutableStateOf(false) } var showSamplingPopup by remember { mutableStateOf(false) }
var showSearchProviderMenu by remember { mutableStateOf(false) }
val isVertexModel = currentModelId.startsWith("vertex:")
Row( Row(
modifier modifier
@ -224,19 +229,19 @@ fun ModePillsRow(
shadowElevation = 16.dp, shadowElevation = 16.dp,
shape = KaizenShapes.md, shape = KaizenShapes.md,
) { ) {
Column(Modifier.padding(horizontal = 16.dp, vertical = 12.dp).width(260.dp)) { Column(Modifier.padding(horizontal = 14.dp, vertical = 10.dp).width(220.dp)) {
SamplingRow( SamplingRow(
label = stringResource(R.string.sampling_temperature), label = stringResource(R.string.sampling_temperature),
param = samplingPrefs.temperature, min = 0f, max = 2f, steps = 200, param = samplingPrefs.temperature, min = 0f, max = 2f, steps = 200,
onUpdate = { onSamplingChange(samplingPrefs.copy(temperature = it)) }, onUpdate = { onSamplingChange(samplingPrefs.copy(temperature = it)) },
) )
Spacer(Modifier.height(14.dp)) Spacer(Modifier.height(6.dp))
SamplingRow( SamplingRow(
label = stringResource(R.string.sampling_top_p), label = stringResource(R.string.sampling_top_p),
param = samplingPrefs.topP, min = 0f, max = 1f, steps = 100, param = samplingPrefs.topP, min = 0f, max = 1f, steps = 100,
onUpdate = { onSamplingChange(samplingPrefs.copy(topP = it)) }, onUpdate = { onSamplingChange(samplingPrefs.copy(topP = it)) },
) )
Spacer(Modifier.height(14.dp)) Spacer(Modifier.height(6.dp))
SamplingRow( SamplingRow(
label = stringResource(R.string.sampling_top_k), label = stringResource(R.string.sampling_top_k),
param = samplingPrefs.topK, min = 1f, max = 100f, steps = 99, param = samplingPrefs.topK, min = 1f, max = 100f, steps = 99,
@ -255,6 +260,51 @@ fun ModePillsRow(
active = active, active = active,
onClick = { onToggle(mode.labelRes) }, onClick = { onToggle(mode.labelRes) },
) )
// Search provider dropdown — only for Vertex models when search is active
if (mode.labelRes == R.string.mode_search && active && isVertexModel) {
Box {
val providerLabel = stringResource(
if (webSearchProvider == "enterprise") R.string.search_provider_enterprise
else R.string.search_provider_google
)
ModePill(
icon = KaizenIcons.Globe,
label = providerLabel,
active = true,
showChevron = true,
onClick = { showSearchProviderMenu = true },
)
DropdownMenu(
expanded = showSearchProviderMenu,
onDismissRequest = { showSearchProviderMenu = false },
containerColor = if (isDark) Color(0xFF1A2030) else Color(0xFFF8F9FB),
shadowElevation = 16.dp,
shape = KaizenShapes.md,
) {
listOf("google" to R.string.search_provider_google, "enterprise" to R.string.search_provider_enterprise).forEach { (value, labelRes) ->
Row(
Modifier
.fillMaxWidth()
.clickable { onWebSearchProviderChange(value); showSearchProviderMenu = false }
.padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
stringResource(labelRes),
fontSize = 13.5.sp,
color = if (value == webSearchProvider) accent.primary else cs.onBackground.copy(alpha = 0.85f),
fontWeight = if (value == webSearchProvider) FontWeight.SemiBold else FontWeight.Normal,
modifier = Modifier.weight(1f),
)
if (value == webSearchProvider) {
Icon(KaizenIcons.Check, null, tint = accent.primary, modifier = Modifier.size(15.dp))
}
}
}
}
}
}
} }
} }
} }
@ -272,22 +322,22 @@ private fun ModePill(
val accent = LocalKaizenAccent.current val accent = LocalKaizenAccent.current
val bg = when { val bg = when {
active -> accent.primary.copy(alpha = if (isDark) 0.15f else 0.10f) active -> accent.primary.copy(alpha = if (isDark) 0.20f else 0.12f)
else -> if (isDark) Color.White.copy(alpha = 0.06f) else Color.Black.copy(alpha = 0.04f) else -> if (isDark) Color.White.copy(alpha = 0.10f) else Color.Black.copy(alpha = 0.07f)
} }
val borderBrush = if (active) { val borderBrush = if (active) {
Brush.verticalGradient(listOf( Brush.verticalGradient(listOf(
accent.primary.copy(alpha = if (isDark) 0.30f else 0.22f), accent.primary.copy(alpha = if (isDark) 0.40f else 0.30f),
accent.primary.copy(alpha = if (isDark) 0.08f else 0.06f), accent.primary.copy(alpha = if (isDark) 0.12f else 0.08f),
)) ))
} else { } else {
Brush.verticalGradient(listOf( 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.14f) else Color.Black.copy(alpha = 0.10f),
if (isDark) Color.White.copy(alpha = 0.02f) else Color.Black.copy(alpha = 0.03f), if (isDark) Color.White.copy(alpha = 0.05f) else Color.Black.copy(alpha = 0.04f),
)) ))
} }
val iconTint = if (active) accent.primary else cs.onSurfaceVariant.copy(alpha = 0.60f) val iconTint = if (active) accent.primary else cs.onSurfaceVariant.copy(alpha = 0.75f)
val textColor = if (active) cs.onBackground else cs.onSurfaceVariant.copy(alpha = 0.70f) val textColor = if (active) cs.onBackground else cs.onSurfaceVariant.copy(alpha = 0.80f)
Row( Row(
Modifier Modifier
@ -317,7 +367,7 @@ private fun SamplingRow(label: String, param: SamplingParam, min: Float, max: Fl
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Box( Box(
Modifier Modifier
.size(20.dp) .size(16.dp)
.clip(KaizenShapes.xs) .clip(KaizenShapes.xs)
.border( .border(
1.dp, 1.dp,
@ -329,18 +379,17 @@ private fun SamplingRow(label: String, param: SamplingParam, min: Float, max: Fl
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
if (param.enabled) { if (param.enabled) {
Icon(KaizenIcons.Check, null, tint = accent.primary, modifier = Modifier.size(13.dp)) Icon(KaizenIcons.Check, null, tint = accent.primary, modifier = Modifier.size(11.dp))
} }
} }
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(8.dp))
Text(label, color = cs.onBackground.copy(alpha = if (param.enabled) 0.90f else 0.50f), fontSize = 13.sp, modifier = Modifier.weight(1f)) Text(label, color = cs.onBackground.copy(alpha = if (param.enabled) 0.90f else 0.50f), fontSize = 12.sp, modifier = Modifier.weight(1f))
Text( Text(
if (max > 2f) param.value.toInt().toString() else "%.2f".format(param.value), if (max > 2f) param.value.toInt().toString() else "%.2f".format(param.value),
color = if (param.enabled) accent.primary else cs.onSurfaceVariant.copy(alpha = 0.40f), color = if (param.enabled) accent.primary else cs.onSurfaceVariant.copy(alpha = 0.40f),
fontSize = 12.sp, fontWeight = FontWeight.Medium, fontSize = 11.sp, fontWeight = FontWeight.Medium,
) )
} }
Spacer(Modifier.height(4.dp))
Slider( Slider(
value = param.value, onValueChange = { onUpdate(param.copy(value = it)) }, value = param.value, onValueChange = { onUpdate(param.copy(value = it)) },
valueRange = min..max, steps = steps, valueRange = min..max, steps = steps,
@ -353,7 +402,7 @@ private fun SamplingRow(label: String, param: SamplingParam, min: Float, max: Fl
disabledActiveTrackColor = cs.onSurfaceVariant.copy(alpha = 0.15f), disabledActiveTrackColor = cs.onSurfaceVariant.copy(alpha = 0.15f),
disabledInactiveTrackColor = if (isDark) Color.White.copy(alpha = 0.05f) else Color.Black.copy(alpha = 0.04f), disabledInactiveTrackColor = if (isDark) Color.White.copy(alpha = 0.05f) else Color.Black.copy(alpha = 0.04f),
), ),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth().height(28.dp),
) )
} }

View file

@ -114,6 +114,7 @@ fun ChatScreen(
var activeMode by remember { mutableStateOf<Int?>(null) } var activeMode by remember { mutableStateOf<Int?>(null) }
var reasoningPreset by remember { mutableStateOf(ReasoningPreset.Standard) } var reasoningPreset by remember { mutableStateOf(ReasoningPreset.Standard) }
var samplingPrefs by remember { mutableStateOf(SamplingPrefs()) } var samplingPrefs by remember { mutableStateOf(SamplingPrefs()) }
var webSearchProvider by remember { mutableStateOf("google") }
var usedTokens by remember { mutableStateOf(0) } var usedTokens by remember { mutableStateOf(0) }
var chatModel by remember { mutableStateOf<String?>(null) } var chatModel by remember { mutableStateOf<String?>(null) }
var streamJob by remember { mutableStateOf<Job?>(null) } var streamJob by remember { mutableStateOf<Job?>(null) }
@ -584,6 +585,7 @@ fun ChatScreen(
preferThroughput = reasoningPreset.throughput, preferThroughput = reasoningPreset.throughput,
attachments = uploadedAtts.takeIf { it.isNotEmpty() }, attachments = uploadedAtts.takeIf { it.isNotEmpty() },
webSearch = activeMode == R.string.mode_search, webSearch = activeMode == R.string.mode_search,
webSearchProvider = if (activeMode == R.string.mode_search && cfg.model.startsWith("vertex:")) webSearchProvider else null,
temperature = samplingPrefs.temperature.takeIf { it.enabled }?.value, temperature = samplingPrefs.temperature.takeIf { it.enabled }?.value,
topP = samplingPrefs.topP.takeIf { it.enabled }?.value, topP = samplingPrefs.topP.takeIf { it.enabled }?.value,
topK = samplingPrefs.topK.takeIf { it.enabled }?.value?.toInt(), topK = samplingPrefs.topK.takeIf { it.enabled }?.value?.toInt(),
@ -1055,6 +1057,9 @@ fun ChatScreen(
onReasoningChange = { reasoningPreset = it; haptics.tick() }, onReasoningChange = { reasoningPreset = it; haptics.tick() },
samplingPrefs = samplingPrefs, samplingPrefs = samplingPrefs,
onSamplingChange = { samplingPrefs = it }, 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) 6.dp else 10.dp))
ChatInput( ChatInput(

View file

@ -37,6 +37,7 @@ import java.util.concurrent.TimeUnit
val preferThroughput: Boolean? = null, val preferThroughput: Boolean? = null,
val attachments: List<Attachment>? = null, val attachments: List<Attachment>? = null,
val webSearch: Boolean? = null, val webSearch: Boolean? = null,
val webSearchProvider: String? = null,
val temperature: Float? = null, val temperature: Float? = null,
val topP: Float? = null, val topP: Float? = null,
val topK: Int? = null, val topK: Int? = null,
@ -487,6 +488,7 @@ object KaizenApi {
preferThroughput: Boolean = false, preferThroughput: Boolean = false,
attachments: List<Attachment>? = null, attachments: List<Attachment>? = null,
webSearch: Boolean = false, webSearch: Boolean = false,
webSearchProvider: String? = null,
temperature: Float? = null, temperature: Float? = null,
topP: Float? = null, topP: Float? = null,
topK: Int? = null, topK: Int? = null,
@ -499,6 +501,7 @@ object KaizenApi {
preferThroughput = if (preferThroughput) true else null, preferThroughput = if (preferThroughput) true else null,
attachments = attachments?.takeIf { it.isNotEmpty() }, attachments = attachments?.takeIf { it.isNotEmpty() },
webSearch = if (webSearch) true else null, webSearch = if (webSearch) true else null,
webSearchProvider = if (webSearch) webSearchProvider else null,
temperature = temperature, temperature = temperature,
topP = topP, topP = topP,
topK = topK, topK = topK,

View file

@ -48,6 +48,8 @@
<string name="mode_search">Search</string> <string name="mode_search">Search</string>
<string name="mode_video">Video</string> <string name="mode_video">Video</string>
<string name="mode_audio">Audio</string> <string name="mode_audio">Audio</string>
<string name="search_provider_google">Google</string>
<string name="search_provider_enterprise">Enterprise</string>
<!-- Suggestion pills --> <!-- Suggestion pills -->
<string name="suggest_brainstorm">Brainstorm</string> <string name="suggest_brainstorm">Brainstorm</string>

View file

@ -50,6 +50,8 @@
<string name="mode_search">Suche</string> <string name="mode_search">Suche</string>
<string name="mode_video">Video</string> <string name="mode_video">Video</string>
<string name="mode_audio">Audio</string> <string name="mode_audio">Audio</string>
<string name="search_provider_google">Google</string>
<string name="search_provider_enterprise">Enterprise</string>
<!-- Suggestion pills --> <!-- Suggestion pills -->
<string name="suggest_brainstorm">Brainstormen</string> <string name="suggest_brainstorm">Brainstormen</string>