UI: implement core flow's screens

This commit is contained in:
Han Yin 2025-04-11 14:39:17 -07:00
parent 5ad65919e9
commit 7e5c80cee9
5 changed files with 1320 additions and 0 deletions

View File

@ -0,0 +1,143 @@
package com.example.llama.revamp.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
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.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DrawerState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.llama.revamp.engine.InferenceEngine
import com.example.llama.revamp.navigation.NavigationActions
import com.example.llama.revamp.ui.components.AppScaffold
import com.example.llama.revamp.ui.theme.MonospacedTextStyle
import com.example.llama.revamp.viewmodel.MainViewModel
@Composable
fun BenchmarkScreen(
onBackPressed: () -> Unit,
onRerunPressed: () -> Unit,
onSharePressed: () -> Unit,
drawerState: DrawerState,
navigationActions: NavigationActions,
viewModel: MainViewModel = viewModel()
) {
val engineState by viewModel.engineState.collectAsState()
val benchmarkResults by viewModel.benchmarkResults.collectAsState()
val selectedModel by viewModel.selectedModel.collectAsState()
AppScaffold(
title = "Benchmark Results",
drawerState = drawerState,
navigationActions = navigationActions,
onBackPressed = onBackPressed,
onRerunPressed = onRerunPressed,
onSharePressed = onSharePressed
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
// Model info
selectedModel?.let { model ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = model.name,
style = MaterialTheme.typography.titleLarge
)
Text(
text = "${model.parameters} [${model.quantization}]",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
// Benchmark results or loading indicator
when {
engineState is InferenceEngine.State.Benchmarking -> {
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator()
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Running benchmark...",
style = MaterialTheme.typography.bodyMedium
)
}
}
}
benchmarkResults != null -> {
Card(
modifier = Modifier.fillMaxWidth()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(8.dp)
)
.padding(16.dp)
) {
Text(
text = benchmarkResults ?: "",
style = MonospacedTextStyle,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
else -> {
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "Benchmark results will appear here",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
}

View File

@ -0,0 +1,354 @@
package com.example.llama.revamp.ui.screens
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.filled.Send
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DrawerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.llama.revamp.engine.InferenceEngine
import com.example.llama.revamp.navigation.NavigationActions
import com.example.llama.revamp.ui.components.AppScaffold
import com.example.llama.revamp.viewmodel.MainViewModel
import com.example.llama.revamp.viewmodel.Message
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ConversationScreen(
onBackPressed: () -> Unit,
drawerState: DrawerState,
navigationActions: NavigationActions,
viewModel: MainViewModel = viewModel()
) {
val engineState by viewModel.engineState.collectAsState()
val messages by viewModel.messages.collectAsState()
val systemPrompt by viewModel.systemPrompt.collectAsState()
val selectedModel by viewModel.selectedModel.collectAsState()
val isProcessing = engineState is InferenceEngine.State.ProcessingUserPrompt
val isGenerating = engineState is InferenceEngine.State.Generating
val lazyListState = rememberLazyListState()
var inputText by remember { mutableStateOf("") }
// Auto-scroll to bottom when messages change
LaunchedEffect(messages.size) {
if (messages.isNotEmpty()) {
lazyListState.animateScrollToItem(messages.size - 1)
}
}
AppScaffold(
title = selectedModel?.name ?: "Conversation",
drawerState = drawerState,
navigationActions = navigationActions,
onBackPressed = onBackPressed
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
// System prompt display (collapsible)
systemPrompt?.let { prompt ->
var expanded by remember { mutableStateOf(false) }
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Column(
modifier = Modifier.padding(8.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "System Prompt",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = { expanded = !expanded }) {
Icon(
imageVector = if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
contentDescription = if (expanded) "Collapse" else "Expand"
)
}
}
AnimatedVisibility(visible = expanded) {
Text(
text = prompt,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(8.dp)
)
}
}
}
}
// Messages
LazyColumn(
state = lazyListState,
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
items(messages) { message ->
MessageBubble(message = message)
}
// Show thinking indicator when processing
item {
if (isProcessing) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Assistant avatar
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary),
contentAlignment = Alignment.Center
) {
Text(
text = "AI",
color = MaterialTheme.colorScheme.onPrimary
)
}
Spacer(modifier = Modifier.width(8.dp))
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Thinking...",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
// Input area
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
value = inputText,
onValueChange = { inputText = it },
modifier = Modifier.weight(1f),
placeholder = { Text("Type your message...") },
singleLine = false,
maxLines = 5,
enabled = !isProcessing && !isGenerating
)
Spacer(modifier = Modifier.width(8.dp))
IconButton(
onClick = {
if (inputText.isNotBlank()) {
viewModel.sendMessage(inputText)
inputText = ""
}
},
enabled = inputText.isNotBlank() && !isProcessing && !isGenerating
) {
Icon(
imageVector = Icons.Default.Send,
contentDescription = "Send",
tint = if (inputText.isNotBlank() && !isProcessing && !isGenerating)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
)
}
}
}
}
}
}
@Composable
fun MessageBubble(message: Message) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
verticalAlignment = Alignment.Top
) {
when (message) {
is Message.User -> {
Spacer(modifier = Modifier.weight(1f))
Box(
modifier = Modifier
.width(40.dp)
.height(40.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.secondaryContainer),
contentAlignment = Alignment.Center
) {
Text(
text = "You",
color = MaterialTheme.colorScheme.onSecondaryContainer,
style = MaterialTheme.typography.labelMedium
)
}
Spacer(modifier = Modifier.width(8.dp))
Box(
modifier = Modifier
.weight(5f)
.clip(
RoundedCornerShape(
topStart = 16.dp,
topEnd = 16.dp,
bottomStart = 16.dp,
bottomEnd = 4.dp
)
)
.background(MaterialTheme.colorScheme.primaryContainer)
.padding(12.dp)
) {
Column {
Text(
text = message.content,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Text(
text = message.formattedTime,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f),
modifier = Modifier.align(Alignment.End)
)
}
}
}
is Message.Assistant -> {
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary),
contentAlignment = Alignment.Center
) {
Text(
text = "AI",
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelMedium
)
}
Spacer(modifier = Modifier.width(8.dp))
Box(
modifier = Modifier
.weight(5f)
.clip(
RoundedCornerShape(
topStart = 16.dp,
topEnd = 16.dp,
bottomStart = 4.dp,
bottomEnd = 16.dp
)
)
.background(MaterialTheme.colorScheme.surfaceVariant)
.padding(12.dp)
) {
Column {
Text(
text = message.content,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
if (message.isComplete) {
Text(
text = message.formattedTime,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
modifier = Modifier.align(Alignment.End)
)
} else {
CircularProgressIndicator(
modifier = Modifier
.size(12.dp)
.align(Alignment.End),
strokeWidth = 2.dp
)
}
}
}
Spacer(modifier = Modifier.weight(1f))
}
}
}
}

View File

@ -0,0 +1,393 @@
package com.example.llama.revamp.ui.screens
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Divider
import androidx.compose.material3.DrawerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.example.llama.revamp.data.model.SystemPrompt
import com.example.llama.revamp.engine.InferenceEngine
import com.example.llama.revamp.navigation.NavigationActions
import com.example.llama.revamp.ui.components.AppScaffold
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ModeSelectionScreen(
engineState: InferenceEngine.State,
onBenchmarkSelected: () -> Unit,
onConversationSelected: (String?) -> Unit,
onBackPressed: () -> Unit,
drawerState: DrawerState,
navigationActions: NavigationActions
) {
val staffPickedPrompts = remember { SystemPrompt.getStaffPickedPrompts() }
val recentPrompts = remember { SystemPrompt.getRecentPrompts() }
var selectedMode by remember { mutableStateOf<Mode?>(null) }
var useSystemPrompt by remember { mutableStateOf(false) }
var selectedPrompt by remember { mutableStateOf<SystemPrompt?>(null) }
var tabIndex by remember { mutableStateOf(0) }
// Custom prompt sheet state
val sheetState = rememberModalBottomSheetState()
var showCustomPromptSheet by remember { mutableStateOf(false) }
var customPromptText by remember { mutableStateOf("") }
val coroutineScope = rememberCoroutineScope()
// Check if we're in a loading state
val isLoading = engineState !is InferenceEngine.State.Uninitialized &&
engineState !is InferenceEngine.State.LibraryLoaded &&
engineState !is InferenceEngine.State.AwaitingUserPrompt
AppScaffold(
title = "Select Mode",
drawerState = drawerState,
navigationActions = navigationActions,
onBackPressed = onBackPressed
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
) {
// Mode selection cards
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp)
.selectable(
selected = selectedMode == Mode.BENCHMARK,
onClick = { selectedMode = Mode.BENCHMARK },
enabled = !isLoading,
role = Role.RadioButton
)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selectedMode == Mode.BENCHMARK,
onClick = null // handled by parent selectable
)
Text(
text = "Benchmark",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(start = 8.dp)
)
}
}
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp)
.selectable(
selected = selectedMode == Mode.CONVERSATION,
onClick = {
selectedMode = Mode.CONVERSATION
},
enabled = !isLoading,
role = Role.RadioButton
)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selectedMode == Mode.CONVERSATION,
onClick = null // handled by parent selectable
)
Text(
text = "Conversation",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(start = 8.dp)
)
}
}
// System prompt section (only visible when conversation mode is selected)
AnimatedVisibility(visible = selectedMode == Mode.CONVERSATION) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.selectableGroup(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Use system prompt",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.weight(1f))
TextButton(
onClick = { useSystemPrompt = !useSystemPrompt }
) {
Icon(
imageVector = if (useSystemPrompt) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
contentDescription = if (useSystemPrompt) "Collapse" else "Expand"
)
}
}
AnimatedVisibility(visible = useSystemPrompt) {
Column {
TabRow(selectedTabIndex = tabIndex) {
Tab(
selected = tabIndex == 0,
onClick = { tabIndex = 0 },
text = { Text("Staff Picks") }
)
Tab(
selected = tabIndex == 1,
onClick = { tabIndex = 1 },
text = { Text("Recent") }
)
}
// Tab content
when (tabIndex) {
0 -> PromptList(
prompts = staffPickedPrompts,
selectedPrompt = selectedPrompt,
onPromptSelected = { selectedPrompt = it }
)
1 -> PromptList(
prompts = recentPrompts,
selectedPrompt = selectedPrompt,
onPromptSelected = { selectedPrompt = it }
)
}
// Custom prompt button
OutlinedButton(
onClick = { showCustomPromptSheet = true },
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
) {
Text("Custom prompt...")
}
}
}
}
}
}
Spacer(modifier = Modifier.weight(1f))
// Start button
Button(
onClick = {
when (selectedMode) {
Mode.BENCHMARK -> onBenchmarkSelected()
Mode.CONVERSATION -> {
val systemPrompt = if (useSystemPrompt) {
selectedPrompt?.content ?: customPromptText.takeIf { it.isNotBlank() }
} else null
onConversationSelected(systemPrompt)
}
null -> { /* No mode selected */ }
}
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
enabled = selectedMode != null && !isLoading
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier
.height(24.dp)
.width(24.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = when(engineState) {
is InferenceEngine.State.LoadingModel -> "Loading model..."
is InferenceEngine.State.ProcessingSystemPrompt -> "Processing system prompt..."
is InferenceEngine.State.ModelLoaded -> "Preparing conversation..."
else -> "Processing..."
}
)
} else {
Text("Start")
}
}
}
// Custom prompt bottom sheet
if (showCustomPromptSheet) {
ModalBottomSheet(
onDismissRequest = { showCustomPromptSheet = false },
sheetState = sheetState
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "Custom System Prompt",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = customPromptText,
onValueChange = { customPromptText = it },
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
label = { Text("Enter system prompt") },
placeholder = { Text("You are a helpful assistant...") }
)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
TextButton(
onClick = {
coroutineScope.launch {
sheetState.hide()
showCustomPromptSheet = false
}
}
) {
Text("Cancel")
}
Spacer(modifier = Modifier.weight(1f))
Button(
onClick = {
selectedPrompt = null
coroutineScope.launch {
sheetState.hide()
showCustomPromptSheet = false
}
},
enabled = customPromptText.isNotBlank()
) {
Text("Use Custom Prompt")
}
}
}
}
}
}
}
@Composable
fun PromptList(
prompts: List<SystemPrompt>,
selectedPrompt: SystemPrompt?,
onPromptSelected: (SystemPrompt) -> Unit
) {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.padding(vertical = 8.dp)
) {
items(prompts) { prompt ->
Row(
modifier = Modifier
.fillMaxWidth()
.selectable(
selected = selectedPrompt?.id == prompt.id,
onClick = { onPromptSelected(prompt) },
role = Role.RadioButton
)
.padding(vertical = 8.dp, horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selectedPrompt?.id == prompt.id,
onClick = null // handled by parent selectable
)
Column(
modifier = Modifier.padding(start = 16.dp)
) {
Text(
text = prompt.name,
style = MaterialTheme.typography.titleMedium
)
Text(
text = prompt.content,
style = MaterialTheme.typography.bodySmall,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Divider(modifier = Modifier.padding(horizontal = 16.dp))
}
}
}
enum class Mode {
BENCHMARK,
CONVERSATION
}

View File

@ -0,0 +1,171 @@
package com.example.llama.revamp.ui.screens
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Divider
import androidx.compose.material3.DrawerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.example.llama.revamp.data.model.ModelInfo
import com.example.llama.revamp.navigation.NavigationActions
import com.example.llama.revamp.ui.components.AppScaffold
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ModelSelectionScreen(
onModelSelected: (ModelInfo) -> Unit,
onManageModelsClicked: () -> Unit,
onMenuClicked: () -> Unit,
drawerState: DrawerState,
navigationActions: NavigationActions
) {
// For demo purposes, we'll use sample models
val models = remember { ModelInfo.getSampleModels() }
AppScaffold(
title = "Models",
drawerState = drawerState,
navigationActions = navigationActions,
onMenuPressed = onMenuClicked
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(horizontal = 16.dp)
) {
TextButton(
onClick = onManageModelsClicked,
modifier = Modifier
.align(Alignment.End)
.padding(top = 8.dp, bottom = 8.dp)
) {
Text("Manage Models")
}
Text(
text = "Downloaded Models",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(vertical = 8.dp)
)
LazyColumn {
items(models) { model ->
ModelCard(
model = model,
onClick = { onModelSelected(model) }
)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
}
@Composable
fun ModelCard(
model: ModelInfo,
onClick: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick),
elevation = CardDefaults.cardElevation(
defaultElevation = 2.dp
)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = model.name,
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(4.dp))
Row {
Text(
text = model.parameters,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "${model.quantization}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "${model.formattedSize}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Context Length: ${model.contextLength}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
model.lastUsed?.let { lastUsed ->
val dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.getDefault())
Text(
text = "Last used: ${dateFormat.format(Date(lastUsed))}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.weight(1f))
IconButton(
onClick = onClick
) {
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = "Select model",
tint = MaterialTheme.colorScheme.primary
)
}
}
}
}
}

View File

@ -0,0 +1,259 @@
package com.example.llama.revamp.ui.screens
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.Divider
import androidx.compose.material3.DrawerState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
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.ui.components.AppScaffold
import com.example.llama.revamp.util.ViewModelFactoryProvider
import com.example.llama.revamp.viewmodel.PerformanceViewModel
/**
* Tabs for the settings screen.
*/
enum class SettingsTab {
GENERAL,
MODEL_MANAGEMENT,
ADVANCED
}
@Composable
fun SettingsScreen(
selectedTab: SettingsTab = SettingsTab.GENERAL,
onBackPressed: () -> Unit,
drawerState: DrawerState,
navigationActions: NavigationActions
) {
// State for tab selection
var currentTab by remember { mutableStateOf(selectedTab) }
// 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)
AppScaffold(
title = "Settings",
drawerState = drawerState,
navigationActions = navigationActions,
onBackPressed = onBackPressed
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
// Tabs for different settings categories
TabRow(selectedTabIndex = currentTab.ordinal) {
Tab(
selected = currentTab == SettingsTab.GENERAL,
onClick = { currentTab = SettingsTab.GENERAL },
text = { Text("General") }
)
Tab(
selected = currentTab == SettingsTab.MODEL_MANAGEMENT,
onClick = { currentTab = SettingsTab.MODEL_MANAGEMENT },
text = { Text("Models") }
)
Tab(
selected = currentTab == SettingsTab.ADVANCED,
onClick = { currentTab = SettingsTab.ADVANCED },
text = { Text("Advanced") }
)
}
// Content for the selected tab
when (currentTab) {
SettingsTab.GENERAL -> GeneralSettings(performanceViewModel)
SettingsTab.MODEL_MANAGEMENT -> ModelManagementSettings()
SettingsTab.ADVANCED -> AdvancedSettings()
}
}
}
}
@Composable
fun GeneralSettings(performanceViewModel: PerformanceViewModel) {
val isMonitoringEnabled by performanceViewModel.isMonitoringEnabled.collectAsState()
val useFahrenheit by performanceViewModel.useFahrenheitUnit.collectAsState()
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
SettingsCategory(title = "Performance Monitoring") {
SettingsSwitch(
title = "Enable Monitoring",
description = "Display memory, battery and temperature information",
checked = isMonitoringEnabled,
onCheckedChange = { performanceViewModel.setMonitoringEnabled(it) }
)
SettingsSwitch(
title = "Use Fahrenheit",
description = "Display temperature in Fahrenheit instead of Celsius",
checked = useFahrenheit,
onCheckedChange = { performanceViewModel.setUseFahrenheitUnit(it) }
)
}
SettingsCategory(title = "Theme") {
SettingsSwitch(
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 */ }
)
}
}
}
@Composable
fun ModelManagementSettings() {
// This would be populated with actual functionality in a real implementation
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
Text(
text = "Model Management",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(bottom = 16.dp)
)
Text(
text = "This section will allow you to download, delete, and manage LLM models.",
style = MaterialTheme.typography.bodyMedium
)
}
}
@Composable
fun AdvancedSettings() {
// This would be populated with actual functionality in a real implementation
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
Text(
text = "Advanced Settings",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(bottom = 16.dp)
)
Text(
text = "This section will contain advanced settings such as memory management, cache configuration, and debugging options.",
style = MaterialTheme.typography.bodyMedium
)
}
}
@Composable
fun SettingsCategory(
title: String,
content: @Composable () -> Unit
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 8.dp)
)
Card(
modifier = Modifier.fillMaxWidth()
) {
Column {
content()
}
}
Spacer(modifier = Modifier.height(16.dp))
}
}
@Composable
fun SettingsSwitch(
title: String,
description: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium
)
Text(
text = description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch(
checked = checked,
onCheckedChange = onCheckedChange
)
}
}
Divider()
}