UI: polish model card

This commit is contained in:
Han Yin 2025-04-20 17:13:29 -07:00
parent c12ef7a779
commit 6b74c49e6b
2 changed files with 165 additions and 126 deletions

View File

@ -46,8 +46,7 @@ import java.util.Locale
* Displays model information in a card format with core details.
*
* This component shows essential model information like name, context length,
* architecture, and quantization in a compact card format. It can be used
* in lists where only basic information is needed.
* architecture, quantization and file size in a compact card format.
*
* @param model The model information to display
* @param onClick Action to perform when the card is clicked
@ -92,52 +91,12 @@ fun ModelCardCore(
}
}
@Composable
fun ModelCardContentCore(
model: ModelInfo,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
// Row 1: Model full name
Text(
text = model.formattedFullName,
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(12.dp))
// Row 2: Context length, size label
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
ModelCardContentField("Context", model.formattedContextLength)
ModelCardContentField("Params", model.formattedParamSize)
}
Spacer(modifier = Modifier.height(8.dp))
// Row 3: Architecture, quantization, formatted size
Row(
modifier = Modifier.fillMaxWidth(),
) {
ModelCardContentField("Architecture", model.formattedArchitecture)
Spacer(modifier = Modifier.weight(1f))
ModelCardContentField(model.formattedQuantization, model.formattedFileSize)
}
}
}
/**
* Displays model information in a card format with expandable details.
*
* This component shows essential model information and can be expanded to show
* additional details such as dates, tags, and languages. The expanded state is
* toggled by clicking on the content area of the card.
* additional details such as dates, tags, and languages.
* The expanded state is toggled by clicking on the content area of the card.
*
* @param model The model information to display
* @param isSelected Optional selection state (shows checkbox when not null)
@ -145,6 +104,7 @@ fun ModelCardContentCore(
* @param isExpanded Whether additional details is expanded or shrunk
* @param onExpanded Action to perform when the card is expanded or shrunk
*/
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ModelCardExpandable(
model: ModelInfo,
@ -187,11 +147,27 @@ fun ModelCardExpandable(
.weight(1f)
.padding(start = 16.dp, top = 16.dp, end = 16.dp)
) {
// Core content always visible
ModelCardContentCore(model = model)
// Row 1: Model full name
Text(
text = model.formattedFullName,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Medium
)
}
}
Spacer(modifier = Modifier.height(12.dp))
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
// Row 2: Context length, size label
ModelCardContentContextRow(model)
Spacer(modifier = Modifier.height(8.dp))
// Row 3: Architecture, quantization, formatted size
ModelCardContentArchitectureRow(model)
}
// Expandable content
AnimatedVisibility(
visible = isExpanded,
@ -203,86 +179,28 @@ fun ModelCardExpandable(
.weight(1f)
.padding(horizontal = 16.dp)
) {
ExpandableModelDetails(model = model)
}
}
}
}
}
}
// Displays expandable details for a model, to be used only inside ModelCardExpandable.
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun ExpandableModelDetails(model: ModelInfo) {
val dateFormatter = remember { SimpleDateFormat("MMM d, yyyy", Locale.getDefault()) }
Column(modifier = Modifier.padding(top = 16.dp)) {
Column(modifier = Modifier.padding(top = 12.dp)) {
// Divider between core and expanded content
HorizontalDivider(modifier = Modifier.padding(bottom = 16.dp))
HorizontalDivider(modifier = Modifier.padding(bottom = 12.dp))
// Dates
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
// Added date
ModelCardContentField("Added", dateFormatter.format(Date(model.dateAdded)))
// Row 4: Dates
ModelCardContentDatesRow(model)
// Last used date (if available)
model.dateLastUsed?.let { lastUsed ->
ModelCardContentField("Last used", dateFormatter.format(Date(lastUsed)))
}
}
// Tags (if available)
// Row 5: Tags
model.tags?.let { tags ->
Spacer(modifier = Modifier.height(8.dp))
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
tags.forEach { tag ->
AssistChip(
onClick = { /* No action */ },
label = {
Text(
text = tag,
style = MaterialTheme.typography.bodySmall,
fontStyle = FontStyle.Italic,
fontWeight = FontWeight.Light,
color = MaterialTheme.colorScheme.onSurface
)
}
)
}
}
ModelCardContentTagsSection(tags)
}
// Languages (if available)
// Row 6: Languages
model.languages?.let { languages ->
Spacer(modifier = Modifier.height(8.dp))
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
languages.forEach { language ->
AssistChip(
onClick = { /* No action */ },
label = {
Text(
text = language,
style = MaterialTheme.typography.bodySmall,
fontStyle = FontStyle.Italic,
fontWeight = FontWeight.Light,
color = MaterialTheme.colorScheme.onSurface
)
ModelCardContentLanguagesSections(languages)
}
}
}
)
}
}
}
@ -358,6 +276,119 @@ fun ModelCardWithSystemPrompt(
}
}
@Composable
fun ModelCardContentCore(
model: ModelInfo,
modifier: Modifier = Modifier
) =
Column(modifier = modifier) {
// Row 1: Model full name
Text(
text = model.formattedFullName,
style = MaterialTheme.typography.headlineSmall, // TODO
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(12.dp))
// Row 2: Context length, size label
ModelCardContentContextRow(model)
Spacer(modifier = Modifier.height(8.dp))
// Row 3: Architecture, quantization, formatted size
ModelCardContentArchitectureRow(model)
}
@Composable
private fun ModelCardContentContextRow(model: ModelInfo) =
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
ModelCardContentField("Context", model.formattedContextLength)
ModelCardContentField("Params", model.formattedParamSize)
}
@Composable
private fun ModelCardContentArchitectureRow(model: ModelInfo) =
Row(
modifier = Modifier.fillMaxWidth(),
) {
ModelCardContentField("Architecture", model.formattedArchitecture)
Spacer(modifier = Modifier.weight(1f))
ModelCardContentField(model.formattedQuantization, model.formattedFileSize)
}
@Composable
private fun ModelCardContentDatesRow(model: ModelInfo) {
val dateFormatter = remember { SimpleDateFormat("MMM d, yyyy", Locale.getDefault()) }
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
// Added date
ModelCardContentField("Added", dateFormatter.format(Date(model.dateAdded)))
// Last used date (if available)
model.dateLastUsed?.let { lastUsed ->
ModelCardContentField("Last used", dateFormatter.format(Date(lastUsed)))
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun ModelCardContentTagsSection(tags: List<String>) =
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
tags.forEach { tag ->
AssistChip(
onClick = { /* No action */ },
label = {
Text(
text = tag,
style = MaterialTheme.typography.bodySmall,
fontStyle = FontStyle.Italic,
fontWeight = FontWeight.Light,
color = MaterialTheme.colorScheme.onSurface
)
}
)
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun ModelCardContentLanguagesSections(languages: List<String>) =
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
languages.forEach { language ->
AssistChip(
onClick = { /* No action */ },
label = {
Text(
text = language,
style = MaterialTheme.typography.bodySmall,
fontStyle = FontStyle.Italic,
fontWeight = FontWeight.Light,
color = MaterialTheme.colorScheme.onSurface
)
}
)
}
}
@Composable
private fun ModelCardContentField(name: String, value: String) =
Row {

View File

@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope
import com.example.llama.revamp.data.model.ModelInfo
import com.example.llama.revamp.data.repository.InsufficientStorageException
import com.example.llama.revamp.data.repository.ModelRepository
import com.example.llama.revamp.util.GgufMetadataReader
import com.example.llama.revamp.util.getFileNameFromUri
import com.example.llama.revamp.util.getFileSizeFromUri
import com.example.llama.revamp.viewmodel.ModelManagementState.Deletion
@ -192,7 +193,14 @@ class ModelsManagementViewModel @Inject constructor(
}
// TODO-han.yin: Stub for now. Would need to investigate HuggingFace APIs
fun importFromHuggingFace() {}
fun importFromHuggingFace() {
viewModelScope.launch {
// val path = "/data/user/0/com.example.llama/files/models/Phi-4-mini-instruct-Q4_0.gguf"
val path = "/data/user/0/com.example.llama/files/models/gemma-3-4b-it-Q4_K_M.gguf"
val metadata = GgufMetadataReader().readStructuredMetadata(path)
Log.i("JOJO", "GGUF Metadata for $path:\n $metadata")
}
}
/**
* First show confirmation instead of starting deletion immediately