From 310771f6aaeee4c17118f82003b69c0ea74c0388 Mon Sep 17 00:00:00 2001 From: Han Yin Date: Mon, 7 Jul 2025 20:26:32 -0700 Subject: [PATCH] UI: scaffold Models Management screen and view model --- .../java/com/example/llama/MainActivity.kt | 2 +- .../ui/screens/ModelsManagementScreen.kt | 2 + .../viewmodel/ModelsManagementViewModel.kt | 76 +++++++++++++++---- 3 files changed, 64 insertions(+), 16 deletions(-) diff --git a/examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt b/examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt index be6da82e35..e833439cd0 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt @@ -390,7 +390,7 @@ fun AppContent( modelsManagementViewModel.toggleImportMenu(false) }, importFromHuggingFace = { - modelsManagementViewModel.importFromHuggingFace() + modelsManagementViewModel.queryModelsFromHuggingFace() modelsManagementViewModel.toggleImportMenu(false) } ) diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsManagementScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsManagementScreen.kt index 9c7b5a8fad..935ff506f8 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsManagementScreen.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsManagementScreen.kt @@ -219,6 +219,8 @@ fun ModelsManagementScreen( } is ModelManagementState.Idle -> { /* Idle state, nothing to show */ } + + else -> TODO() } } diff --git a/examples/llama.android/app/src/main/java/com/example/llama/viewmodel/ModelsManagementViewModel.kt b/examples/llama.android/app/src/main/java/com/example/llama/viewmodel/ModelsManagementViewModel.kt index 8153266b34..6f8f7cc562 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/viewmodel/ModelsManagementViewModel.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/viewmodel/ModelsManagementViewModel.kt @@ -10,12 +10,15 @@ import com.example.llama.data.model.ModelInfo import com.example.llama.data.model.ModelSortOrder import com.example.llama.data.model.filterBy import com.example.llama.data.model.sortByOrder +import com.example.llama.data.remote.HuggingFaceDownloadInfo import com.example.llama.data.remote.HuggingFaceModel import com.example.llama.data.repository.InsufficientStorageException import com.example.llama.data.repository.ModelRepository +import com.example.llama.util.formatFileByteSize import com.example.llama.util.getFileNameFromUri import com.example.llama.util.getFileSizeFromUri import com.example.llama.viewmodel.ModelManagementState.Deletion +import com.example.llama.viewmodel.ModelManagementState.Download import com.example.llama.viewmodel.ModelManagementState.Importation import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext @@ -130,6 +133,10 @@ class ModelsManagementViewModel @Inject constructor( _showImportModelMenu.value = show } + // UI state: HuggingFace models query result + private val _huggingFaceModels = MutableSharedFlow>() + val huggingFaceModels: SharedFlow> = _huggingFaceModels + init { viewModelScope.launch { combine( @@ -152,13 +159,8 @@ class ModelsManagementViewModel @Inject constructor( _managementState.value = ModelManagementState.Idle } - fun viewModelDetails(model: ModelInfo) { - // TODO-han.yin: print logs for now. Would navigate to model details screen or show dialog - Log.i(TAG, "${model.name} metadata: \n" + model.metadata.toString()) - } - /** - * First show confirmation instead of starting import immediately + * First show confirmation instead of starting import local file immediately */ fun localModelFileSelected(uri: Uri) = viewModelScope.launch { try { @@ -216,18 +218,56 @@ class ModelsManagementViewModel @Inject constructor( } } - // TODO-han.yin: UI TO BE IMPLEMENTED - private val _huggingFaceModels = MutableSharedFlow>() - val huggingFaceModels: SharedFlow> = _huggingFaceModels - - fun importFromHuggingFace() = viewModelScope.launch { + /** + * Query models on HuggingFace available for download even without signing in + */ + fun queryModelsFromHuggingFace() = viewModelScope.launch { modelRepository.searchHuggingFaceModels().let { models -> _huggingFaceModels.emit(models) - Log.d(TAG, "Fetched ${models.size} models from HuggingFace:") - models.forEachIndexed { index, model -> - Log.d(TAG, "#$index: $model") - } + + // TODO-han.yin: remove these logs +// models.forEachIndexed { index, model -> +// Log.d(TAG, "#$index: $model") +// } + } + } + + /** + * First show confirmation instead of dispatch download immediately + */ + fun downloadHuggingFaceModelSelected(model: HuggingFaceModel) { + _managementState.value = Download.Confirming(model) + } + + /** + * Dispatch download request to [DownloadManager] and update UI + */ + fun downloadHuggingFaceModel(model: HuggingFaceModel) = viewModelScope.launch { + try { + require(!model.gated) { "Model is gated!" } + require(!model.private) { "Model is private!" } + val downloadInfo = model.toDownloadInfo() + requireNotNull(downloadInfo) { "Download URL is missing!" } + + val actualSize = modelRepository.getHuggingFaceModelFileSize(downloadInfo) + requireNotNull(actualSize) { "Unknown model file size!" } + Log.d(TAG, "Model file size: ${formatFileByteSize(actualSize)}") + + modelRepository.importHuggingFaceModel(downloadInfo, actualSize) + .onSuccess { + _managementState.value = Download.Dispatched(downloadInfo) + } + .onFailure { throw it } + + } catch (e: InsufficientStorageException) { + _managementState.value = Download.Error( + message = e.message ?: "Insufficient storage space to download ${model.modelId}", + ) + } catch (e: Exception) { + _managementState.value = Download.Error( + message = e.message ?: "Unknown error downloading ${model.modelId}", + ) } } @@ -283,6 +323,12 @@ sealed class ModelManagementState { data class Error(val message: String) : Importation() } + sealed class Download: ModelManagementState() { + data class Confirming(val model: HuggingFaceModel) : Download() + data class Dispatched(val downloadInfo: HuggingFaceDownloadInfo) : Download() + data class Error(val message: String) : Download() + } + sealed class Deletion : ModelManagementState() { data class Confirming(val models: Map): ModelManagementState() data class Deleting(val progress: Float = 0f, val models: Map) : ModelManagementState()