feat: slash command autocomplete, dynamic theme colors, interactive model selection & drawer icons

This commit is contained in:
Hy4ri committed 2026-06-16 18:15:01 +03:00
1 parent c06e8417db
commit 3830236585
5 files changed
+314 -67

No files matched your search

@@ -2,6 +2,7 @@ package com.m57.hermescontrol.ui.model
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -15,6 +16,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Card
@@ -196,11 +198,68 @@ fun ModelScreen(
)
} else {
models.forEach { model ->
Text(
text = "• $model",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(vertical = 4.dp),
)
val isActive =
state.activeProfile?.provider == provider.slug &&
state.activeProfile?.model == model
Card(
onClick = {
viewModel.selectModel(provider.slug, model)
},
modifier =
Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
colors =
CardDefaults.cardColors(
containerColor =
if (isActive) {
MaterialTheme.colorScheme.primaryContainer.copy(
alpha = 0.3f,
)
} else {
MaterialTheme.colorScheme.surface
},
),
border =
BorderStroke(
width = 1.dp,
color =
if (isActive) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)
},
),
) {
Row(
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = model,
style = MaterialTheme.typography.bodyMedium,
fontWeight =
if (isActive) FontWeight.Bold else FontWeight.Normal,
color =
if (isActive) {
MaterialTheme.colorScheme.onPrimaryContainer
} else {
MaterialTheme.colorScheme.onSurface
},
)
if (isActive) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = "Active Model",
tint = MaterialTheme.colorScheme.primary,
)
}
}
}
}
}
}
@@ -3,6 +3,8 @@ package com.m57.hermescontrol.ui.model
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.m57.hermescontrol.data.model.ModelProvider
import com.m57.hermescontrol.data.model.ProfileInfo
import com.m57.hermescontrol.data.model.UpdateProfileModelRequest
import com.m57.hermescontrol.data.remote.ApiClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -15,6 +17,7 @@ import kotlinx.coroutines.withContext
data class ModelUiState(
val isLoading: Boolean = false,
val providers: List<ModelProvider> = emptyList(),
val activeProfile: ProfileInfo? = null,
val errorMessage: String? = null,
val toastMessage: String? = null,
)
@@ -31,11 +34,30 @@ class ModelViewModel : ViewModel() {
withContext(Dispatchers.IO) {
ApiClient.hermesApi.getModelOptions()
}
val activeProfileNameRes =
withContext(Dispatchers.IO) {
ApiClient.hermesApi.getActiveProfile()
}
val profilesRes =
withContext(Dispatchers.IO) {
ApiClient.hermesApi.getProfiles()
}
if (response.isSuccessful) {
val activeName =
if (activeProfileNameRes.isSuccessful) activeProfileNameRes.body()?.active else null
val activeProfile =
if (profilesRes.isSuccessful && activeName != null) {
profilesRes.body()?.profiles?.find { it.name == activeName }
} else {
null
}
_uiState.update {
it.copy(
isLoading = false,
providers = response.body()?.providers.orEmpty(),
activeProfile = activeProfile,
)
}
} else {
@@ -57,6 +79,49 @@ class ModelViewModel : ViewModel() {
}
}
fun selectModel(
providerSlug: String,
modelName: String,
) {
viewModelScope.launch {
try {
val activeProfileNameRes =
withContext(Dispatchers.IO) {
ApiClient.hermesApi.getActiveProfile()
}
if (!activeProfileNameRes.isSuccessful) {
_uiState.update {
it.copy(
toastMessage = "Failed to fetch active profile: HTTP ${activeProfileNameRes.code()}",
)
}
return@launch
}
val activeProfileName = activeProfileNameRes.body()?.active
if (activeProfileName == null) {
_uiState.update { it.copy(toastMessage = "No active profile found") }
return@launch
}
val updateRes =
withContext(Dispatchers.IO) {
ApiClient.hermesApi.updateProfileModel(
activeProfileName,
UpdateProfileModelRequest(providerSlug, modelName),
)
}
if (updateRes.isSuccessful) {
_uiState.update { it.copy(toastMessage = "Successfully set model to $modelName") }
loadModelOptions()
} else {
_uiState.update { it.copy(toastMessage = "Failed to set model: HTTP ${updateRes.code()}") }
}
} catch (e: Exception) {
_uiState.update { it.copy(toastMessage = "Failed to set model: ${e.message}") }
}
}
}
fun clearToast() {
_uiState.update { it.copy(toastMessage = null) }
}