data: move Model related actions (query, filter, sort) into ModelInfo file
This commit is contained in:
parent
ef3791207b
commit
05c620cc52
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue