UI: replace model selection screen's data stubbing; add empty view
This commit is contained in:
parent
6b48f7473f
commit
2614f91226
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue