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 progressTracker: ImportProgressTracker? = null
): ModelInfo ): ModelInfo
suspend fun updateModelLastUsed(modelId: String)
suspend fun deleteModel(modelId: String) suspend fun deleteModel(modelId: String)
suspend fun deleteModels(modelIds: List<String>) suspend fun deleteModels(modelIds: List<String>)
@ -230,16 +232,20 @@ class ModelRepositoryImpl @Inject constructor(
input.close() 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 -> modelDao.getModelById(modelId)?.let { model ->
File(model.path).let { File(model.path).let {
if (it.exists()) { it.delete() } if (it.exists()) { it.delete() }
} }
modelDao.deleteModel(model) 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 -> modelDao.getModelsByIds(modelIds).let { models ->
models.forEach { model -> models.forEach { model ->
File(model.path).let { File(model.path).let {

View File

@ -1,24 +1,36 @@
package com.example.llama.revamp.ui.screens package com.example.llama.revamp.ui.screens
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.LazyColumn
import androidx.compose.foundation.lazy.items 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.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp 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.data.model.ModelInfo
import com.example.llama.revamp.ui.components.ModelCard import com.example.llama.revamp.ui.components.ModelCard
import com.example.llama.revamp.ui.components.ModelCardActions import com.example.llama.revamp.ui.components.ModelCardActions
import com.example.llama.revamp.ui.components.PerformanceAppScaffold import com.example.llama.revamp.ui.components.PerformanceAppScaffold
import com.example.llama.revamp.viewmodel.MainViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -26,9 +38,9 @@ fun ModelSelectionScreen(
onModelSelected: (ModelInfo) -> Unit, onModelSelected: (ModelInfo) -> Unit,
onManageModelsClicked: () -> Unit, onManageModelsClicked: () -> Unit,
onMenuClicked: () -> Unit, onMenuClicked: () -> Unit,
viewModel: MainViewModel = hiltViewModel(),
) { ) {
// For demo purposes, we'll use sample models val models by viewModel.availableModels.collectAsState()
val models = remember { ModelInfo.getSampleModels() }
PerformanceAppScaffold( PerformanceAppScaffold(
title = "Models", title = "Models",
@ -41,15 +53,9 @@ fun ModelSelectionScreen(
.padding(paddingValues) .padding(paddingValues)
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) { ) {
TextButton( if (models.isEmpty()) {
onClick = onManageModelsClicked, EmptyModelsView(onManageModelsClicked)
modifier = Modifier } else {
.align(Alignment.End)
.padding(top = 8.dp, bottom = 8.dp)
) {
Text("Manage Models")
}
LazyColumn { LazyColumn {
items(models) { model -> items(models) { model ->
ModelCard( ModelCard(
@ -66,4 +72,51 @@ 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 package com.example.llama.revamp.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
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.repository.ModelRepository
import com.example.llama.revamp.engine.InferenceEngine import com.example.llama.revamp.engine.InferenceEngine
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
@ -23,7 +25,8 @@ import javax.inject.Inject
*/ */
@HiltViewModel @HiltViewModel
class MainViewModel @Inject constructor ( class MainViewModel @Inject constructor (
private val inferenceEngine: InferenceEngine private val inferenceEngine: InferenceEngine,
private val modelRepository: ModelRepository,
) : ViewModel() { ) : ViewModel() {
// Expose the engine state // Expose the engine state
@ -32,6 +35,14 @@ class MainViewModel @Inject constructor (
// Benchmark results // Benchmark results
val benchmarkResults: StateFlow<String?> = inferenceEngine.benchmarkResults 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 // Selected model information
private val _selectedModel = MutableStateFlow<ModelInfo?>(null) private val _selectedModel = MutableStateFlow<ModelInfo?>(null)
val selectedModel: StateFlow<ModelInfo?> = _selectedModel.asStateFlow() val selectedModel: StateFlow<ModelInfo?> = _selectedModel.asStateFlow()
@ -52,6 +63,10 @@ class MainViewModel @Inject constructor (
*/ */
fun selectModel(modelInfo: ModelInfo) { fun selectModel(modelInfo: ModelInfo) {
_selectedModel.value = 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() { suspend fun unloadModel() {
// Cancel any ongoing token collection
tokenCollectionJob?.cancel() tokenCollectionJob?.cancel()
// Clear messages
_messages.value = emptyList() _messages.value = emptyList()
// Unload model
inferenceEngine.unloadModel() inferenceEngine.unloadModel()
} }
@ -259,17 +272,10 @@ class MainViewModel @Inject constructor (
super.onCleared() super.onCleared()
} }
/** companion object {
* Factory for creating MainViewModel instances. private val TAG = MainViewModel::class.java.simpleName
*/
class Factory(private val inferenceEngine: InferenceEngine) : ViewModelProvider.Factory { private const val SUBSCRIPTION_TIMEOUT_MS = 5000L
@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")
}
} }
} }