UI: disable triggering drawer via gesture; enable alert dialog on back navigation inside conversation and benchmark

This commit is contained in:
Han Yin 2025-04-11 21:00:02 -07:00
parent a7ae8b7ce0
commit 648b97818e
3 changed files with 229 additions and 134 deletions

View File

@ -2,30 +2,40 @@ package com.example.llama.revamp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.example.llama.revamp.engine.InferenceEngine
import com.example.llama.revamp.navigation.AppDestinations
import com.example.llama.revamp.navigation.NavigationActions
import com.example.llama.revamp.ui.components.AppNavigationDrawer
import com.example.llama.revamp.ui.components.UnloadModelConfirmationDialog
import com.example.llama.revamp.ui.screens.BenchmarkScreen
import com.example.llama.revamp.ui.screens.ConversationScreen
@ -71,6 +81,11 @@ fun AppContent() {
val engineState by viewModel.engineState.collectAsState()
// Track if model is loaded for gesture handling
val isModelLoaded = remember(engineState) {
viewModel.isModelLoaded()
}
val navigationActions = remember(navController) {
NavigationActions(navController)
}
@ -79,6 +94,65 @@ fun AppContent() {
var showUnloadDialog by remember { mutableStateOf(false) }
var pendingNavigation by remember { mutableStateOf<(() -> Unit)?>(null) }
// Get current route
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute by remember {
derivedStateOf { navBackStackEntry?.destination?.route }
}
// Determine if current route requires model unloading
val routeNeedsModelUnloading by remember(currentRoute) {
derivedStateOf {
currentRoute == AppDestinations.CONVERSATION_ROUTE ||
currentRoute == AppDestinations.BENCHMARK_ROUTE ||
currentRoute == AppDestinations.MODE_SELECTION_ROUTE
}
}
// Get local back dispatcher
val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
val lifecycleOwner = LocalLifecycleOwner.current
// Helper function to handle back press with model unloading check
val handleBackWithModelCheck = {
if (viewModel.isModelLoaded()) {
showUnloadDialog = true
pendingNavigation = { navController.popBackStack() }
true // Mark as handled
} else {
navController.popBackStack()
true // Mark as handled
}
}
// Register a system back handler for screens that need unload confirmation
DisposableEffect(lifecycleOwner, backDispatcher, currentRoute, isModelLoaded) {
val callback = object : OnBackPressedCallback(
// Only enable for screens that need model unloading confirmation
routeNeedsModelUnloading && isModelLoaded
) {
override fun handleOnBackPressed() {
handleBackWithModelCheck()
}
}
backDispatcher?.addCallback(lifecycleOwner, callback)
// Remove the callback when the effect leaves the composition
onDispose {
callback.remove()
}
}
// Compose BackHandler for added protection (this handles Compose-based back navigation)
BackHandler(
enabled = routeNeedsModelUnloading &&
isModelLoaded &&
drawerState.currentValue == DrawerValue.Closed
) {
handleBackWithModelCheck()
}
// Observe back button
LaunchedEffect(navController) {
navController.addOnDestinationChangedListener { _, destination, _ ->
@ -94,7 +168,12 @@ fun AppContent() {
}
}
// Main Content
// Main Content with navigation drawer wrapper
AppNavigationDrawer(
drawerState = drawerState,
navigationActions = navigationActions,
modelLoaded = isModelLoaded
) {
NavHost(
navController = navController,
startDestination = AppDestinations.MODEL_SELECTION_ROUTE
@ -129,12 +208,7 @@ fun AppContent() {
},
onBackPressed = {
// Need to unload model before going back
if (viewModel.isModelLoaded()) {
showUnloadDialog = true
pendingNavigation = { navController.popBackStack() }
} else {
navController.popBackStack()
}
handleBackWithModelCheck()
},
drawerState = drawerState,
navigationActions = navigationActions
@ -146,12 +220,7 @@ fun AppContent() {
ConversationScreen(
onBackPressed = {
// Need to unload model before going back
if (viewModel.isModelLoaded()) {
showUnloadDialog = true
pendingNavigation = { navController.popBackStack() }
} else {
navController.popBackStack()
}
handleBackWithModelCheck()
},
drawerState = drawerState,
navigationActions = navigationActions,
@ -164,12 +233,7 @@ fun AppContent() {
BenchmarkScreen(
onBackPressed = {
// Need to unload model before going back
if (viewModel.isModelLoaded()) {
showUnloadDialog = true
pendingNavigation = { navController.popBackStack() }
} else {
navController.popBackStack()
}
handleBackWithModelCheck()
},
onRerunPressed = {
viewModel.rerunBenchmark()
@ -208,6 +272,7 @@ fun AppContent() {
)
}
}
}
// Model unload confirmation dialog
if (showUnloadDialog) {

View File

@ -62,10 +62,6 @@ fun AppScaffold(
// Formatted memory usage
val memoryText = "${memoryUsage.availableGb}GB available"
AppNavigationDrawer(
drawerState = drawerState,
navigationActions = navigationActions
) {
Scaffold(
topBar = {
SystemStatusTopBar(
@ -85,5 +81,4 @@ fun AppScaffold(
},
content = content
)
}
}

View File

@ -1,5 +1,6 @@
package com.example.llama.revamp.ui.components
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@ -7,10 +8,13 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Divider
import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -19,9 +23,14 @@ import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.example.llama.revamp.navigation.NavigationActions
@ -29,19 +38,45 @@ import kotlinx.coroutines.launch
/**
* App navigation drawer that provides access to different sections of the app.
* Gesture opening is disabled when a model is loaded to prevent accidental navigation,
* but gesture dismissal is always enabled.
*/
@Composable
fun AppNavigationDrawer(
drawerState: DrawerState,
navigationActions: NavigationActions,
modelLoaded: Boolean = false,
content: @Composable () -> Unit
) {
val coroutineScope = rememberCoroutineScope()
val configuration = LocalConfiguration.current
// Calculate drawer width (60% of screen width)
val drawerWidth = (configuration.screenWidthDp * 0.6).dp
// Determine if gestures should be enabled
// Always enable when drawer is open (to allow dismissal)
// Only enable when model is not loaded (to prevent accidental opening)
val gesturesEnabled by remember(drawerState.currentValue, modelLoaded) {
derivedStateOf {
drawerState.currentValue == DrawerValue.Open || !modelLoaded
}
}
// Handle back button to close drawer if open
BackHandler(enabled = drawerState.currentValue == DrawerValue.Open) {
coroutineScope.launch {
drawerState.close()
}
}
ModalNavigationDrawer(
drawerState = drawerState,
gesturesEnabled = gesturesEnabled,
drawerContent = {
ModalDrawerSheet {
ModalDrawerSheet(
modifier = Modifier.width(drawerWidth)
) {
DrawerContent(
onHomeClicked = {
coroutineScope.launch {