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,
samplingPrefs: SamplingPrefs,
onSamplingChange: (SamplingPrefs) -> Unit,
currentModelId: String = "",
webSearchProvider: String = "google",
onWebSearchProviderChange: (String) -> Unit = {},
modifier: Modifier = Modifier,
) {
val cs = MaterialTheme.colorScheme
@ -159,6 +162,8 @@ fun ModePillsRow(
var showReasoningMenu by remember { mutableStateOf(false) }
var showSamplingPopup by remember { mutableStateOf(false) }
var showSearchProviderMenu by remember { mutableStateOf(false) }
val isVertexModel = currentModelId.startsWith("vertex:")
Row(
modifier
@ -224,19 +229,19 @@ fun ModePillsRow(
shadowElevation = 16.dp,
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(
label = stringResource(R.string.sampling_temperature),
param = samplingPrefs.temperature, min = 0f, max = 2f, steps = 200,
onUpdate = { onSamplingChange(samplingPrefs.copy(temperature = it)) },
)
Spacer(Modifier.height(14.dp))
Spacer(Modifier.height(6.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(14.dp))
Spacer(Modifier.height(6.dp))
SamplingRow(
label = stringResource(R.string.sampling_top_k),
param = samplingPrefs.topK, min = 1f, max = 100f, steps = 99,
@ -255,6 +260,51 @@ fun ModePillsRow(
active = active,
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 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)
active -> accent.primary.copy(alpha = if (isDark) 0.20f else 0.12f)
else -> if (isDark) Color.White.copy(alpha = 0.10f) else Color.Black.copy(alpha = 0.07f)
}
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),
accent.primary.copy(alpha = if (isDark) 0.40f else 0.30f),
accent.primary.copy(alpha = if (isDark) 0.12f else 0.08f),
))
} 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),
if (isDark) Color.White.copy(alpha = 0.14f) else Color.Black.copy(alpha = 0.10f),
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 textColor = if (active) cs.onBackground else cs.onSurfaceVariant.copy(alpha = 0.70f)
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.80f)
Row(
Modifier
@ -317,7 +367,7 @@ private fun SamplingRow(label: String, param: SamplingParam, min: Float, max: Fl
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
Modifier
.size(20.dp)
.size(16.dp)
.clip(KaizenShapes.xs)
.border(
1.dp,
@ -329,18 +379,17 @@ private fun SamplingRow(label: String, param: SamplingParam, min: Float, max: Fl
contentAlignment = Alignment.Center,
) {
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))
Text(label, color = cs.onBackground.copy(alpha = if (param.enabled) 0.90f else 0.50f), fontSize = 13.sp, modifier = Modifier.weight(1f))
Spacer(Modifier.width(8.dp))
Text(label, color = cs.onBackground.copy(alpha = if (param.enabled) 0.90f else 0.50f), fontSize = 12.sp, modifier = Modifier.weight(1f))
Text(
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),
fontSize = 12.sp, fontWeight = FontWeight.Medium,
fontSize = 11.sp, fontWeight = FontWeight.Medium,
)
}
Spacer(Modifier.height(4.dp))
Slider(
value = param.value, onValueChange = { onUpdate(param.copy(value = it)) },
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),
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 reasoningPreset by remember { mutableStateOf(ReasoningPreset.Standard) }
var samplingPrefs by remember { mutableStateOf(SamplingPrefs()) }
var webSearchProvider by remember { mutableStateOf("google") }
var usedTokens by remember { mutableStateOf(0) }
var chatModel by remember { mutableStateOf<String?>(null) }
var streamJob by remember { mutableStateOf<Job?>(null) }
@ -584,6 +585,7 @@ fun ChatScreen(
preferThroughput = reasoningPreset.throughput,
attachments = uploadedAtts.takeIf { it.isNotEmpty() },
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,
topP = samplingPrefs.topP.takeIf { it.enabled }?.value,
topK = samplingPrefs.topK.takeIf { it.enabled }?.value?.toInt(),
@ -1055,6 +1057,9 @@ fun ChatScreen(
onReasoningChange = { reasoningPreset = it; haptics.tick() },
samplingPrefs = samplingPrefs,
onSamplingChange = { samplingPrefs = it },
currentModelId = session.config?.model ?: "",
webSearchProvider = webSearchProvider,
onWebSearchProviderChange = { webSearchProvider = it; haptics.tick() },
)
Spacer(Modifier.height(if (imeVisible) 6.dp else 10.dp))
ChatInput(

View file

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

View file

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

View file

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