UI: refactor ModelCard UI to show GGUF metadata
This commit is contained in:
parent
9056f27a91
commit
b81a0c6e6d
|
|
@ -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() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)}"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue