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