UI: refactor ModelCard UI to show GGUF metadata

This commit is contained in:
Han Yin 2025-04-19 22:43:22 -07:00
parent 9056f27a91
commit b81a0c6e6d
11 changed files with 602 additions and 120 deletions

View File

@ -1,7 +1,10 @@
package com.example.llama.revamp.data.model
import com.example.llama.revamp.util.FileType
import com.example.llama.revamp.util.GgufMetadata
import com.example.llama.revamp.util.formatSize
import com.example.llama.revamp.util.formatContextLength
import com.example.llama.revamp.util.formatFileByteSize
/**
* Data class containing information about an LLM model.
@ -15,6 +18,25 @@ data class ModelInfo(
val dateAdded: Long,
val dateLastUsed: Long? = null,
) {
val formattedSize: String
get() = formatSize(sizeInBytes)
val formattedFullName: String
get() = metadata.fullModelName ?: name
val formattedFileSize: String
get() = formatFileByteSize(sizeInBytes)
val formattedArchitecture: String
get() = metadata.architecture?.architecture ?: "-"
val formattedParamSize: String
get() = metadata.basic.sizeLabel ?: "-"
val formattedContextLength: String
get() = metadata.dimensions?.contextLength?.let { formatContextLength(it) } ?: "-"
val formattedQuantization: String
get() = metadata.architecture?.fileType?.let { FileType.fromCode(it).label } ?: "-"
val tags: List<String>? = metadata.additional?.tags?.takeIf { it.isNotEmpty() }
val languages: List<String>? = metadata.additional?.languages?.takeIf { it.isNotEmpty() }
}

View File

@ -11,7 +11,7 @@ import com.example.llama.revamp.data.repository.ModelRepository.ImportProgressTr
import com.example.llama.revamp.util.GgufMetadataReader
import com.example.llama.revamp.util.copyWithBuffer
import com.example.llama.revamp.util.copyWithChannels
import com.example.llama.revamp.util.formatSize
import com.example.llama.revamp.util.formatFileByteSize
import com.example.llama.revamp.util.getFileNameFromUri
import com.example.llama.revamp.util.getFileSizeFromUri
import dagger.hilt.android.qualifiers.ApplicationContext
@ -135,7 +135,7 @@ class ModelRepositoryImpl @Inject constructor(
val fileSize = size ?: getFileSizeFromUri(context, uri) ?: throw FileNotFoundException("File size N/A")
if (!hasEnoughSpaceForImport(fileSize)) {
throw InsufficientStorageException(
"Not enough storage space. Required: ${formatSize(fileSize)}, Available: ${formatSize(availableSpaceBytes)}"
"Not enough storage space. Required: ${formatFileByteSize(fileSize)}, Available: ${formatFileByteSize(availableSpaceBytes)}"
)
}

View File

@ -6,32 +6,36 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
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.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.AssistChip
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.example.llama.revamp.data.model.ModelInfo
import java.text.SimpleDateFormat
@ -39,24 +43,30 @@ import java.util.Date
import java.util.Locale
/**
* Standard model card for selection lists
* 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.
*
* @param model The model information to display
* @param onClick Action to perform when the card is clicked
* @param isSelected Optional selection state (shows checkbox when not null)
*/
@Composable
fun ModelCard(
fun ModelCardCore(
model: ModelInfo,
onClick: () -> Unit,
modifier: Modifier = Modifier,
isSelected: Boolean? = null,
actionButton: @Composable (() -> Unit)? = null
) {
Card(
modifier = modifier
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick),
colors = when {
isSelected == true -> CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
isSelected == false -> CardDefaults.cardColors()
else -> CardDefaults.cardColors() // Not in selection mode
colors = when (isSelected) {
true -> CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
false -> CardDefaults.cardColors()
else -> CardDefaults.cardColors()
},
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
@ -73,15 +83,204 @@ fun ModelCard(
)
}
// Model info
ModelInfoContent(
// Core model info
ModelCardContentCore(
model = model,
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(0.dp)
modifier = Modifier.weight(1f)
)
}
}
}
// Custom action button or built-in ones
actionButton?.invoke() ?: Spacer(modifier = Modifier.width(8.dp))
@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.
*
* @param model The model information to display
* @param onClick Action to perform when the card is clicked (for selection)
* @param expanded Whether additional details are currently shown
* @param isSelected Optional selection state (shows checkbox when not null)
*/
@Composable
fun ModelCardExpandable(
model: ModelInfo,
onClick: () -> Unit,
expanded: Boolean,
isSelected: Boolean? = null,
) {
var isExpanded by remember { mutableStateOf(expanded) }
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { isExpanded = !isExpanded }
,
colors = when (isSelected) {
true -> CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
false -> CardDefaults.cardColors()
else -> CardDefaults.cardColors()
},
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier.padding(bottom = 16.dp)
) {
Row(
verticalAlignment = Alignment.Top
) {
// Show checkbox if in selection mode
if (isSelected != null) {
Checkbox(
checked = isSelected,
onCheckedChange = { onClick() },
modifier = Modifier.padding(top = 16.dp, start = 16.dp)
)
}
Box(
modifier = Modifier.weight(1f)
.padding(start = 16.dp, top = 16.dp, end = 16.dp)
) {
// Core content always visible
ModelCardContentCore(model = model)
}
}
// Expandable content
AnimatedVisibility(
visible = isExpanded,
enter = expandVertically() + fadeIn(),
exit = shrinkVertically() + fadeOut()
) {
Box(
modifier = Modifier.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)) {
// Divider between core and expanded content
HorizontalDivider(modifier = Modifier.padding(bottom = 16.dp))
// Dates
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)))
}
}
// Tags (if available)
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
)
}
)
}
}
}
// Languages (if available)
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
)
}
)
}
}
}
}
}
@ -111,9 +310,8 @@ fun ModelCardWithSystemPrompt(
modifier = Modifier.padding(16.dp)
) {
// Model info section
ModelInfoContent(
model = model,
contentPadding = PaddingValues(0.dp)
ModelCardContentCore(
model = model
)
// Add divider between model info and system prompt
@ -156,60 +354,23 @@ fun ModelCardWithSystemPrompt(
}
}
/**
* Core model info display component that can be used by other card variants
*/
@Composable
private fun ModelInfoContent(
model: ModelInfo,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(16.dp)
) {
Column(modifier = modifier.padding(contentPadding)) {
private fun ModelCardContentField(name: String, value: String) =
Row {
Text(
text = model.name,
style = MaterialTheme.typography.titleMedium
text = name,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Normal,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.width(8.dp))
// TODO-han.yin: make use of GGUF metadata
// Last used date
model.lastUsed?.let { lastUsed ->
val dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.getDefault())
Text(
text = "Last used: ${dateFormat.format(Date(lastUsed))}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Text(
text = value,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Light,
fontStyle = FontStyle.Italic,
color = MaterialTheme.colorScheme.onSurface
)
}
}
/**
* Predefined action buttons for ModelCard
*/
object ModelCardActions {
@Composable
fun PlayButton(onClick: () -> Unit) {
IconButton(onClick = onClick) {
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = "Select model",
tint = MaterialTheme.colorScheme.primary
)
}
}
@Composable
fun InfoButton(onClick: () -> Unit) {
IconButton(onClick = onClick) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = "Model details"
)
}
}
}

View File

@ -25,7 +25,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.llama.revamp.engine.ModelLoadingMetrics
import com.example.llama.revamp.ui.components.ModelCard
import com.example.llama.revamp.ui.components.ModelCardCore
import com.example.llama.revamp.ui.components.ModelUnloadDialogHandler
import com.example.llama.revamp.ui.theme.MonospacedTextStyle
import com.example.llama.revamp.viewmodel.BenchmarkViewModel
@ -59,10 +59,9 @@ fun BenchmarkScreen(
) {
// Selected model card
selectedModel?.let { model ->
ModelCard(
ModelCardCore(
model = model,
onClick = { /* No action on click */ },
modifier = Modifier.padding(bottom = 16.dp),
isSelected = null
)
}

View File

@ -0,0 +1,293 @@
package com.example.llama.revamp.ui.screens
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AssistChip
import androidx.compose.material3.Card
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.example.llama.revamp.data.model.ModelInfo
import com.example.llama.revamp.ui.components.ModelCardContentCore
import com.example.llama.revamp.util.FileType
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ModelDetailsScreen(
model: ModelInfo,
) {
Column(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.verticalScroll(rememberScrollState())
) {
// Always show the core and expanded content
ModelCardContentCore(model = model)
Spacer(modifier = Modifier.height(16.dp))
// Dates section
Card(
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Dates",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
val dateFormatter = remember { SimpleDateFormat("MMM d, yyyy", Locale.getDefault()) }
Column {
ListItem(
headlineContent = { Text("Added") },
supportingContent = {
Text(dateFormatter.format(Date(model.dateAdded)))
}
)
model.dateLastUsed?.let { lastUsed ->
ListItem(
headlineContent = { Text("Last used") },
supportingContent = {
Text(dateFormatter.format(Date(lastUsed)))
}
)
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// Metadata sections - only show if data exists
model.metadata.additional?.let { additional ->
if (additional.tags?.isNotEmpty() == true || additional.languages?.isNotEmpty() == true) {
Card(
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Additional Information",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
additional.tags?.takeIf { it.isNotEmpty() }?.let { tags ->
Text(
text = "Tags",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(4.dp))
FlowRow(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
tags.forEach { tag ->
AssistChip(
onClick = { /* No action */ },
label = { Text(tag) }
)
}
}
Spacer(modifier = Modifier.height(12.dp))
}
additional.languages?.takeIf { it.isNotEmpty() }?.let { languages ->
Text(
text = "Languages",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(4.dp))
FlowRow(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
languages.forEach { language ->
AssistChip(
onClick = { /* No action */ },
label = { Text(language) }
)
}
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
}
}
// Technical details section
Card(
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Technical Details",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
// Architecture details
model.metadata.architecture?.let { architecture ->
ListItem(
headlineContent = { Text("Architecture") },
supportingContent = { Text(architecture.architecture ?: "Unknown") }
)
architecture.fileType?.let {
ListItem(
headlineContent = { Text("Quantization") },
supportingContent = { Text(FileType.fromCode(it).label) }
)
}
architecture.vocabSize?.let {
ListItem(
headlineContent = { Text("Vocabulary Size") },
supportingContent = { Text(it.toString()) }
)
}
}
// Context length
model.metadata.dimensions?.contextLength?.let {
ListItem(
headlineContent = { Text("Context Length") },
supportingContent = { Text("$it tokens") }
)
}
// ROPE params if available
model.metadata.rope?.let { rope ->
ListItem(
headlineContent = { Text("RoPE Base") },
supportingContent = {
rope.frequencyBase?.let { Text(it.toString()) }
}
)
ListItem(
headlineContent = { Text("RoPE Scaling") },
supportingContent = {
if (rope.scalingType != null && rope.scalingFactor != null) {
Text("${rope.scalingType}: ${rope.scalingFactor}")
} else {
Text("None")
}
}
)
}
// File size
ListItem(
headlineContent = { Text("File Size") },
supportingContent = { Text(model.formattedFileSize) }
)
// File path
ListItem(
headlineContent = { Text("File Path") },
supportingContent = {
Text(
text = model.path,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
)
}
}
// Add author and attribution section if available
model.metadata.author?.let { author ->
if (author.author != null || author.organization != null || author.license != null) {
Spacer(modifier = Modifier.height(16.dp))
Card(
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Attribution",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
author.author?.let {
ListItem(
headlineContent = { Text("Author") },
supportingContent = { Text(it) }
)
}
author.organization?.let {
ListItem(
headlineContent = { Text("Organization") },
supportingContent = { Text(it) }
)
}
author.license?.let {
ListItem(
headlineContent = { Text("License") },
supportingContent = { Text(it) }
)
}
author.url?.let {
ListItem(
headlineContent = { Text("URL") },
supportingContent = {
Text(
text = it,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.primary
)
}
)
}
}
}
}
}
Spacer(modifier = Modifier.height(24.dp))
}
}

View File

@ -50,7 +50,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.example.llama.revamp.data.model.SystemPrompt
import com.example.llama.revamp.engine.ModelLoadingMetrics
import com.example.llama.revamp.ui.components.ModelCard
import com.example.llama.revamp.ui.components.ModelCardCore
import com.example.llama.revamp.ui.components.ModelUnloadDialogHandler
import com.example.llama.revamp.viewmodel.ModelLoadingViewModel
@ -116,10 +116,9 @@ fun ModelLoadingScreen(
) {
// Selected model card
selectedModel?.let { model ->
ModelCard(
ModelCardCore(
model = model,
onClick = { /* No action on click */ },
modifier = Modifier.padding(bottom = 16.dp),
isSelected = null
)
}

View File

@ -27,8 +27,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.example.llama.revamp.data.model.ModelInfo
import com.example.llama.revamp.ui.components.ModelCard
import com.example.llama.revamp.ui.components.ModelCardActions
import com.example.llama.revamp.ui.components.ModelCardExpandable
import com.example.llama.revamp.viewmodel.ModelSelectionViewModel
@OptIn(ExperimentalMaterial3Api::class)
@ -55,16 +54,17 @@ fun ModelSelectionScreen(
} else {
LazyColumn {
items(models) { model ->
ModelCard(
ModelCardExpandable(
model = model,
onClick = { handleModelSelection(model) },
modifier = Modifier.padding(vertical = 4.dp),
expanded = false,
isSelected = null, // Not in selection mode
actionButton = {
ModelCardActions.PlayButton {
handleModelSelection(model)
}
}
// TODO-han.yin: refactor this
// actionButton = {
// ModelCardActions.PlayButton {
// handleModelSelection(model)
// }
// },
)
Spacer(modifier = Modifier.height(8.dp))
}

View File

@ -33,10 +33,9 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import com.example.llama.revamp.ui.components.ModelCard
import com.example.llama.revamp.ui.components.ModelCardActions
import com.example.llama.revamp.ui.components.ModelCardExpandable
import com.example.llama.revamp.ui.components.ScaffoldEvent
import com.example.llama.revamp.util.formatSize
import com.example.llama.revamp.util.formatFileByteSize
import com.example.llama.revamp.viewmodel.ModelManagementState
import com.example.llama.revamp.viewmodel.ModelManagementState.Deletion
import com.example.llama.revamp.viewmodel.ModelManagementState.Importation
@ -79,7 +78,9 @@ fun ModelsManagementScreen(
.padding(horizontal = 16.dp)
) {
items(items = sortedModels, key = { it.id }) { model ->
ModelCard(
val isSelected = if (isMultiSelectionMode) selectedModels.contains(model.id) else null
ModelCardExpandable(
model = model,
onClick = {
if (isMultiSelectionMode) {
@ -88,17 +89,17 @@ fun ModelsManagementScreen(
viewModel.viewModelDetails(model)
}
},
modifier = Modifier.padding(bottom = 8.dp),
isSelected =
if (isMultiSelectionMode) selectedModels.contains(model.id) else null,
actionButton =
if (!isMultiSelectionMode) {
{
ModelCardActions.InfoButton(
onClick = { viewModel.viewModelDetails(model) }
)
}
} else null
expanded = isSelected == true,
isSelected = isSelected,
// TODO-han.yin: refactor this
// actionButton =
// if (!isMultiSelectionMode) {
// {
// ModelCardActions.InfoButton(
// onClick = { viewModel.viewModelDetails(model) }
// )
// }
// } else null
)
}
}
@ -254,7 +255,7 @@ fun ImportProgressDialog(
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Are you sure you want to import this model (${formatSize(fileSize)})? " +
text = "Are you sure you want to import this model (${formatFileByteSize(fileSize)})? " +
"This may take up to several minutes.",
style = MaterialTheme.typography.bodyMedium,
)

View File

@ -17,18 +17,29 @@ import java.util.Locale
/**
* Convert bytes into human readable sizes
*/
fun formatSize(sizeInBytes: Long) = when {
fun formatFileByteSize(sizeInBytes: Long) = when {
sizeInBytes >= 1_000_000_000 -> {
val sizeInGb = sizeInBytes / 1_000_000_000.0
String.format(Locale.getDefault(), "%.2f GB", sizeInGb)
String.format(Locale.getDefault(), "%.1f GB", sizeInGb)
}
sizeInBytes >= 1_000_000 -> {
val sizeInMb = sizeInBytes / 1_000_000.0
String.format(Locale.getDefault(), "%.2f MB", sizeInMb)
String.format(Locale.getDefault(), "%.0f MB", sizeInMb)
}
else -> {
val sizeInKb = sizeInBytes / 1_000.0
String.format(Locale.getDefault(), "%.2f KB", sizeInKb)
String.format(Locale.getDefault(), "%.0f KB", sizeInKb)
}
}
/**
* Formats numbers to human-readable form (K, M)
*/
fun formatContextLength(contextLength: Int): String {
return when {
contextLength >= 1_000_000 -> String.format(Locale.getDefault(), "%.1fM", contextLength / 1_000_000.0)
contextLength >= 1_000 -> String.format(Locale.getDefault(), "%.0fK", contextLength / 1_000.0)
else -> contextLength.toString()
}
}

View File

@ -67,10 +67,6 @@ data class GgufMetadata(
get() = author?.author
?: baseModels?.firstNotNullOfOrNull { it.author }
/** Context length with unit, e.g. “32768 tokens”. */
val formattedContextLength: String?
get() = dimensions?.contextLength?.let { "$it tokens" }
@Serializable
enum class GgufVersion(val code: Int, val label: String) {
/** First public draft; littleendian only, no alignment key. */

View File

@ -116,7 +116,7 @@ class ModelsManagementViewModel @Inject constructor(
ModelSortOrder.NAME_DESC -> models.sortedByDescending { it.name }
ModelSortOrder.SIZE_ASC -> models.sortedBy { it.sizeInBytes }
ModelSortOrder.SIZE_DESC -> models.sortedByDescending { it.sizeInBytes }
ModelSortOrder.LAST_USED -> models.sortedByDescending { it.lastUsed ?: 0 }
ModelSortOrder.LAST_USED -> models.sortedByDescending { it.dateLastUsed ?: 0 }
}
// Internal state