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:
parent
a16dfffd5c
commit
30d3f2807d
3 changed files with 19 additions and 14 deletions
|
|
@ -142,14 +142,10 @@ fun EmptyHero(userName: String, onPick: (String) -> Unit, modifier: Modifier = M
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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 cs = MaterialTheme.colorScheme
|
||||||
val isDark = isSystemInDarkTheme()
|
val isDark = isSystemInDarkTheme()
|
||||||
|
val accent = LocalKaizenAccent.current
|
||||||
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)))
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier
|
modifier
|
||||||
|
|
@ -158,25 +154,30 @@ fun ModePillsRow(selected: Set<Int>, onToggle: (Int) -> Unit, modifier: Modifier
|
||||||
.padding(horizontal = 14.dp),
|
.padding(horizontal = 14.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.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 label = stringResource(mode.labelRes)
|
||||||
val active = mode.labelRes in selected
|
val active = mode.labelRes == activeMode
|
||||||
val backgroundFill = if (active) {
|
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 {
|
} else {
|
||||||
if (isDark) cs.surface.copy(alpha = 0.35f) else cs.surface.copy(alpha = 0.55f)
|
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(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.clip(KaizenShapes.pill)
|
.clip(KaizenShapes.pill)
|
||||||
.background(backgroundFill)
|
.background(backgroundFill)
|
||||||
.border(1.2.dp, glassBorder, KaizenShapes.pill)
|
.border(1.2.dp, borderColor, KaizenShapes.pill)
|
||||||
.clickable { onToggle(mode.labelRes) }
|
.clickable { onToggle(mode.labelRes) }
|
||||||
.padding(horizontal = 13.dp, vertical = 8.dp),
|
.padding(horizontal = 13.dp, vertical = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
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))
|
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)
|
Text(label, color = if (active) cs.onBackground else cs.onSurfaceVariant, fontSize = 13.sp, fontWeight = if (active) FontWeight.SemiBold else FontWeight.Medium)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ fun ChatScreen(
|
||||||
var input by remember { mutableStateOf("") }
|
var input by remember { mutableStateOf("") }
|
||||||
var isStreaming by remember { mutableStateOf(false) }
|
var isStreaming by remember { mutableStateOf(false) }
|
||||||
var nextId by remember { mutableStateOf(0L) }
|
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
|
// Navigation State
|
||||||
var currentScreen by remember { mutableStateOf(AppScreen.Chat) }
|
var currentScreen by remember { mutableStateOf(AppScreen.Chat) }
|
||||||
|
|
@ -339,6 +339,7 @@ fun ChatScreen(
|
||||||
KaizenApi.chat(
|
KaizenApi.chat(
|
||||||
cfg.baseUrl, cfg.token, cfg.model, history, cfg.speed,
|
cfg.baseUrl, cfg.token, cfg.model, history, cfg.speed,
|
||||||
attachments = uploadedAtts.takeIf { it.isNotEmpty() },
|
attachments = uploadedAtts.takeIf { it.isNotEmpty() },
|
||||||
|
webSearch = activeMode == R.string.mode_search,
|
||||||
).collect { state ->
|
).collect { state ->
|
||||||
val idx = messages.indexOfLast { it.id == assistantId }
|
val idx = messages.indexOfLast { it.id == assistantId }
|
||||||
if (idx < 0) return@collect
|
if (idx < 0) return@collect
|
||||||
|
|
@ -621,9 +622,9 @@ fun ChatScreen(
|
||||||
) {
|
) {
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
ModePillsRow(
|
ModePillsRow(
|
||||||
selected = selectedModes,
|
activeMode = activeMode,
|
||||||
onToggle = { mode ->
|
onToggle = { mode ->
|
||||||
selectedModes = if (mode in selectedModes) selectedModes - mode else selectedModes + mode
|
activeMode = if (activeMode == mode) null else mode
|
||||||
haptics.tick()
|
haptics.tick()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import java.util.concurrent.TimeUnit
|
||||||
val reasoningEffort: String? = null,
|
val reasoningEffort: String? = null,
|
||||||
val preferThroughput: Boolean? = null,
|
val preferThroughput: Boolean? = null,
|
||||||
val attachments: List<Attachment>? = 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. */
|
/** 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>,
|
messages: List<WireMessage>,
|
||||||
speed: ChatSpeed = ChatSpeed.Normal,
|
speed: ChatSpeed = ChatSpeed.Normal,
|
||||||
attachments: List<Attachment>? = null,
|
attachments: List<Attachment>? = null,
|
||||||
|
webSearch: Boolean = false,
|
||||||
): Flow<StreamState> = flow {
|
): Flow<StreamState> = flow {
|
||||||
val payload = json.encodeToString(
|
val payload = json.encodeToString(
|
||||||
ChatRequest(
|
ChatRequest(
|
||||||
|
|
@ -448,6 +450,7 @@ object KaizenApi {
|
||||||
reasoningEffort = speed.reasoningEffort,
|
reasoningEffort = speed.reasoningEffort,
|
||||||
preferThroughput = if (speed.preferThroughput) true else null,
|
preferThroughput = if (speed.preferThroughput) true else null,
|
||||||
attachments = attachments?.takeIf { it.isNotEmpty() },
|
attachments = attachments?.takeIf { it.isNotEmpty() },
|
||||||
|
webSearch = if (webSearch) true else null,
|
||||||
),
|
),
|
||||||
).toRequestBody(jsonMedia)
|
).toRequestBody(jsonMedia)
|
||||||
val req = Request.Builder()
|
val req = Request.Builder()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue