data: move Model related actions (query, filter, sort) into ModelInfo file

This commit is contained in:
Han Yin 2025-04-21 14:08:26 -07:00
parent ef3791207b
commit 05c620cc52
4 changed files with 206 additions and 67 deletions

View File

@ -8,6 +8,17 @@ import com.example.llama.revamp.util.formatFileByteSize
/** /**
* Data class containing information about an LLM model. * Data class containing information about an LLM model.
*
* This class represents a language model with its associated metadata, including
* file information, architecture details, and usage statistics.
*
* @property id Unique identifier for the model
* @property name Display name of the model
* @property path File path to the model on device storage
* @property sizeInBytes Size of the model file in bytes
* @property metadata Structured metadata extracted from the GGUF file
* @property dateAdded Timestamp when the model was added to the app
* @property dateLastUsed Timestamp when the model was last used, or null if never used
*/ */
data class ModelInfo( data class ModelInfo(
val id: String, val id: String,
@ -18,25 +29,203 @@ data class ModelInfo(
val dateAdded: Long, val dateAdded: Long,
val dateLastUsed: Long? = null, val dateLastUsed: Long? = null,
) { ) {
/**
* Full model name including version and parameter size if available, otherwise fallback to file name.
*/
val formattedFullName: String val formattedFullName: String
get() = metadata.fullModelName ?: name get() = metadata.fullModelName ?: name
/**
* Human-readable file size with appropriate unit (KB, MB, GB).
*/
val formattedFileSize: String val formattedFileSize: String
get() = formatFileByteSize(sizeInBytes) get() = formatFileByteSize(sizeInBytes)
/**
* Architecture name of the model (e.g., "llama", "mistral"), or "-" if unavailable.
*/
val formattedArchitecture: String val formattedArchitecture: String
get() = metadata.architecture?.architecture ?: "-" get() = metadata.architecture?.architecture ?: "-"
/**
* Model parameter size with suffix (e.g., "7B", "13B"), or "-" if unavailable.
*/
val formattedParamSize: String val formattedParamSize: String
get() = metadata.basic.sizeLabel ?: "-" get() = metadata.basic.sizeLabel ?: "-"
/**
* Human-readable context length (e.g., "4K", "8K tokens"), or "-" if unavailable.
*/
val formattedContextLength: String val formattedContextLength: String
get() = metadata.dimensions?.contextLength?.let { formatContextLength(it) } ?: "-" get() = metadata.dimensions?.contextLength?.let { formatContextLength(it) } ?: "-"
/**
* Quantization format of the model (e.g., "Q4_0", "Q5_K_M"), or "-" if unavailable.
*/
val formattedQuantization: String val formattedQuantization: String
get() = metadata.architecture?.fileType?.let { FileType.fromCode(it).label } ?: "-" get() = metadata.architecture?.fileType?.let { FileType.fromCode(it).label } ?: "-"
/**
* Tags associated with the model, or null if none are defined.
*/
val tags: List<String>? = metadata.additional?.tags?.takeIf { it.isNotEmpty() } val tags: List<String>? = metadata.additional?.tags?.takeIf { it.isNotEmpty() }
/**
* Languages supported by the model, or null if none are defined.
*/
val languages: List<String>? = metadata.additional?.languages?.takeIf { it.isNotEmpty() } val languages: List<String>? = metadata.additional?.languages?.takeIf { it.isNotEmpty() }
} }
/**
* Filters models by search query.
*
* Searches through model names, tags, languages, and architecture.
* Returns the original list if the query is blank.
*
* @param query The search term to filter by
* @return List of models matching the search criteria
*/
fun List<ModelInfo>.queryBy(query: String): List<ModelInfo> {
if (query.isBlank()) return this
return filter { model ->
model.name.contains(query, ignoreCase = true) ||
model.metadata.fullModelName?.contains(query, ignoreCase = true) == true ||
model.metadata.additional?.tags?.any { it.contains(query, ignoreCase = true) } == true ||
model.metadata.additional?.languages?.any { it.contains(query, ignoreCase = true) } == true ||
model.metadata.architecture?.architecture?.contains(query, ignoreCase = true) == true
}
}
/**
* Sorting options for model lists.
*/
enum class ModelSortOrder {
NAME_ASC,
NAME_DESC,
SIZE_ASC,
SIZE_DESC,
LAST_USED
}
/**
* Sorts models according to the specified order.
*
* @param order The sort order to apply
* @return Sorted list of models
*/
fun List<ModelInfo>.sortByOrder(order: ModelSortOrder): List<ModelInfo> {
return when (order) {
ModelSortOrder.NAME_ASC -> sortedBy { it.name }
ModelSortOrder.NAME_DESC -> sortedByDescending { it.name }
ModelSortOrder.SIZE_ASC -> sortedBy { it.sizeInBytes }
ModelSortOrder.SIZE_DESC -> sortedByDescending { it.sizeInBytes }
ModelSortOrder.LAST_USED -> sortedWith(
compareByDescending<ModelInfo> { it.dateLastUsed }
.thenBy { it.name }
)
}
}
/**
* Filters for categorizing and filtering models.
*
* @property displayName Human-readable name shown in the UI
* @property predicate Function that determines if a model matches this filter
*/
enum class ModelFilter(val displayName: String, val predicate: (ModelInfo) -> Boolean) {
// Parameter size filters
TINY_PARAMS("Tiny (<1B parameters)", {
it.metadata.basic.sizeLabel?.let { size ->
size.contains("M") || (size.contains("B") && size.replace("B", "").toFloatOrNull()?.let { n -> n < 1f } == true)
} == true
}),
SMALL_PARAMS("Small (1-3B parameters)", {
it.metadata.basic.sizeLabel?.let { size ->
size.contains("B") && size.replace("B", "").toFloatOrNull()?.let { n -> n >= 1f && n <= 3f } == true
} == true
}),
MEDIUM_PARAMS("Medium (4-7B parameters)", {
it.metadata.basic.sizeLabel?.let { size ->
size.contains("B") && size.replace("B", "").toFloatOrNull()?.let { n -> n >= 4f && n <= 7f } == true
} == true
}),
LARGE_PARAMS("Large (8-13B parameters)", {
it.metadata.basic.sizeLabel?.let { size ->
size.contains("B") && size.replace("B", "").toFloatOrNull()?.let { n -> n >= 8f && n <= 13f } == true
} == true
}),
XLARGE_PARAMS("X-Large (>13B parameters)", {
it.metadata.basic.sizeLabel?.let { size ->
size.contains("B") && size.replace("B", "").toFloatOrNull()?.let { n -> n > 13f } == true
} == true
}),
// Context length filters
TINY_CONTEXT("Tiny context (<4K)", {
it.metadata.dimensions?.contextLength?.let { it < 4096 } == true
}),
SHORT_CONTEXT("Short context (4-8K)", {
it.metadata.dimensions?.contextLength?.let { it >= 4096 && it <= 8192 } == true
}),
MEDIUM_CONTEXT("Medium context (8-32K)", {
it.metadata.dimensions?.contextLength?.let { it > 8192 && it <= 32768 } == true
}),
LONG_CONTEXT("Long context (32-128K)", {
it.metadata.dimensions?.contextLength?.let { it > 32768 && it <= 131072 } == true
}),
XLARGE_CONTEXT("Extended context (>128K)", {
it.metadata.dimensions?.contextLength?.let { it > 131072 } == true
}),
// Quantization filters
INT2_QUANT("2-bit quantization", {
it.formattedQuantization.let { it.contains("Q2") || it.contains("IQ2") }
}),
INT3_QUANT("3-bit quantization", {
it.formattedQuantization.let { it.contains("Q3") || it.contains("IQ3") }
}),
INT4_QUANT("4-bit quantization", {
it.formattedQuantization.let { it.contains("Q4") || it.contains("IQ4") }
}),
// Special features
MULTILINGUAL("Multilingual", {
it.languages?.let { languages ->
languages.size > 1 || languages.any { it.contains("multi", ignoreCase = true) }
} == true
}),
HAS_TAGS("Has tags", {
!it.tags.isNullOrEmpty()
});
companion object {
// Group filters by category for UI
private val PARAMETER_FILTERS = listOf(TINY_PARAMS, SMALL_PARAMS, MEDIUM_PARAMS, LARGE_PARAMS)
private val CONTEXT_FILTERS = listOf(SHORT_CONTEXT, MEDIUM_CONTEXT, LONG_CONTEXT)
private val QUANTIZATION_FILTERS = listOf(INT2_QUANT, INT3_QUANT, INT4_QUANT)
private val FEATURE_FILTERS = listOf(MULTILINGUAL, HAS_TAGS)
// All filters flattened
val ALL_FILTERS = PARAMETER_FILTERS + CONTEXT_FILTERS + QUANTIZATION_FILTERS + FEATURE_FILTERS
}
}
/**
* Filters models based on a set of active filters.
*
* @param filters Map of filters to their enabled state
* @return List of models that match all active filters
*/
fun List<ModelInfo>.filterBy(filters: Map<ModelFilter, Boolean>): List<ModelInfo> {
val activeFilters = filters.filterValues { it }
return if (activeFilters.isEmpty()) {
this
} else {
filter { model ->
activeFilters.keys.all { filter ->
filter.predicate(model)
}
}
}
}

View File

@ -36,8 +36,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.example.llama.R import com.example.llama.R
import com.example.llama.revamp.data.model.ModelFilter
import com.example.llama.revamp.data.model.ModelInfo import com.example.llama.revamp.data.model.ModelInfo
import com.example.llama.revamp.viewmodel.ModelSortOrder import com.example.llama.revamp.data.model.ModelSortOrder
/** /**
* [BottomAppBar] configurations * [BottomAppBar] configurations
@ -68,8 +69,8 @@ sealed class BottomBarConfig {
data class FilteringConfig( data class FilteringConfig(
val isActive: Boolean, val isActive: Boolean,
val filters: Map<String, Boolean>, // Filter name -> enabled val filters: Map<ModelFilter, Boolean>,
val onToggleFilter: (String, Boolean) -> Unit, val onToggleFilter: (ModelFilter, Boolean) -> Unit,
val onClearFilters: () -> Unit, val onClearFilters: () -> Unit,
val isMenuVisible: Boolean, val isMenuVisible: Boolean,
val toggleMenu: (Boolean) -> Unit val toggleMenu: (Boolean) -> Unit
@ -210,7 +211,7 @@ fun ModelSelectionBottomBar(
filtering.filters.forEach { (filter, isEnabled) -> filtering.filters.forEach { (filter, isEnabled) ->
DropdownMenuItem( DropdownMenuItem(
text = { Text(filter) }, text = { Text(filter.displayName) },
leadingIcon = { leadingIcon = {
Checkbox( Checkbox(
checked = isEnabled, checked = isEnabled,

View File

@ -5,7 +5,12 @@ import androidx.compose.foundation.text.input.clearText
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.example.llama.revamp.data.model.ModelFilter
import com.example.llama.revamp.data.model.ModelInfo import com.example.llama.revamp.data.model.ModelInfo
import com.example.llama.revamp.data.model.ModelSortOrder
import com.example.llama.revamp.data.model.filterBy
import com.example.llama.revamp.data.model.queryBy
import com.example.llama.revamp.data.model.sortByOrder
import com.example.llama.revamp.data.repository.ModelRepository import com.example.llama.revamp.data.repository.ModelRepository
import com.example.llama.revamp.engine.InferenceService import com.example.llama.revamp.engine.InferenceService
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -59,17 +64,12 @@ class ModelSelectionViewModel @Inject constructor(
} }
// UI state: filters // UI state: filters
// TODO-han.yin: Refactor this into Enums! private val _activeFilters = MutableStateFlow<Map<ModelFilter, Boolean>>(
private val _activeFilters = MutableStateFlow<Map<String, Boolean>>(mapOf( ModelFilter.ALL_FILTERS.associateWith { false }
"Has context length" to false, )
"Support system prompt" to false, val activeFilters: StateFlow<Map<ModelFilter, Boolean>> = _activeFilters.asStateFlow()
"7B models" to false,
"13B models" to false,
"70B models" to false
))
val activeFilters: StateFlow<Map<String, Boolean>> = _activeFilters.asStateFlow()
fun toggleFilter(filter: String, enabled: Boolean) { fun toggleFilter(filter: ModelFilter, enabled: Boolean) {
_activeFilters.update { current -> _activeFilters.update { current ->
current.toMutableMap().apply { current.toMutableMap().apply {
this[filter] = enabled this[filter] = enabled
@ -110,7 +110,7 @@ class ModelSelectionViewModel @Inject constructor(
_sortOrder, _sortOrder,
) { models, filters, sortOrder -> ) { models, filters, sortOrder ->
models.filterBy(filters).sortByOrder(sortOrder) models.filterBy(filters).sortByOrder(sortOrder)
}.collect { }.collectLatest {
_filteredModels.value = it _filteredModels.value = it
} }
} }
@ -131,50 +131,6 @@ class ModelSelectionViewModel @Inject constructor(
} }
} }
private fun List<ModelInfo>.queryBy(query: String): List<ModelInfo> {
if (query.isBlank()) return this
return filter { model ->
model.name.contains(query, ignoreCase = true) ||
model.metadata.fullModelName?.contains(query, ignoreCase = true) == true ||
model.metadata.additional?.tags?.any { it.contains(query, ignoreCase = true) } == true ||
model.metadata.additional?.languages?.any { it.contains(query, ignoreCase = true) } == true ||
model.metadata.architecture?.architecture?.contains(query, ignoreCase = true) == true
}
}
// TODO-han.yin: Refactor this into Enums!
private fun List<ModelInfo>.filterBy(filters: Map<String, Boolean>): List<ModelInfo> {
val activeFilters = filters.filterValues { it }
if (activeFilters.isEmpty()) return this
return filter { model ->
activeFilters.all { (filter, _) ->
when (filter) {
"Has context length" -> model.metadata.dimensions?.contextLength != null
"Support system prompt" -> true
"7B models" -> model.metadata.basic.sizeLabel?.contains("7B") == true
"13B models" -> model.metadata.basic.sizeLabel?.contains("13B") == true
"70B models" -> model.metadata.basic.sizeLabel?.contains("70B") == true
else -> true
}
}
}
}
private fun List<ModelInfo>.sortByOrder(order: ModelSortOrder): List<ModelInfo> {
return when (order) {
ModelSortOrder.NAME_ASC -> sortedBy { it.name }
ModelSortOrder.NAME_DESC -> sortedByDescending { it.name }
ModelSortOrder.SIZE_ASC -> sortedBy { it.sizeInBytes }
ModelSortOrder.SIZE_DESC -> sortedByDescending { it.sizeInBytes }
ModelSortOrder.LAST_USED -> sortedWith(
compareByDescending<ModelInfo> { it.dateLastUsed }
.thenBy { it.name }
)
}
}
/** /**
* Pre-select a model * Pre-select a model
*/ */

View File

@ -6,6 +6,7 @@ import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.example.llama.revamp.data.model.ModelInfo import com.example.llama.revamp.data.model.ModelInfo
import com.example.llama.revamp.data.model.ModelSortOrder
import com.example.llama.revamp.data.repository.InsufficientStorageException import com.example.llama.revamp.data.repository.InsufficientStorageException
import com.example.llama.revamp.data.repository.ModelRepository import com.example.llama.revamp.data.repository.ModelRepository
import com.example.llama.revamp.util.getFileNameFromUri import com.example.llama.revamp.util.getFileNameFromUri
@ -236,14 +237,6 @@ class ModelsManagementViewModel @Inject constructor(
} }
} }
enum class ModelSortOrder {
NAME_ASC,
NAME_DESC,
SIZE_ASC,
SIZE_DESC,
LAST_USED
}
sealed class ModelManagementState { sealed class ModelManagementState {
object Idle : ModelManagementState() object Idle : ModelManagementState()