diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/BenchmarkScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/BenchmarkScreen.kt new file mode 100644 index 0000000000..0a25840369 --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/BenchmarkScreen.kt @@ -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 + ) + } + } + } + } + } +} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ConversationScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ConversationScreen.kt new file mode 100644 index 0000000000..59c88fdc4e --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ConversationScreen.kt @@ -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)) + } + } + } +} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModeSelectionScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModeSelectionScreen.kt new file mode 100644 index 0000000000..a4071e8c5f --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModeSelectionScreen.kt @@ -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(null) } + var useSystemPrompt by remember { mutableStateOf(false) } + var selectedPrompt by remember { mutableStateOf(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, + 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 +} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelSelectionScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelSelectionScreen.kt new file mode 100644 index 0000000000..3dc4b386f2 --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelSelectionScreen.kt @@ -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 + ) + } + } + } + } +} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/SettingsScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/SettingsScreen.kt new file mode 100644 index 0000000000..2552c11dcd --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/SettingsScreen.kt @@ -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() +}