UI: code polish

This commit is contained in:
Han Yin 2025-04-12 20:44:20 -07:00
parent fddf060d92
commit e8b84c6ebf
12 changed files with 113 additions and 70 deletions

View File

@ -278,6 +278,7 @@ fun AppContent() {
}
// Model unload confirmation dialog
// TODO-han.yin: show a progress indicator until the model success unloads?
if (showUnloadDialog) {
UnloadModelConfirmationDialog(
onConfirm = {

View File

@ -46,6 +46,11 @@ sealed class SystemPrompt {
* Creates a list of sample presets.
*/
val STUB_PRESETS = listOf(
Preset(
id = "haiku",
name = "Matsuo Bashō",
content = "You are a wise and contemplative Japanese poet in the spirit of Matsuo Bashō. You speak only through haiku—short poems that capture fleeting moments, natures beauty, or quiet reflections of life. Each of your responses must follow the traditional haiku format: 3 lines; 5 syllables in the first line; 7 syllables in the second line; 5 syllables in the third line. Your words are serene, subtle, and full of meaning. You draw on imagery from nature, emotion, and the impermanence of all things. You do not explain or elaborate. You let the silence between the words speak for itself. Never break character. Never explain your form. Only respond in haiku."
),
Preset(
id = "assistant",
name = "Helpful Assistant",

View File

@ -21,7 +21,7 @@ class PerformanceMonitor(private val context: Context) {
/**
* Provides a flow of memory usage information that updates at the specified interval.
*/
fun monitorMemoryUsage(intervalMs: Long = 5000): Flow<MemoryMetrics> = flow {
fun monitorMemoryUsage(intervalMs: Long = MEMORY_POLLING_INTERVAL): Flow<MemoryMetrics> = flow {
while(true) {
emit(getMemoryInfo())
delay(intervalMs)
@ -31,7 +31,7 @@ class PerformanceMonitor(private val context: Context) {
/**
* Provides a flow of battery information that updates at the specified interval.
*/
fun monitorBattery(intervalMs: Long = 10000): Flow<BatteryMetrics> = flow {
fun monitorBattery(intervalMs: Long = BATTERY_POLLING_INTERVAL): Flow<BatteryMetrics> = flow {
while(true) {
emit(getBatteryInfo())
delay(intervalMs)
@ -41,7 +41,7 @@ class PerformanceMonitor(private val context: Context) {
/**
* Provides a flow of temperature information that updates at the specified interval.
*/
fun monitorTemperature(intervalMs: Long = 10000): Flow<TemperatureMetrics> = flow {
fun monitorTemperature(intervalMs: Long = TEMP_POLLING_INTERVAL): Flow<TemperatureMetrics> = flow {
while(true) {
emit(getTemperatureInfo())
delay(intervalMs)
@ -122,6 +122,12 @@ class PerformanceMonitor(private val context: Context) {
repeat(decimals) { multiplier *= 10 }
return (this * multiplier).roundToInt() / multiplier
}
companion object {
private const val MEMORY_POLLING_INTERVAL = 5000L
private const val BATTERY_POLLING_INTERVAL = 10000L
private const val TEMP_POLLING_INTERVAL = 10000L
}
}
/**

View File

@ -54,14 +54,11 @@ fun AppScaffold(
val temperatureInfo by performanceViewModel.temperatureInfo.collectAsState()
val useFahrenheit by performanceViewModel.useFahrenheitUnit.collectAsState()
// Formatted memory usage
val memoryText = String.format("%.1f / %.1f GB", memoryUsage.availableGb, memoryUsage.totalGb)
Scaffold(
topBar = {
SystemStatusTopBar(
title = title,
memoryUsage = memoryText,
memoryUsage = memoryUsage,
batteryLevel = batteryInfo.level,
temperature = temperatureInfo.temperature,
useFahrenheit = useFahrenheit,

View File

@ -154,7 +154,7 @@ private fun DrawerContent(
DrawerNavigationItem(
icon = Icons.Default.Folder,
label = "Models Management",
label = "Models",
isSelected = currentRoute == com.example.llama.revamp.navigation.AppDestinations.MODELS_MANAGEMENT_ROUTE,
onClick = { onNavigate { navigationActions.navigateToModelsManagement() } }
)

View File

@ -5,25 +5,28 @@ 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.automirrored.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.Memory
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.material.icons.filled.Thermostat
import androidx.compose.material.icons.filled.WarningAmber
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.TopAppBar
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
import com.example.llama.revamp.monitoring.MemoryMetrics
/**
* Top app bar that displays system status information and navigation controls.
@ -32,7 +35,7 @@ import androidx.compose.ui.unit.dp
@Composable
fun SystemStatusTopBar(
title: String,
memoryUsage: String,
memoryUsage: MemoryMetrics,
batteryLevel: Int,
temperature: Float,
useFahrenheit: Boolean = false,
@ -41,14 +44,14 @@ fun SystemStatusTopBar(
onRerunPressed: (() -> Unit)? = null,
onSharePressed: (() -> Unit)? = null
) {
CenterAlignedTopAppBar(
TopAppBar(
title = { Text(title) },
navigationIcon = {
when {
onBackPressed != null -> {
IconButton(onClick = onBackPressed) {
Icon(
imageVector = Icons.Default.ArrowBack,
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back"
)
}
@ -64,15 +67,28 @@ fun SystemStatusTopBar(
}
},
actions = {
Row(verticalAlignment = Alignment.CenterVertically) {
// Memory usage
Icon(
imageVector = Icons.Default.Memory,
contentDescription = "RAM space",
tint = when {
memoryUsage.availableGb < 1 -> MaterialTheme.colorScheme.error
memoryUsage.availableGb < 3 -> MaterialTheme.colorScheme.tertiary
else -> MaterialTheme.colorScheme.onSurface
}
)
Spacer(modifier = Modifier.width(2.dp))
val memoryText = String.format("%.1f / %.1f GB", memoryUsage.availableGb, memoryUsage.totalGb)
Text(
text = memoryUsage,
text = memoryText,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(end = 8.dp)
)
// Battery and temperature
Row(verticalAlignment = Alignment.CenterVertically) {
// Battery icon and percentage
Icon(
imageVector = when {
@ -92,9 +108,21 @@ fun SystemStatusTopBar(
style = MaterialTheme.typography.bodySmall
)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(2.dp))
// Temperature icon and display
Icon(
imageVector = when {
temperature > 40 -> Icons.Default.WarningAmber
else -> Icons.Default.Thermostat
},
contentDescription = "Device temperature",
tint = when {
temperature > 40 -> MaterialTheme.colorScheme.error
else -> MaterialTheme.colorScheme.onSurface
}
)
// Temperature display
val tempDisplay = if (useFahrenheit) {
"${(temperature * 9/5 + 32).toInt()}°F"
} else {
@ -103,7 +131,7 @@ fun SystemStatusTopBar(
val tempTint = when {
temperature >= 45 -> MaterialTheme.colorScheme.error
temperature >= 40 -> Color(0xFFFFA500) // Orange warning color
temperature >= 35 -> MaterialTheme.colorScheme.tertiary
else -> MaterialTheme.colorScheme.onSurface
}

View File

@ -57,11 +57,9 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.repeatOnLifecycle
import com.example.llama.revamp.engine.InferenceEngine
import com.example.llama.revamp.navigation.NavigationActions
import com.example.llama.revamp.ui.components.AppScaffold
@ -123,7 +121,7 @@ fun ConversationScreen(
}
AppScaffold(
title = selectedModel?.name ?: "Conversation",
title = "Chat",
drawerState = drawerState,
navigationActions = navigationActions,
onBackPressed = onBackPressed
@ -134,7 +132,7 @@ fun ConversationScreen(
.padding(paddingValues)
) {
// System prompt display (collapsible)
AnimatedSystemPrompt(systemPrompt)
AnimatedSystemPrompt(selectedModel?.name, systemPrompt)
// Messages list
Box(
@ -165,15 +163,19 @@ fun ConversationScreen(
}
@Composable
fun AnimatedSystemPrompt(systemPrompt: String?) {
fun AnimatedSystemPrompt(modelName: String?, systemPrompt: String?) {
var expanded by remember { mutableStateOf(false) }
// TODO-han.yin: add model name into this card, on top of system prompt!
if (!systemPrompt.isNullOrBlank()) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
onClick = { expanded = !expanded }
onClick = {
expanded = !expanded
}
) {
Column(
modifier = Modifier.padding(16.dp)
@ -369,7 +371,7 @@ fun AssistantMessageBubble(
Spacer(modifier = Modifier.width(4.dp))
Text(
text = if (isThinking) "Thinking..." else "",
text = if (isThinking) "Thinking" else "",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary
)

View File

@ -120,14 +120,17 @@ fun ModelLoadingScreen(
.padding(paddingValues)
.padding(16.dp)
) {
// Mode selection cards
// Benchmark card
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp)
.selectable(
selected = selectedMode == Mode.BENCHMARK,
onClick = { selectedMode = Mode.BENCHMARK },
onClick = {
selectedMode = Mode.BENCHMARK
useSystemPrompt = false
},
enabled = !isLoading,
role = Role.RadioButton
)
@ -142,23 +145,24 @@ fun ModelLoadingScreen(
)
Text(
text = "Benchmark",
style = MaterialTheme.typography.titleMedium,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(start = 8.dp)
)
}
}
// Conversation card with integrated system prompt
// Conversation card with integrated system prompt picker & editor
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp)
.padding(bottom = 4.dp)
// Only use weight if system prompt is active, otherwise wrap content
.then(if (useSystemPrompt) Modifier.weight(1f) else Modifier)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
// Only fill height if system prompt is active
.then(if (useSystemPrompt) Modifier.fillMaxSize() else Modifier)
) {
@ -172,7 +176,7 @@ fun ModelLoadingScreen(
enabled = !isLoading,
role = Role.RadioButton
)
.padding(16.dp),
.padding(top = 16.dp, start = 16.dp, end = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
@ -181,7 +185,7 @@ fun ModelLoadingScreen(
)
Text(
text = "Conversation",
style = MaterialTheme.typography.titleMedium,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(start = 8.dp)
)
}
@ -190,11 +194,11 @@ fun ModelLoadingScreen(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "System prompt",
text = "Use system prompt",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.padding(start = 32.dp) // Align with radio text
@ -222,10 +226,13 @@ fun ModelLoadingScreen(
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxSize() // Fill remaining card space
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxSize()
.padding(start = 48.dp, end = 16.dp)
) {
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
HorizontalDivider(
modifier = Modifier
.padding(top = 4.dp, bottom = 8.dp)
)
// Tab selector using SegmentedButton
SingleChoiceSegmentedButtonRow(
@ -277,7 +284,7 @@ fun ModelLoadingScreen(
)
}
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(8.dp))
// Content based on selected tab
when (selectedTab) {
@ -353,6 +360,8 @@ fun ModelLoadingScreen(
// Flexible spacer when system prompt is not active
if (!useSystemPrompt) {
Spacer(modifier = Modifier.weight(1f))
} else {
Spacer(modifier = Modifier.height(8.dp))
}
// Start button
@ -369,8 +378,10 @@ fun ModelLoadingScreen(
viewModel.savePromptToRecents(prompt)
prompt.content
}
SystemPromptTab.CUSTOM ->
customPromptText.takeIf { it.isNotBlank() }?.also { promptText ->
customPromptText.takeIf { it.isNotBlank() }
?.also { promptText ->
// Save custom prompt to database
viewModel.saveCustomPromptToRecents(promptText)
}
@ -378,7 +389,9 @@ fun ModelLoadingScreen(
} else null
onConversationSelected(systemPrompt)
}
null -> { /* No mode selected */ }
null -> { /* No mode selected */
}
}
},
modifier = Modifier
@ -395,15 +408,16 @@ fun ModelLoadingScreen(
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = when(engineState) {
text = when (engineState) {
is InferenceEngine.State.LoadingModel -> "Loading model..."
is InferenceEngine.State.ProcessingSystemPrompt -> "Processing system prompt..."
is InferenceEngine.State.ModelLoaded -> "Preparing conversation..."
else -> "Processing..."
}
},
style = MaterialTheme.typography.titleMedium
)
} else {
Text("Start")
Text(text = "Start", style = MaterialTheme.typography.titleMedium)
}
}
}
@ -423,7 +437,7 @@ fun PromptList(
modifier = Modifier
.fillMaxWidth()
.fillMaxSize(), // Fill available space
verticalArrangement = Arrangement.spacedBy(8.dp)
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(
items = prompts,
@ -480,7 +494,7 @@ fun PromptList(
if (prompt.id != prompts.last().id) {
HorizontalDivider(
modifier = Modifier.padding(top = 8.dp, start = 40.dp)
modifier = Modifier.padding(top = 8.dp, start = 32.dp)
)
}
}

View File

@ -66,12 +66,6 @@ fun ModelSelectionScreen(
Text("Manage Models")
}
Text(
text = "Downloaded Models",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(vertical = 8.dp)
)
LazyColumn {
items(models) { model ->
ModelCard(

View File

@ -1,6 +1,5 @@
package com.example.llama.revamp.ui.screens
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@ -8,18 +7,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.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DrawerState
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@ -27,7 +21,6 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@ -57,7 +50,7 @@ fun ModelsManagementScreen(
val installedModels = remember { ModelInfo.getSampleModels() }
AppScaffold(
title = "Models Management",
title = "Models",
navigationActions = navigationActions,
onBackPressed = onBackPressed,
onMenuPressed = onMenuClicked

View File

@ -90,7 +90,9 @@ fun SettingsGeneralScreen(
title = "Dark Theme",
description = "Use dark theme throughout the app",
checked = true, // This would be connected to theme state in a real app
onCheckedChange = { /* TODO: Implement theme switching */ }
onCheckedChange = {
/* TODO-hyin: Implement theme switching between Auto, Light and Dark */
}
)
}

View File

@ -66,5 +66,6 @@ val md_theme_dark_outlineVariant = Color(0xFF49454E)
val md_theme_dark_scrim = Color(0xFF000000)
// Additional app-specific colors
val CriticalColor = Color(0xFFFF0000)
val WarningColor = Color(0xFFFFA000)
val SuccessColor = Color(0xFF388E3C)