diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/data/model/ModelInfo.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/data/model/ModelInfo.kt index 0b6b580d63..b4c58ba2a2 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/data/model/ModelInfo.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/data/model/ModelInfo.kt @@ -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? = metadata.additional?.tags?.takeIf { it.isNotEmpty() } + + val languages: List? = metadata.additional?.languages?.takeIf { it.isNotEmpty() } } diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/data/repository/ModelRepository.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/data/repository/ModelRepository.kt index 8bdba78cc4..8cb7216ffa 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/data/repository/ModelRepository.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/data/repository/ModelRepository.kt @@ -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)}" ) } diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/ModelCards.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/ModelCards.kt index 0f9ef5e427..487c0a14fb 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/ModelCards.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/ModelCards.kt @@ -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" - ) - } - } -} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/BenchmarkScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/BenchmarkScreen.kt index 36297f2d89..46e15a39f8 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/BenchmarkScreen.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/BenchmarkScreen.kt @@ -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 ) } diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelDetailsScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelDetailsScreen.kt new file mode 100644 index 0000000000..7bfbe6c871 --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelDetailsScreen.kt @@ -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)) + } +} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelLoadingScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelLoadingScreen.kt index 132a80afd1..666a1fc5ee 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelLoadingScreen.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelLoadingScreen.kt @@ -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 ) } diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelSelectionScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelSelectionScreen.kt index 5ee47da6d7..a7a8fc4cb7 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelSelectionScreen.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelSelectionScreen.kt @@ -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)) } diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelsManagementScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelsManagementScreen.kt index 8ef5b175c7..73265e906f 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelsManagementScreen.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelsManagementScreen.kt @@ -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, ) diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/util/FileUtils.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/util/FileUtils.kt index 5078bfbf77..0beb36080d 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/util/FileUtils.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/util/FileUtils.kt @@ -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() } } diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/util/GgufMetadata.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/util/GgufMetadata.kt index 6a95d0502c..41a6c2d74c 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/util/GgufMetadata.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/util/GgufMetadata.kt @@ -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; little‑endian only, no alignment key. */ diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/viewmodel/ModelsManagementViewModel.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/viewmodel/ModelsManagementViewModel.kt index 9c552a1d9f..e8dcc46527 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/viewmodel/ModelsManagementViewModel.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/viewmodel/ModelsManagementViewModel.kt @@ -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