diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/AppScaffold.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/AppScaffold.kt new file mode 100644 index 0000000000..6334b86f86 --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/AppScaffold.kt @@ -0,0 +1,89 @@ +package com.example.llama.revamp.ui.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DrawerState +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.viewmodel.compose.viewModel +import com.example.llama.revamp.data.preferences.UserPreferences +import com.example.llama.revamp.monitoring.PerformanceMonitor +import com.example.llama.revamp.navigation.NavigationActions +import com.example.llama.revamp.util.ViewModelFactoryProvider +import com.example.llama.revamp.viewmodel.PerformanceViewModel + +/** + * Main scaffold for the app that provides the top bar with system status + * and wraps content in a consistent layout. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppScaffold( + title: String, + navigationActions: NavigationActions, + drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Closed), + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, + onBackPressed: (() -> Unit)? = null, + onMenuPressed: (() -> Unit)? = null, + onRerunPressed: (() -> Unit)? = null, + onSharePressed: (() -> Unit)? = null, + content: @Composable (PaddingValues) -> Unit +) { + // Create dependencies for PerformanceViewModel + val context = LocalContext.current + val performanceMonitor = remember { PerformanceMonitor(context) } + val userPreferences = remember { UserPreferences(context) } + + // Create factory for PerformanceViewModel + val factory = remember { ViewModelFactoryProvider.getPerformanceViewModelFactory(performanceMonitor, userPreferences) } + + // Get ViewModel instance with factory + val performanceViewModel: PerformanceViewModel = viewModel(factory = factory) + + // Collect performance metrics + val memoryUsage by performanceViewModel.memoryUsage.collectAsState() + val batteryInfo by performanceViewModel.batteryInfo.collectAsState() + val temperatureInfo by performanceViewModel.temperatureInfo.collectAsState() + val useFahrenheit by performanceViewModel.useFahrenheitUnit.collectAsState() + + // Formatted memory usage + val memoryText = "${memoryUsage.availableGb}GB available" + + AppNavigationDrawer( + drawerState = drawerState, + navigationActions = navigationActions + ) { + Scaffold( + topBar = { + SystemStatusTopBar( + title = title, + memoryUsage = memoryText, + batteryLevel = batteryInfo.level, + temperature = temperatureInfo.temperature, + useFahrenheit = useFahrenheit, + onBackPressed = onBackPressed, + onMenuPressed = onMenuPressed, + onRerunPressed = onRerunPressed, + onSharePressed = onSharePressed + ) + }, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, + content = content + ) + } +} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/NavigationDrawer.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/NavigationDrawer.kt new file mode 100644 index 0000000000..c6a51670f3 --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/NavigationDrawer.kt @@ -0,0 +1,133 @@ +package com.example.llama.revamp.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +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.material.icons.Icons +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.DrawerState +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.example.llama.revamp.navigation.NavigationActions +import kotlinx.coroutines.launch + +/** + * App navigation drawer that provides access to different sections of the app. + */ +@Composable +fun AppNavigationDrawer( + drawerState: DrawerState, + navigationActions: NavigationActions, + content: @Composable () -> Unit +) { + val coroutineScope = rememberCoroutineScope() + + ModalNavigationDrawer( + drawerState = drawerState, + drawerContent = { + ModalDrawerSheet { + DrawerContent( + onHomeClicked = { + coroutineScope.launch { + drawerState.close() + navigationActions.navigateToModelSelection() + } + }, + onSettingsClicked = { + coroutineScope.launch { + drawerState.close() + navigationActions.navigateToSettings() + } + } + ) + } + }, + content = content + ) +} + +@Composable +private fun DrawerContent( + onHomeClicked: () -> Unit, + onSettingsClicked: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxSize() + .padding(16.dp) + ) { + Text( + text = "Local LLM", + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp) + ) + + HorizontalDivider() + + Spacer(modifier = Modifier.height(16.dp)) + + // Navigation Items + DrawerNavigationItem( + icon = Icons.Default.Home, + label = "Home", + onClick = onHomeClicked + ) + + DrawerNavigationItem( + icon = Icons.Default.Settings, + label = "Settings", + onClick = onSettingsClicked + ) + } +} + +@Composable +private fun DrawerNavigationItem( + icon: ImageVector, + label: String, + onClick: () -> Unit +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(vertical = 8.dp), + color = MaterialTheme.colorScheme.surface + ) { + Column( + modifier = Modifier.padding(8.dp) + ) { + Icon( + imageVector = icon, + contentDescription = label, + tint = MaterialTheme.colorScheme.primary + ) + + Text( + text = label, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 8.dp, top = 4.dp) + ) + } + } +} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/SystemStatusTopBar.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/SystemStatusTopBar.kt new file mode 100644 index 0000000000..5a31684668 --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/SystemStatusTopBar.kt @@ -0,0 +1,141 @@ +package com.example.llama.revamp.ui.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.BatteryAlert +import androidx.compose.material.icons.filled.BatteryFull +import androidx.compose.material.icons.filled.BatteryStd +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +/** + * Top app bar that displays system status information and navigation controls. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SystemStatusTopBar( + title: String, + memoryUsage: String, + batteryLevel: Int, + temperature: Float, + useFahrenheit: Boolean = false, + onBackPressed: (() -> Unit)? = null, + onMenuPressed: (() -> Unit)? = null, + onRerunPressed: (() -> Unit)? = null, + onSharePressed: (() -> Unit)? = null +) { + CenterAlignedTopAppBar( + title = { Text(title) }, + navigationIcon = { + when { + onBackPressed != null -> { + IconButton(onClick = onBackPressed) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = "Back" + ) + } + } + onMenuPressed != null -> { + IconButton(onClick = onMenuPressed) { + Icon( + imageVector = Icons.Default.Menu, + contentDescription = "Menu" + ) + } + } + } + }, + actions = { + // Memory usage + Text( + text = memoryUsage, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(end = 8.dp) + ) + + // Battery and temperature + Row(verticalAlignment = Alignment.CenterVertically) { + // Battery icon and percentage + Icon( + imageVector = when { + batteryLevel > 70 -> Icons.Default.BatteryFull + batteryLevel > 30 -> Icons.Default.BatteryStd + else -> Icons.Default.BatteryAlert + }, + contentDescription = "Battery level", + tint = when { + batteryLevel <= 15 -> MaterialTheme.colorScheme.error + else -> MaterialTheme.colorScheme.onSurface + } + ) + + Text( + text = "$batteryLevel%", + style = MaterialTheme.typography.bodySmall + ) + + Spacer(modifier = Modifier.width(8.dp)) + + // Temperature display + val tempDisplay = if (useFahrenheit) { + "${(temperature * 9/5 + 32).toInt()}°F" + } else { + "${temperature.toInt()}°C" + } + + val tempTint = when { + temperature >= 45 -> MaterialTheme.colorScheme.error + temperature >= 40 -> Color(0xFFFFA500) // Orange warning color + else -> MaterialTheme.colorScheme.onSurface + } + + Text( + text = tempDisplay, + style = MaterialTheme.typography.bodySmall, + color = tempTint + ) + } + + // Optional action buttons + onRerunPressed?.let { + IconButton(onClick = it) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = "Rerun benchmark" + ) + } + } + + onSharePressed?.let { + IconButton(onClick = it) { + Icon( + imageVector = Icons.Default.Share, + contentDescription = "Share results" + ) + } + } + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + titleContentColor = MaterialTheme.colorScheme.onSurface + ) + ) +} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/UnloadModelConfirmationDialog.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/UnloadModelConfirmationDialog.kt new file mode 100644 index 0000000000..2f98fc1ef5 --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/UnloadModelConfirmationDialog.kt @@ -0,0 +1,40 @@ +package com.example.llama.revamp.ui.components + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable + +/** + * 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 +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text("Confirm Exit") + }, + text = { + Text( + "Going back will unload the current model. " + + "This operation cannot be undone. " + + "Any unsaved conversation will be lost." + ) + }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text("Yes, Exit") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + } + ) +}