UI: implement basic UI components

This commit is contained in:
Han Yin 2025-04-11 14:37:17 -07:00
parent 32608fb225
commit 4dd755e25b
4 changed files with 403 additions and 0 deletions

View File

@ -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
)
}
}

View File

@ -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)
)
}
}
}

View File

@ -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
)
)
}

View File

@ -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")
}
}
)
}