ui: input bar reasoning chip tweaks (#673)
* ui: input bar reasoning chip tweaks - Remove emoji from reasoning level labels (dropdown + chip text) - Reduce reasoning chip horizontal padding (8dp -> 4dp) - Move reasoning chip to right side (after spacer, next to mic) - Increase model chip minimum width (120dp -> 140dp) - Remove unused width import * fix(ui): add missing widthIn import in ComposerToolbar Resolves 'Unresolved reference widthIn' compile error at ComposerToolbar.kt:95 that broke CI (unit-tests, android-lint, instrumented-tests all fail on compileDebugKotlin). * ui(chat): enhance input bar layout, reasoning menu header, and card shadow Give ChatInputBar a floating rounded container with shadow contrast. Make model chip take available space and add header to reasoning menu. * fix(ui): remove trailing blank line in ComposerToolbar for ktlint Fix standard:no-consecutive-blank-lines ktlint violation. * ui(model): enhance ModelPickerDialog with rounded cards and provider badges Apply card styling, rounded corners (24dp dialog, 12dp cards), provider badges, and shadow contrast to ModelPickerDialog. * feat(model): add in-dialog model pinning to ModelPickerDialog Add pin/unpin icon buttons to model cards in ModelPickerDialog across both ModelScreen and ChatScreen. * ui(model): remove provider badge chip and eliminate bottom whitespace in ModelPickerDialog Replace bottom Cancel button with top-right Close icon to eliminate bottom whitespace. Remove inline provider badge chip from model cards. * perf(model): optimize ModelPickerDialog load time with lazy items and O(1) set lookup Lazy-register individual model items in LazyColumn and memoize pinnedSet to prevent synchronous rendering lag when opening dialog. --------- Co-authored-by: M57 <Hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
56de3323f0
commit
0bb418b4a7
7 files changed
+255
-86
No files matched your search
@@ -242,6 +242,8 @@ fun ModelScreen(
|
||||
providers = state.providers,
|
||||
title = "Set Main Model",
|
||||
isLoading = state.isLoading && state.providers.isEmpty(),
|
||||
pinnedModels = state.pinnedModels,
|
||||
onPinToggle = { provider, model -> viewModel.togglePinModel(provider, model) },
|
||||
onSelect = { provider, model ->
|
||||
viewModel.setMainModel(provider, model)
|
||||
},
|
||||
@@ -266,6 +268,8 @@ fun ModelScreen(
|
||||
providers = state.providers,
|
||||
title = "Set Aux: ${state.auxPickerTask}",
|
||||
isLoading = state.isLoading && state.providers.isEmpty(),
|
||||
pinnedModels = state.pinnedModels,
|
||||
onPinToggle = { provider, model -> viewModel.togglePinModel(provider, model) },
|
||||
onSelect = { provider, model ->
|
||||
viewModel.setAuxTask(state.auxPickerTask, provider, model)
|
||||
},
|
||||
|
||||
@@ -524,4 +524,16 @@ class ModelViewModel :
|
||||
_uiState.update { it.copy(pinnedModels = currentPinned) }
|
||||
}
|
||||
}
|
||||
|
||||
fun togglePinModel(
|
||||
providerSlug: String,
|
||||
modelName: String,
|
||||
) {
|
||||
val target = PinnedModel(providerSlug, modelName)
|
||||
if (_uiState.value.pinnedModels.contains(target)) {
|
||||
unpinModel(providerSlug, modelName)
|
||||
} else {
|
||||
pinModel(providerSlug, modelName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,29 @@
|
||||
package com.m57.hermescontrol.ui.model.components
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.PushPin
|
||||
import androidx.compose.material.icons.outlined.PushPin
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -25,7 +31,9 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.m57.hermescontrol.data.model.ModelProvider
|
||||
import com.m57.hermescontrol.data.model.PinnedModel
|
||||
@@ -40,6 +48,7 @@ import com.m57.hermescontrol.ui.common.SearchBar
|
||||
*
|
||||
* [pinnedModels] renders a pinned section at the top (mirrors the global model
|
||||
* screen's pin section) so frequently-used models are one tap away.
|
||||
* [onPinToggle] allows pinning and unpinning models directly inside the dialog.
|
||||
*/
|
||||
@Composable
|
||||
fun ModelPickerDialog(
|
||||
@@ -47,6 +56,7 @@ fun ModelPickerDialog(
|
||||
title: String,
|
||||
isLoading: Boolean = false,
|
||||
pinnedModels: List<PinnedModel> = emptyList(),
|
||||
onPinToggle: ((provider: String, model: String) -> Unit)? = null,
|
||||
onSelect: (provider: String, model: String) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
@@ -54,7 +64,33 @@ fun ModelPickerDialog(
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(title) },
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = 6.dp,
|
||||
title = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
IconButton(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier.size(32.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Close",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
if (isLoading) {
|
||||
@@ -81,21 +117,47 @@ fun ModelPickerDialog(
|
||||
}
|
||||
}
|
||||
|
||||
val pinnedSet =
|
||||
remember(pinnedModels) {
|
||||
pinnedModels.map { "${it.providerSlug}:${it.modelName}" }.toSet()
|
||||
}
|
||||
|
||||
val filteredProvidersWithModels =
|
||||
remember(pickerQuery, providers) {
|
||||
providers.mapNotNull { provider ->
|
||||
val matchingModels =
|
||||
provider.models.orEmpty().filter { model ->
|
||||
pickerQuery.isBlank() ||
|
||||
provider.name.contains(pickerQuery, ignoreCase = true) ||
|
||||
provider.slug.contains(pickerQuery, ignoreCase = true) ||
|
||||
model.contains(pickerQuery, ignoreCase = true)
|
||||
}
|
||||
if (matchingModels.isNotEmpty()) {
|
||||
provider to matchingModels
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SearchBar(
|
||||
query = pickerQuery,
|
||||
onQueryChange = { pickerQuery = it },
|
||||
placeholder = "Search models and providers...",
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
LazyColumn(modifier = Modifier.height(400.dp)) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.heightIn(max = 420.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
// ── Pinned section ──
|
||||
if (filteredPinned.isNotEmpty()) {
|
||||
item(key = "pinned-header") {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = Modifier.padding(top = 4.dp, bottom = 4.dp),
|
||||
modifier = Modifier.padding(top = 4.dp, bottom = 6.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PushPin,
|
||||
@@ -105,8 +167,9 @@ fun ModelPickerDialog(
|
||||
)
|
||||
Text(
|
||||
text = "Pinned",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -114,66 +177,122 @@ fun ModelPickerDialog(
|
||||
filteredPinned,
|
||||
key = { "pinned:${it.providerSlug}:${it.modelName}" },
|
||||
) { pinned ->
|
||||
OutlinedButton(
|
||||
ModelItemCard(
|
||||
modelName = pinned.modelName,
|
||||
isPinned = true,
|
||||
onPinToggle =
|
||||
if (onPinToggle != null) {
|
||||
{ onPinToggle(pinned.providerSlug, pinned.modelName) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onClick = { onSelect(pinned.providerSlug, pinned.modelName) },
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
|
||||
contentPadding =
|
||||
androidx.compose.foundation.layout
|
||||
.PaddingValues(horizontal = 12.dp, vertical = 6.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "${pinned.modelName} · ${pinned.providerSlug}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── All providers / models ──
|
||||
items(
|
||||
providers.filter { provider ->
|
||||
pickerQuery.isBlank() ||
|
||||
provider.name.contains(pickerQuery, ignoreCase = true) ||
|
||||
provider.slug.contains(pickerQuery, ignoreCase = true) ||
|
||||
provider.models.orEmpty().any {
|
||||
it.contains(pickerQuery, ignoreCase = true)
|
||||
}
|
||||
},
|
||||
key = { it.slug },
|
||||
) { provider ->
|
||||
val models =
|
||||
provider.models.orEmpty().filter {
|
||||
pickerQuery.isBlank() ||
|
||||
it.contains(pickerQuery, ignoreCase = true)
|
||||
// ── All providers / models (lazy item per model) ──
|
||||
filteredProvidersWithModels.forEach { (provider, models) ->
|
||||
item(key = "header:${provider.slug}") {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(top = 12.dp, bottom = 6.dp),
|
||||
) {
|
||||
Text(
|
||||
text = provider.name,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = "${models.size} models",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (models.isNotEmpty()) {
|
||||
Text(
|
||||
text = provider.name,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp),
|
||||
items(
|
||||
items = models,
|
||||
key = { model -> "${provider.slug}:$model" },
|
||||
) { model ->
|
||||
val isPinned = "${provider.slug}:$model" in pinnedSet
|
||||
ModelItemCard(
|
||||
modelName = model,
|
||||
isPinned = isPinned,
|
||||
onPinToggle =
|
||||
if (onPinToggle != null) {
|
||||
{ onPinToggle(provider.slug, model) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onClick = { onSelect(provider.slug, model) },
|
||||
)
|
||||
|
||||
models.forEach { model ->
|
||||
OutlinedButton(
|
||||
onClick = { onSelect(provider.slug, model) },
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
|
||||
contentPadding =
|
||||
androidx.compose.foundation.layout
|
||||
.PaddingValues(horizontal = 12.dp, vertical = 6.dp),
|
||||
) {
|
||||
Text(text = model, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) { Text("Cancel") }
|
||||
},
|
||||
confirmButton = {},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ModelItemCard(
|
||||
modelName: String,
|
||||
isPinned: Boolean,
|
||||
onPinToggle: (() -> Unit)?,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Surface(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 2.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.clickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
border =
|
||||
BorderStroke(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f),
|
||||
),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(start = 12.dp, end = 4.dp, top = 4.dp, bottom = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = modelName,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
if (onPinToggle != null) {
|
||||
IconButton(
|
||||
onClick = onPinToggle,
|
||||
modifier = Modifier.size(32.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin,
|
||||
contentDescription = if (isPinned) "Unpin model" else "Pin model",
|
||||
tint =
|
||||
if (isPinned) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)
|
||||
},
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user