UI: implement basic UI components
This commit is contained in:
parent
32608fb225
commit
4dd755e25b
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue