UI: replace model selection screen's data stubbing; add empty view

This commit is contained in:
Han Yin 2025-04-14 23:36:12 -07:00
parent 6b48f7473f
commit 2614f91226
3 changed files with 111 additions and 46 deletions

View File

@ -51,6 +51,8 @@ interface ModelRepository {
progressTracker: ImportProgressTracker? = null
): ModelInfo
suspend fun updateModelLastUsed(modelId: String)
suspend fun deleteModel(modelId: String)
suspend fun deleteModels(modelIds: List<String>)
@ -230,16 +232,20 @@ class ModelRepositoryImpl @Inject constructor(
input.close()
}
override suspend fun deleteModel(modelId: String) {
override suspend fun updateModelLastUsed(modelId: String) = withContext(Dispatchers.IO) {
modelDao.updateLastUsed(modelId, System.currentTimeMillis())
}
override suspend fun deleteModel(modelId: String) = withContext(Dispatchers.IO) {
modelDao.getModelById(modelId)?.let { model ->
File(model.path).let {
if (it.exists()) { it.delete() }
}
modelDao.deleteModel(model)
}
} ?: Unit
}
override suspend fun deleteModels(modelIds: List<String>) {
override suspend fun deleteModels(modelIds: List<String>) = withContext(Dispatchers.IO) {
modelDao.getModelsByIds(modelIds).let { models ->
models.forEach { model ->
File(model.path).let {

View File

@ -1,24 +1,36 @@
package com.example.llama.revamp.ui.screens
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.PerformanceAppScaffold
import com.example.llama.revamp.viewmodel.MainViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -26,9 +38,9 @@ fun ModelSelectionScreen(
onModelSelected: (ModelInfo) -> Unit,
onManageModelsClicked: () -> Unit,
onMenuClicked: () -> Unit,
viewModel: MainViewModel = hiltViewModel(),
) {
// For demo purposes, we'll use sample models
val models = remember { ModelInfo.getSampleModels() }
val models by viewModel.availableModels.collectAsState()
PerformanceAppScaffold(
title = "Models",
@ -41,15 +53,9 @@ fun ModelSelectionScreen(
.padding(paddingValues)
.padding(horizontal = 16.dp)
) {
TextButton(
onClick = onManageModelsClicked,
modifier = Modifier
.align(Alignment.End)
.padding(top = 8.dp, bottom = 8.dp)
) {
Text("Manage Models")
}
if (models.isEmpty()) {
EmptyModelsView(onManageModelsClicked)
} else {
LazyColumn {
items(models) { model ->
ModelCard(
@ -67,3 +73,50 @@ fun ModelSelectionScreen(
}
}
}
}
@Composable
private fun EmptyModelsView(onManageModelsClicked: () -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Default.FolderOpen,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "No Models Available",
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Add models to get started with local LLM inference",
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(24.dp))
Button(onClick = onManageModelsClicked) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Add Models")
}
}
}

View File

@ -1,17 +1,19 @@
package com.example.llama.revamp.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.example.llama.revamp.data.model.ModelInfo
import com.example.llama.revamp.data.repository.ModelRepository
import com.example.llama.revamp.engine.InferenceEngine
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
@ -23,7 +25,8 @@ import javax.inject.Inject
*/
@HiltViewModel
class MainViewModel @Inject constructor (
private val inferenceEngine: InferenceEngine
private val inferenceEngine: InferenceEngine,
private val modelRepository: ModelRepository,
) : ViewModel() {
// Expose the engine state
@ -32,6 +35,14 @@ class MainViewModel @Inject constructor (
// Benchmark results
val benchmarkResults: StateFlow<String?> = inferenceEngine.benchmarkResults
// Available models for selection
val availableModels: StateFlow<List<ModelInfo>> = modelRepository.getModels()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(SUBSCRIPTION_TIMEOUT_MS),
initialValue = emptyList()
)
// Selected model information
private val _selectedModel = MutableStateFlow<ModelInfo?>(null)
val selectedModel: StateFlow<ModelInfo?> = _selectedModel.asStateFlow()
@ -52,6 +63,10 @@ class MainViewModel @Inject constructor (
*/
fun selectModel(modelInfo: ModelInfo) {
_selectedModel.value = modelInfo
viewModelScope.launch {
modelRepository.updateModelLastUsed(modelInfo.id)
}
}
/**
@ -238,16 +253,14 @@ class MainViewModel @Inject constructor (
}
/**
* Unloads the currently loaded model.
* Unloads the currently loaded model after cleanup chores:
* - Cancel any ongoing token collection
* - Clear messages
*/
suspend fun unloadModel() {
// Cancel any ongoing token collection
tokenCollectionJob?.cancel()
// Clear messages
_messages.value = emptyList()
// Unload model
inferenceEngine.unloadModel()
}
@ -259,17 +272,10 @@ class MainViewModel @Inject constructor (
super.onCleared()
}
/**
* Factory for creating MainViewModel instances.
*/
class Factory(private val inferenceEngine: InferenceEngine) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
return MainViewModel(inferenceEngine) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
companion object {
private val TAG = MainViewModel::class.java.simpleName
private const val SUBSCRIPTION_TIMEOUT_MS = 5000L
}
}