UI: expose a single facade ModelUnloadDialogHandler; move UnloadModelState into ModelUnloadingViewModel.kt

This commit is contained in:
Han Yin 2025-04-18 15:00:25 -07:00
parent c5a3ac7eb1
commit f61c512223
4 changed files with 198 additions and 136 deletions

View File

@ -0,0 +1,167 @@
package com.example.llama.revamp.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.llama.revamp.viewmodel.UnloadModelState
/**
* Reusable component for handling model unloading dialogs
*
* @param [UnloadModelState]:
* - Hidden: default state without showing any UI
* - Confirming: show dismissible [UnloadModelDialog] and asks for user confirmation to unload current model
* - Unloading: show non-dismissible [UnloadModelDialog] while unloading model
* - Error: show [UnloadModelErrorDialog] to prompt error message to user
*/
@Composable
fun ModelUnloadDialogHandler(
unloadModelState: UnloadModelState,
onUnloadConfirmed: (onNavigateBack: () -> Unit) -> Unit,
onUnloadDismissed: () -> Unit,
onNavigateBack: () -> Unit
) {
when (unloadModelState) {
is UnloadModelState.Confirming -> {
UnloadModelDialog(
onConfirm = {
onUnloadConfirmed(onNavigateBack)
},
onDismiss = onUnloadDismissed,
isUnloading = false
)
}
is UnloadModelState.Unloading -> {
UnloadModelDialog(
onConfirm = {
onUnloadConfirmed(onNavigateBack)
},
onDismiss = onUnloadDismissed,
isUnloading = true
)
}
is UnloadModelState.Error -> {
UnloadModelErrorDialog(
errorMessage = unloadModelState.message,
onConfirm = {
onUnloadDismissed()
onNavigateBack()
},
onDismiss = onUnloadDismissed
)
}
is UnloadModelState.Hidden -> {
// Dialog not shown
}
}
}
@Composable
private fun UnloadModelDialog(
onConfirm: () -> Unit,
onDismiss: () -> Unit,
isUnloading: Boolean = false
) {
AlertDialog(
onDismissRequest = {
// Ignore dismiss requests while unloading the model
if (!isUnloading) onDismiss()
},
title = {
Text("Confirm Exit")
},
text = {
Column {
Text(
"Going back will unload the current model. " +
"Any unsaved conversation will be lost."
)
if (isUnloading) {
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
strokeWidth = 2.dp
)
Spacer(modifier = Modifier.width(8.dp))
Text("Unloading model...")
}
}
}
},
confirmButton = {
TextButton(
onClick = onConfirm,
enabled = !isUnloading
) {
Text("Yes, Exit")
}
},
dismissButton = {
TextButton(
onClick = onDismiss,
enabled = !isUnloading
) {
Text("Cancel")
}
}
)
}
@Composable
private fun UnloadModelErrorDialog(
errorMessage: String,
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
text = "Error Unloading Model",
color = MaterialTheme.colorScheme.error
)
},
text = {
Column {
Text(
text = errorMessage,
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "You may need to restart the app if this problem persists.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
},
confirmButton = {
TextButton(onClick = onConfirm) { Text("Continue") }
},
dismissButton = {
TextButton(onClick = onDismiss) { Text("Stay on Screen") }
}
)
}

View File

@ -1,91 +0,0 @@
package com.example.llama.revamp.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/**
* UI state for [UnloadModelConfirmationDialog]
*/
sealed class UnloadDialogState {
object Hidden : UnloadDialogState()
object Confirming : UnloadDialogState()
object Unloading : UnloadDialogState()
data class Error(val message: String) : UnloadDialogState()
}
/**
* Confirmation dialog shown when the user attempts to navigate away from
* a screen that would require unloading the current model.
*/
@Composable
fun UnloadModelConfirmationDialog(
onConfirm: () -> Unit,
onDismiss: () -> Unit,
isUnloading: Boolean = false
) {
AlertDialog(
onDismissRequest = {
// Ignore dismiss requests while unloading the model
if (!isUnloading) onDismiss()
},
title = {
Text("Confirm Exit")
},
text = {
Column {
Text(
"Going back will unload the current model. " +
"This operation cannot be undone. " +
"Any unsaved conversation will be lost."
)
if (isUnloading) {
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
strokeWidth = 2.dp
)
Spacer(modifier = Modifier.width(8.dp))
Text("Unloading model...")
}
}
}
},
confirmButton = {
TextButton(
onClick = onConfirm,
enabled = !isUnloading
) {
Text("Yes, Exit")
}
},
dismissButton = {
TextButton(
onClick = onDismiss,
enabled = !isUnloading
) {
Text("Cancel")
}
}
)
}

View File

@ -25,8 +25,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.example.llama.revamp.ui.components.ModelCard import com.example.llama.revamp.ui.components.ModelCard
import com.example.llama.revamp.ui.components.UnloadDialogState import com.example.llama.revamp.ui.components.ModelUnloadDialogHandler
import com.example.llama.revamp.ui.components.UnloadModelConfirmationDialog
import com.example.llama.revamp.ui.theme.MonospacedTextStyle import com.example.llama.revamp.ui.theme.MonospacedTextStyle
import com.example.llama.revamp.viewmodel.BenchmarkViewModel import com.example.llama.revamp.viewmodel.BenchmarkViewModel
@ -38,7 +37,7 @@ fun BenchmarkScreen(
val engineState by viewModel.engineState.collectAsState() val engineState by viewModel.engineState.collectAsState()
val benchmarkResults by viewModel.benchmarkResults.collectAsState() val benchmarkResults by viewModel.benchmarkResults.collectAsState()
val selectedModel by viewModel.selectedModel.collectAsState() val selectedModel by viewModel.selectedModel.collectAsState()
val unloadDialogState by viewModel.unloadDialogState.collectAsState() val unloadDialogState by viewModel.unloadModelState.collectAsState()
// Run benchmark when entering the screen // Run benchmark when entering the screen
LaunchedEffect(selectedModel) { LaunchedEffect(selectedModel) {
@ -126,33 +125,10 @@ fun BenchmarkScreen(
} }
// Unload confirmation dialog // Unload confirmation dialog
when (val state = unloadDialogState) { ModelUnloadDialogHandler(
is UnloadDialogState.Confirming -> { unloadModelState = unloadDialogState,
UnloadModelConfirmationDialog( onUnloadConfirmed = { viewModel.onUnloadConfirmed(onNavigateBack) },
onConfirm = { onUnloadDismissed = { viewModel.onUnloadDismissed() },
viewModel.onUnloadConfirmed(onNavigateBack) onNavigateBack = onNavigateBack,
},
onDismiss = {
viewModel.onUnloadDismissed()
},
isUnloading = false
) )
} }
is UnloadDialogState.Unloading -> {
UnloadModelConfirmationDialog(
onConfirm = {
viewModel.onUnloadConfirmed(onNavigateBack)
},
onDismiss = {
viewModel.onUnloadDismissed()
},
isUnloading = true
)
}
is UnloadDialogState.Error -> {
// TODO-han.yin: TBD
android.util.Log.e("JOJO", "Unload error: ${state.message}")
}
else -> { /* Dialog not shown */ }
}
}

View File

@ -7,12 +7,22 @@ import android.llama.cpp.isUninterruptible
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.example.llama.revamp.engine.InferenceService import com.example.llama.revamp.engine.InferenceService
import com.example.llama.revamp.ui.components.UnloadDialogState
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
/**
* UI states to be consumed by [ModelUnloadDialogHandler], etc.
*/
sealed class UnloadModelState {
object Hidden : UnloadModelState()
object Confirming : UnloadModelState()
object Unloading : UnloadModelState()
data class Error(val message: String) : UnloadModelState()
}
/** /**
* Base ViewModel class for screens that requires additional model unloading functionality * Base ViewModel class for screens that requires additional model unloading functionality
*/ */
@ -39,11 +49,13 @@ abstract class ModelUnloadingViewModel(
/** /**
* [UnloadModelConfirmationDialog]'s UI states * [UnloadModelConfirmationDialog]'s UI states
*/ */
private val _unloadDialogState = MutableStateFlow<UnloadDialogState>(UnloadDialogState.Hidden) private val _unloadModelState = MutableStateFlow<UnloadModelState>(UnloadModelState.Hidden)
val unloadDialogState: StateFlow<UnloadDialogState> = _unloadDialogState.asStateFlow() val unloadModelState: StateFlow<UnloadModelState> = _unloadModelState.asStateFlow()
/** /**
* Handle back press from both back button and top bar * Handle back press from both back button and top bar
*
* Subclass can override this default implementation
*/ */
open fun onBackPressed(onNavigateBack: () -> Unit) = open fun onBackPressed(onNavigateBack: () -> Unit) =
if (isUninterruptible) { if (isUninterruptible) {
@ -53,7 +65,7 @@ abstract class ModelUnloadingViewModel(
onNavigateBack.invoke() onNavigateBack.invoke()
} else { } else {
// If model is loaded, show confirmation dialog // If model is loaded, show confirmation dialog
_unloadDialogState.value = UnloadDialogState.Confirming _unloadModelState.value = UnloadModelState.Confirming
} }
/** /**
@ -62,7 +74,7 @@ abstract class ModelUnloadingViewModel(
fun onUnloadConfirmed(onNavigateBack: () -> Unit) = fun onUnloadConfirmed(onNavigateBack: () -> Unit) =
viewModelScope.launch { viewModelScope.launch {
// Set unloading state to show progress // Set unloading state to show progress
_unloadDialogState.value = UnloadDialogState.Unloading _unloadModelState.value = UnloadModelState.Unloading
try { try {
// Perform screen-specific cleanup // Perform screen-specific cleanup
@ -72,11 +84,11 @@ abstract class ModelUnloadingViewModel(
inferenceService.unloadModel() inferenceService.unloadModel()
// Reset state and navigate back // Reset state and navigate back
_unloadDialogState.value = UnloadDialogState.Hidden _unloadModelState.value = UnloadModelState.Hidden
onNavigateBack() onNavigateBack()
} catch (e: Exception) { } catch (e: Exception) {
// Handle error // Handle error
_unloadDialogState.value = UnloadDialogState.Error( _unloadModelState.value = UnloadModelState.Error(
e.message ?: "Unknown error while unloading the model" e.message ?: "Unknown error while unloading the model"
) )
} }
@ -86,11 +98,11 @@ abstract class ModelUnloadingViewModel(
* Handle dismissal of unload dialog * Handle dismissal of unload dialog
*/ */
fun onUnloadDismissed() = fun onUnloadDismissed() =
when (_unloadDialogState.value) { when (_unloadModelState.value) {
is UnloadDialogState.Unloading -> { is UnloadModelState.Unloading -> {
// Ignore dismissing requests during unloading // Ignore dismissing requests during unloading
} }
else -> _unloadDialogState.value = UnloadDialogState.Hidden else -> _unloadModelState.value = UnloadModelState.Hidden
} }
/** /**
@ -98,7 +110,5 @@ abstract class ModelUnloadingViewModel(
* *
* To be implemented by subclasses if needed * To be implemented by subclasses if needed
*/ */
protected open suspend fun performCleanup() { protected open suspend fun performCleanup() {}
// Default empty implementation
}
} }